Skip to content

Commit

Permalink
Merge pull request #797 from readthedocs/davidfischer/paid-eligible
Browse files Browse the repository at this point in the history
Offers now store if they are paid ad eligible
  • Loading branch information
davidfischer authored Oct 26, 2023
2 parents aab5ec2 + acf169e commit 7ac49be
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 13 deletions.
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

0 comments on commit 7ac49be

Please sign in to comment.