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

Server can control multiple publisher placements #786

Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions adserver/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ class AdDecisionSerializer(serializers.Serializer):
# This purposefully doesn't use a URLField so we can disregard invalid values rather than rejecting them
url = serializers.CharField(max_length=256, required=False)

# The placement index (0-indexed)
# 1 or more means there's multiple placements on this page
index = serializers.IntegerField(required=False, min_value=0, max_value=9)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not have a max here? It seems unlikely, but I know the PSF has a use case where they're showing more than 9 images in a page.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I can remove it. I'd rather have a very large max (99 or even 999) than no max at all though if that makes sense though.


# Used to pass the actual ad viewer's data for targeting purposes
user_ip = serializers.IPAddressField(required=False)
user_ua = serializers.CharField(required=False)
Expand Down
1 change: 1 addition & 0 deletions adserver/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ def decision(self, request, data):
keywords=keywords,
campaign_types=serializer.validated_data.get("campaign_types"),
url=url,
placement_index=serializer.validated_data.get("index"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call the incoming variable placement_index as well to keep it consistent?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# Debugging parameters
ad_slug=serializer.validated_data.get("force_ad"),
campaign_slug=serializer.validated_data.get("force_campaign"),
Expand Down
12 changes: 12 additions & 0 deletions adserver/decisionengine/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ def __init__(self, request, placements, publisher, **kwargs):
log.debug("Publisher has hit their daily cap. publisher=%s", self.publisher)
self.campaign_types.remove(PAID_CAMPAIGN)

# The placement index (0-based) for this ad request
# 1+ means multiple ads on the same page
self.placement_index = kwargs.get("placement_index") or 0

# When set, only return a specific ad or ads from a campaign
self.ad_slug = kwargs.get("ad_slug")
self.campaign_slug = kwargs.get("campaign_slug")
Expand Down Expand Up @@ -173,6 +177,14 @@ def get_placement(self, advertisement):

def should_display_ads(self):
"""Whether to not display ads based on the user, request, or other settings."""
# Check if the publisher is allowed to have multiple placements
if not self.publisher.allow_multiple_placements and self.placement_index > 0:
log.info(
"Multiple placement request on publisher rejected. publisher=%s",
self.publisher,
)
return False

return True


Expand Down
29 changes: 29 additions & 0 deletions adserver/migrations/0087_publisher_allow_multiple_placements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.4 on 2023-09-21 22:20
from django.db import migrations
from django.db import models


class Migration(migrations.Migration):

dependencies = [
("adserver", "0086_region_topic_pricing"),
]

operations = [
migrations.AddField(
model_name="historicalpublisher",
name="allow_multiple_placements",
field=models.BooleanField(
default=False,
help_text="Can this publisher have multiple placements on the same pageview",
),
),
migrations.AddField(
model_name="publisher",
name="allow_multiple_placements",
field=models.BooleanField(
default=False,
help_text="Can this publisher have multiple placements on the same pageview",
),
),
]
5 changes: 5 additions & 0 deletions adserver/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,11 @@ class Publisher(TimeStampedModel, IndestructibleModel):
),
)

allow_multiple_placements = models.BooleanField(
default=False,
help_text=_("Can this publisher have multiple placements on the same pageview"),
)

# Payout information
skip_payouts = models.BooleanField(
_("Skip payouts"),
Expand Down
20 changes: 20 additions & 0 deletions adserver/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,26 @@ def test_sticky_decision(self):
# Clear the cache so this setting doesn't mess up the next test
cache.clear()

def test_multiple_placements(self):
self.query_params["index"] = 0
resp = self.client.get(self.url, self.query_params)
self.assertEqual(resp.status_code, 200)
self.assertTrue("id" in resp.json()) # Gets an ad successfully

# No ad since `publisher.allow_multiple_placements` is False (the default)
self.query_params["index"] = 1
resp = self.client.get(self.url, self.query_params)
self.assertEqual(resp.status_code, 200)
self.assertDictEqual(resp.json(), {})

self.publisher.allow_multiple_placements = True
self.publisher.save()

self.query_params["index"] = 1
resp = self.client.get(self.url, self.query_params)
self.assertEqual(resp.status_code, 200)
self.assertTrue("id" in resp.json()) # Gets an ad successfully


class AdvertiserApiTests(BaseApiTest):
def setUp(self):
Expand Down