Skip to content

Commit

Permalink
Merge branch 'main' into rel
Browse files Browse the repository at this point in the history
  • Loading branch information
davidfischer committed Jan 8, 2025
2 parents 5ca35e2 + 36d287c commit a637fec
Show file tree
Hide file tree
Showing 18 changed files with 311 additions and 73 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ CHANGELOG
.. This is included by docs/developer/changelog.rst
Version v5.13.0
---------------

This release has some minor bug fixes.
The main difference is to add a per-domain report for advertisers
to show more visibility on where their ads are appearing.
We are also switching to UUID7s from UUID4s for the few places we use UUIDs.

:Date: January 8, 2025

* @davidfischer: Fix typo in publisher-placement report (#973)
* @davidfischer: Switch to UUID7s (#971)
* @davidfischer: Advertiser domain report (#968)
* @davidfischer: Only report on domains with 1 view (#967)
* @JasonBarnabe: Geo report should include revenue per country (#962)


Version v5.12.0
---------------

Expand Down
29 changes: 29 additions & 0 deletions adserver/migrations/0102_switch_uuid7.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 5.0.10 on 2025-01-02 21:27

import uuid_utils.compat
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('adserver', '0101_domainimpression_aggregation'),
]

operations = [
migrations.AlterField(
model_name='historicalpublisherpayout',
name='id',
field=models.UUIDField(db_index=True, default=uuid_utils.compat.uuid7, editable=False),
),
migrations.AlterField(
model_name='offer',
name='id',
field=models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='publisherpayout',
name='id',
field=models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False),
),
]
6 changes: 3 additions & 3 deletions adserver/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
import logging
import math
import re
import uuid
from collections import Counter

import bleach
import djstripe.models as djstripe_models
import pytz
import uuid_utils.compat as uuid
from django.conf import settings
from django.core.cache import cache
from django.core.cache import caches
Expand Down Expand Up @@ -2689,7 +2689,7 @@ class Offer(AdBase):
MAX_VIEW_TIME = 5 * 60 # seconds

# Use an ok user-facing pk value
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
id = models.UUIDField(primary_key=True, default=uuid.uuid7, editable=False)

advertisement = models.ForeignKey(
Advertisement,
Expand Down Expand Up @@ -2777,7 +2777,7 @@ class Meta:
class PublisherPayout(TimeStampedModel):
"""Details on historical publisher payouts."""

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
id = models.UUIDField(primary_key=True, default=uuid.uuid7, editable=False)
publisher = models.ForeignKey(
Publisher, related_name="payouts", on_delete=models.PROTECT
)
Expand Down
16 changes: 16 additions & 0 deletions adserver/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .constants import PAID_CAMPAIGN
from .models import AdImpression
from .models import AdvertiserImpression
from .models import DomainImpression
from .models import GeoImpression
from .models import KeywordImpression
from .models import PlacementImpression
Expand Down Expand Up @@ -86,6 +87,9 @@ def get_index_display(self, index):
"""Used to add display logic the index field."""
return index

def get_index_header(self):
return "Day (UTC)"

def generate(self):
raise NotImplementedError("Subclasses implement this method")

Expand Down Expand Up @@ -188,6 +192,18 @@ class AdvertiserPublisherReport(AdvertiserReport):
select_related_fields = ("advertisement", "advertisement__flight", "publisher")


class AdvertiserDomainReport(AdvertiserReport):
"""Report to breakdown advertiser performance by domain where the ad appears."""

model = DomainImpression
index = "domain"
order = "-views"
select_related_fields = ("advertisement", "advertisement__flight")

def get_index_header(self):
return self.index.title()


class PublisherReport(BaseReport):
"""Report for showing daily ad performance for a publisher."""

Expand Down
1 change: 1 addition & 0 deletions adserver/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ def daily_update_domains(day=None):
total_clicks=Count("domain", filter=Q(clicked=True)),
)
.exclude(domain__isnull=True)
.filter(total_views__gt=0)
.order_by("-total_decisions")
.values(
"advertisement",
Expand Down
10 changes: 10 additions & 0 deletions adserver/templates/adserver/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ <h6 class="text-muted">{{ advertiser }}</h6>
</a>
</li>

<li class="nav-item">
<a
class="nav-link"
href="{% url 'advertiser_domain_report' advertiser.slug %}"
>
<span class="fa fa-laptop fa-fw ml-4 text-muted" aria-hidden="true"></span>
<span>{% trans 'Domains' %}</span>
</a>
</li>

<li class="nav-item">
<a class="nav-link" href="{% url 'advertiser_users' advertiser.slug %}">
<span class="fa fa-users fa-fw mr-2 text-muted" aria-hidden="true"></span>
Expand Down
46 changes: 46 additions & 0 deletions adserver/templates/adserver/reports/advertiser-domain.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{% extends "adserver/reports/advertiser.html" %}
{% load humanize %}
{% load i18n %}


{% block title %}{% trans 'Advertiser Domain Report' %} - {{ advertiser }}{% endblock %}


{% block heading %}
{% blocktrans %}Advertiser Domain Report for {{ advertiser }}{% endblocktrans %}
{% endblock heading %}

{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item active">{% trans 'Advertiser Domain Report' %}</li>
{% endblock breadcrumbs %}


{% block additional_filters %}
{{ block.super }}

<div class="col-xl-3 col-md-6 col-12 mb-3">
<label class="col-form-label" for="id_flight">{% trans 'Flight' %}</label>
<select class="form-control" name="flight" id="id_flight">
<option value="">{% trans 'All flights' %}</option>
{% for flight in flights %}
<option value="{{ flight.slug }}"{% if flight.slug == request.GET.flight %} selected{% endif %}>{{ flight.name }}</option>
{% endfor %}
</select>
</div>

{% endblock additional_filters %}


{% block explainer %}
<section class="mb-5">
<h3>{% trans 'About this report' %}</h3>
<p>{% trans 'This report shows the top domains where your ads are shown.' %}</p>
<em>
{% blocktrans %}This report shows the <strong>top {{ limit }} domains</strong> and updates daily. All previous days data is complete.{% endblocktrans %}
</em>
</section>
{% endblock explainer %}


{% block report %}{% endblock report %}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<table class="table table-hover report">
<thead>
<tr>
<th><strong>{% trans 'Day (UTC)' %}</strong></th>
<th><strong>{{ report.get_index_header }}</strong></th>
<th class="text-right"><strong>{% trans 'Views' %}</strong></th>
<th class="text-right"><strong>{% trans 'Clicks' %}</strong></th>
<th class="text-right"><strong>{% trans 'Cost' %}</strong></th>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<h3>About this report</h3>
<p>
This report allows you to break down your different ad placements and view reports on them.
You can use this to optomize the different placement on your site,
You can use this to optimize the different placement on your site,
and better understand how each one is working to be able to improve them.
</p>
<p>
Expand Down
52 changes: 52 additions & 0 deletions adserver/tests/test_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@
from ..models import Advertiser
from ..models import AdvertiserImpression
from ..models import Campaign
from ..models import DomainImpression
from ..models import Flight
from ..models import Offer
from ..models import Publisher
from ..models import PublisherPaidImpression
from ..reports import AdvertiserDomainReport
from ..reports import AdvertiserReport
from ..reports import OptimizedAdvertiserReport
from ..reports import OptimizedPublisherPaidReport
from ..reports import PublisherGeoReport
from ..reports import PublisherReport
from ..tasks import daily_update_advertisers
from ..tasks import daily_update_domains
from ..tasks import daily_update_geos
from ..tasks import daily_update_impressions
from ..tasks import daily_update_keywords
Expand Down Expand Up @@ -464,6 +467,55 @@ def test_advertiser_publisher_report_contents(self):
response = self.client.get(export_url)
self.assertContains(response, "Total,3")

def test_advertiser_domain_report_contents(self):
get(
Offer,
advertisement=self.ad1,
publisher=self.publisher1,
viewed=True,
domain="example.com",
)
get(
Offer,
advertisement=self.ad1,
publisher=self.publisher2,
viewed=True,
clicked=True,
domain="example.com",
)
get(
Offer,
advertisement=self.ad1,
publisher=self.publisher2,
viewed=True,
clicked=False,
domain="example2.com",
)

# Update reporting
daily_update_domains()

url = reverse("advertiser_domain_report", args=[self.advertiser1.slug])

# Anonymous
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertTrue(response["location"].startswith("/accounts/login/"))

self.client.force_login(self.staff_user)

response = self.client.get(url)
self.assertContains(response, "example.com")
self.assertContains(response, "example2.com")

report = AdvertiserDomainReport(DomainImpression.objects.filter(advertisement=self.ad1))
report.generate()

# Check the actual data
self.assertEqual(len(report.results), 2)
self.assertAlmostEqual(report.total["views"], 3)
self.assertAlmostEqual(report.total["clicks"], 1)

def test_advertiser_keyword_report(self):
url = reverse("advertiser_keyword_report", args=[self.advertiser1.slug])

Expand Down
6 changes: 6 additions & 0 deletions adserver/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .views import AdvertiserAuthorizedUsersInviteView
from .views import AdvertiserAuthorizedUsersRemoveView
from .views import AdvertiserAuthorizedUsersView
from .views import AdvertiserDomainReportView
from .views import AdvertiserFlightReportView
from .views import AdvertiserGeoReportView
from .views import AdvertiserKeywordReportView
Expand Down Expand Up @@ -194,6 +195,11 @@
AdvertiserTopicReportView.as_view(),
name="advertiser_topic_report",
),
path(
r"advertiser/<slug:advertiser_slug>/report/domains/",
AdvertiserDomainReportView.as_view(),
name="advertiser_domain_report",
),
path(
r"advertiser/<slug:advertiser_slug>/flights/",
FlightListView.as_view(),
Expand Down
57 changes: 57 additions & 0 deletions adserver/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
from .models import Advertiser
from .models import AdvertiserImpression
from .models import Campaign
from .models import DomainImpression
from .models import Flight
from .models import GeoImpression
from .models import KeywordImpression
Expand All @@ -107,6 +108,7 @@
from .models import RegionTopicImpression
from .models import Topic
from .models import UpliftImpression
from .reports import AdvertiserDomainReport
from .reports import AdvertiserPublisherReport
from .reports import AdvertiserReport
from .reports import OptimizedAdvertiserReport
Expand Down Expand Up @@ -1639,6 +1641,61 @@ def get_context_data(self, **kwargs):
return context


class AdvertiserDomainReportView(AdvertiserAccessMixin, BaseReportView):
LIMIT = 50
DATA_COLLECTION_START_DATE = datetime(
year=2024, month=12, day=1, tzinfo=timezone.get_current_timezone()
)

impression_model = DomainImpression
template_name = "adserver/reports/advertiser-domain.html"

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

advertiser_slug = kwargs.get("advertiser_slug", "")
advertiser = get_object_or_404(Advertiser, slug=advertiser_slug)

flight_slug = self.request.GET.get("flight", "")
flight = Flight.objects.filter(
campaign__advertiser=advertiser, slug=flight_slug
).first()

if context["start_date"] < self.DATA_COLLECTION_START_DATE:
messages.info(
self.request,
_(
"Data for the domain report started being collected in %s. Data for this date range may be incomplete."
)
% (self.DATA_COLLECTION_START_DATE.strftime("%B %Y")),
)

queryset = self.get_queryset(
advertiser=advertiser,
flight=flight,
start_date=context["start_date"],
end_date=context["end_date"],
)

report = AdvertiserDomainReport(
queryset,
max_results=self.LIMIT,
)
report.generate()

context.update(
{
"advertiser": advertiser,
"report": report,
"flights": Flight.objects.filter(
campaign__advertiser=advertiser
).order_by("-start_date"),
}
)

return context


class StaffAdvertiserReportView(BaseReportView):
"""A report aggregating all advertisers."""

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ethical-ad-server",
"version": "5.12.0",
"version": "5.13.0",
"description": "",
"main": "index.js",
"engines": {
Expand Down
Loading

0 comments on commit a637fec

Please sign in to comment.