Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offers now store if they are paid ad eligible #797

Merged
merged 1 commit into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion adserver/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,8 @@ class AdBaseAdmin(RemoveDeleteMixin, admin.ModelAdmin):
"browser_family",
"os_family",
"is_mobile",
"is_bot",
"is_proxy",
"paid_eligible",
"user_agent",
"ip",
"div_id",
Expand All @@ -930,6 +931,8 @@ class AdBaseAdmin(RemoveDeleteMixin, admin.ModelAdmin):
list_select_related = ("advertisement", "publisher")
list_filter = (
"is_mobile",
"is_proxy",
"paid_eligible",
"publisher",
"advertisement__flight__campaign__advertiser",
)
Expand Down
32 changes: 24 additions & 8 deletions adserver/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from rest_framework.views import APIView
from rest_framework_jsonp.renderers import JSONPRenderer

from ..constants import PAID_CAMPAIGN
from ..decisionengine import get_ad_decision_backend
from ..models import AdImpression
from ..models import Advertisement
Expand Down Expand Up @@ -155,7 +156,9 @@ class AdDecisionView(GeoIpMixin, APIView):
permission_classes = (AdDecisionPermission,)
renderer_classes = (JSONRenderer, JSONPRenderer)

def _prepare_response(self, ad, placement, publisher, keywords, url, forced=False):
def _prepare_response(
self, ad, placement, publisher, keywords, url, forced=False, paid_eligible=False
):
"""
Wrap `offer_ad` with the placement for the publisher.

Expand All @@ -180,6 +183,7 @@ def _prepare_response(self, ad, placement, publisher, keywords, url, forced=Fals
div_id=div_id,
keywords=keywords,
url=url,
paid_eligible=paid_eligible,
)
return {}

Expand All @@ -191,6 +195,7 @@ def _prepare_response(self, ad, placement, publisher, keywords, url, forced=Fals
keywords=keywords,
url=url,
forced=forced,
paid_eligible=paid_eligible,
)
log.debug(
"Offering ad. publisher=%s ad_type=%s div_id=%s keywords=%s",
Expand Down Expand Up @@ -275,26 +280,41 @@ def decision(self, request, data):
:return: An add decision (JSON) or an empty JSON dict
"""
serializer = AdDecisionSerializer(data=data)
forced = False

if serializer.is_valid():
publisher = serializer.validated_data["publisher"]
self.check_object_permissions(request, publisher)
url = serializer.validated_data.get("url")
keywords = serializer.validated_data.get("keywords")
campaign_types = serializer.validated_data.get("campaign_types")

forced = False
paid_eligible = False

# Ignore keywords from the API for certain publishers
if not publisher.allow_api_keywords:
keywords = []

if serializer.validated_data.get(
"force_ad"
) or serializer.validated_data.get("force_campaign"):
forced = True

if (
not forced
and publisher.allow_paid_campaigns
and (not campaign_types or PAID_CAMPAIGN in campaign_types)
):
paid_eligible = True

backend = get_ad_decision_backend()(
# Required parameters
request=request,
placements=serializer.validated_data["placements"],
publisher=publisher,
# Optional parameters
keywords=keywords,
campaign_types=serializer.validated_data.get("campaign_types"),
campaign_types=campaign_types,
url=url,
placement_index=serializer.validated_data.get("placement_index"),
# Debugging parameters
Expand All @@ -303,11 +323,6 @@ def decision(self, request, data):
)
ad, placement = backend.get_ad_and_placement()

if serializer.validated_data.get(
"force_ad"
) or serializer.validated_data.get("force_campaign"):
forced = True

return Response(
self._prepare_response(
ad=ad,
Expand All @@ -317,6 +332,7 @@ def decision(self, request, data):
keywords=backend.keywords,
url=url,
forced=forced,
paid_eligible=paid_eligible,
)
)

Expand Down
55 changes: 55 additions & 0 deletions adserver/migrations/0089_paid_eligible_isproxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 4.2.4 on 2023-10-16 21:31
from django.db import migrations
from django.db import models


class Migration(migrations.Migration):

dependencies = [
("adserver", "0088_linked_discounts"),
]

operations = [
migrations.AddField(
model_name="click",
name="is_proxy",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="click",
name="paid_eligible",
field=models.BooleanField(
default=None,
help_text="Whether the impression was eligible for a paid ad",
null=True,
),
),
migrations.AddField(
model_name="offer",
name="is_proxy",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="offer",
name="paid_eligible",
field=models.BooleanField(
default=None,
help_text="Whether the impression was eligible for a paid ad",
null=True,
),
),
migrations.AddField(
model_name="view",
name="is_proxy",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="view",
name="paid_eligible",
field=models.BooleanField(
default=None,
help_text="Whether the impression was eligible for a paid ad",
null=True,
),
),
]
45 changes: 42 additions & 3 deletions adserver/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
from .utils import get_client_ip
from .utils import get_client_user_agent
from .utils import get_domain_from_url
from .utils import is_proxy_ip
from .validators import TargetingParametersValidator
from .validators import TopicPricingValidator
from .validators import TrafficFillValidator
Expand Down Expand Up @@ -1627,7 +1628,15 @@ def incr(self, impression_type, publisher):
)

