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

Advertiser domain report #968

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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
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">
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to test this behind a staff flag before rolling it out more widely?

<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
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:
Copy link
Member

Choose a reason for hiding this comment

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

This is a nice affordance.

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
Loading