def _record_base(
self, request, model, publisher, keywords, url, div_id, ad_type_slug
self,
request,
model,
publisher,
keywords,
url,
div_id,
ad_type_slug,
paid_eligible=False,
):
"""
Save the actual AdBase model to the database.
Expand Down Expand Up @@ -1665,11 +1674,13 @@ def _record_base(
client_id=client_id,
country=country,
url=url,
paid_eligible=paid_eligible,
# Derived user agent data
browser_family=parsed_ua.browser.family,
os_family=parsed_ua.os.family,
is_bot=parsed_ua.is_bot,
is_mobile=parsed_ua.is_mobile,
is_proxy=is_proxy_ip(ip_address),
# Client Data
keywords=keywords if keywords else None, # Don't save empty lists
div_id=div_id,
Expand Down Expand Up @@ -1699,6 +1710,7 @@ def track_click(self, request, publisher, offer):
url=offer.url,
div_id=offer.div_id,
ad_type_slug=offer.ad_type_slug,
paid_eligible=offer.paid_eligible,
)

def track_view(self, request, publisher, offer):
Expand All @@ -1725,6 +1737,7 @@ def track_view(self, request, publisher, offer):
url=offer.url,
div_id=offer.div_id,
ad_type_slug=offer.ad_type_slug,
paid_eligible=offer.paid_eligible,
)

log.debug("Not recording ad view.")
Expand All @@ -1750,7 +1763,15 @@ def track_view_time(self, offer, view_time):
return False

def offer_ad(
self, request, publisher, ad_type_slug, div_id, keywords, url=None, forced=False
self,
request,
publisher,
ad_type_slug,
div_id,
keywords,
url=None,
forced=False,
paid_eligible=False,
):
"""
Offer to display this ad on a specific publisher and a specific display (ad type).
Expand All @@ -1768,6 +1789,7 @@ def offer_ad(
url=url,
div_id=div_id,
ad_type_slug=ad_type_slug,
paid_eligible=paid_eligible,
)

if forced and self.flight.campaign.campaign_type == PAID_CAMPAIGN:
Expand Down Expand Up @@ -1832,7 +1854,16 @@ def offer_ad(
}

@classmethod
def record_null_offer(cls, request, publisher, ad_type_slug, div_id, keywords, url):
def record_null_offer(
cls,
request,
publisher,
ad_type_slug,
div_id,
keywords,
url,
paid_eligible=False,
):
"""
Store null offers, so that we can keep track of our fill rate.

Expand All @@ -1849,6 +1880,7 @@ def record_null_offer(cls, request, publisher, ad_type_slug, div_id, keywords, u
url=url,
div_id=div_id,
ad_type_slug=ad_type_slug,
paid_eligible=paid_eligible,
)

def is_valid_offer(self, impression_type, offer):
Expand Down Expand Up @@ -2386,6 +2418,12 @@ class AdBase(TimeStampedModel, IndestructibleModel):
on_delete=models.PROTECT,
)

paid_eligible = models.BooleanField(
help_text=_("Whether the impression was eligible for a paid ad"),
default=None,
null=True,
)

# User Data
ip = models.GenericIPAddressField(_("Ip Address")) # anonymized
user_agent = models.CharField(
Expand Down Expand Up @@ -2419,6 +2457,7 @@ class AdBase(TimeStampedModel, IndestructibleModel):

is_bot = models.BooleanField(default=False)
is_mobile = models.BooleanField(default=False)
is_proxy = models.BooleanField(default=False)
is_refunded = models.BooleanField(default=False)

impression_type = None
Expand Down
9 changes: 8 additions & 1 deletion adserver/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,9 @@ def test_force_ad_counted(self):
self.assertTrue("id" in resp.json())
self.assertEqual(resp.json()["id"], "ad-slug")
self.proxy_client.get(resp.json()["view_url"])
self.assertFalse(self.ad.offers.first().viewed)
offer = self.ad.offers.first()
self.assertFalse(offer.viewed)
self.assertFalse(offer.paid_eligible)

# House ads are counted even when forced
self.ad.flight.campaign.campaign_type = "house"
Expand Down Expand Up @@ -615,6 +617,9 @@ def test_campaign_types(self):
self.assertEqual(resp_json["id"], "ad-slug", resp_json)
self.assertEqual(resp_json["campaign_type"], PAID_CAMPAIGN)

offer = Offer.objects.get(pk=resp_json["nonce"])
self.assertTrue(offer.paid_eligible)

# Try community only
data["campaign_types"] = [COMMUNITY_CAMPAIGN]
resp = self.client.post(
Expand All @@ -633,6 +638,8 @@ def test_campaign_types(self):
resp_json = resp.json()
self.assertEqual(resp_json["id"], "ad-slug", resp_json)
self.assertEqual(resp_json["campaign_type"], COMMUNITY_CAMPAIGN)
offer = Offer.objects.get(pk=resp_json["nonce"])
self.assertFalse(offer.paid_eligible)

# Try multiple campaign types
data["campaign_types"] = [PAID_CAMPAIGN, HOUSE_CAMPAIGN]
Expand Down