Skip to content
Draft
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
104 changes: 99 additions & 5 deletions adserver/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
from ..models import Advertisement
from ..models import Advertiser
from ..models import Flight
from ..models import GeoImpression
from ..models import Publisher
from ..reports import AdvertiserGeoReport
from ..reports import AdvertiserPublisherReport
from ..reports import AdvertiserReport
from ..reports import PublisherReport
from ..utils import get_client_id
Expand Down Expand Up @@ -398,29 +401,120 @@ class AdvertiserViewSet(viewsets.ReadOnlyModelViewSet):
:>json array days: An array of advertiser results per day
:>json object total: An object of aggregated totals for the advertiser
:>json array flights: An array of flights for this advertiser in the time period

.. http:get:: /api/v1/advertisers/(str:slug)/geo_report/

Return a report of ad performance for this advertiser broken down by country.
This matches the geo report shown in the advertiser dashboard.

:query date start_date: Start the report on a given day inclusive.
If not specified, defaults to 30 days ago
:query date end_date: End the report on a given day inclusive.
If not specified, no end time is used (up to current)

:>json array results: An array of advertiser results per country.
Each result is indexed by the country name in the ``index`` field.
:>json object total: An object of aggregated totals for the advertiser

.. http:get:: /api/v1/advertisers/(str:slug)/publisher_report/

Return a report of ad performance for this advertiser broken down by publisher.
This matches the publisher report shown in the advertiser dashboard.

:query date start_date: Start the report on a given day inclusive.
If not specified, defaults to 30 days ago
:query date end_date: End the report on a given day inclusive.
If not specified, no end time is used (up to current)

:>json array results: An array of advertiser results per publisher.
Each result is indexed by the publisher name in the ``index`` field.
:>json object total: An object of aggregated totals for the advertiser
"""

serializer_class = AdvertiserSerializer
lookup_field = "slug"

# Columns returned for breakdown reports.
# This mirrors ``BaseReportView.fieldnames`` used by the dashboard CSV exports.
report_fields = ("index", "views", "clicks", "cost", "ctr", "ecpm")

def get_queryset(self):
"""Returns Advertisers the user has access to."""
if self.request.user.is_staff:
return Advertiser.objects.all()

return self.request.user.advertisers.all()

@action(detail=True, methods=["get"])
def report(self, request, slug=None): # pylint: disable=unused-argument
"""Return a report of ad performance for this advertiser."""
# This will raise a 404 if the user doesn't have access to the advertiser
advertiser = self.get_object()
def _date_range(self, request):
"""Parse the ``start_date``/``end_date`` query params used by the report actions."""
start_date = parse_date_string(request.query_params.get("start_date"))
end_date = parse_date_string(request.query_params.get("end_date"))

if not start_date:
start_date = timezone.now() - timedelta(days=30)

return start_date, end_date

def _serialize_report_row(self, row):
"""
Project a report row down to the JSON-serializable report columns.

This mirrors the dashboard CSV export (``BaseReportView``), which writes
the same fields and relies on the ``index`` value being stringified --
for the publisher report the raw index is a ``Publisher`` instance.
"""
serialized = {field: row.get(field) for field in self.report_fields}
serialized["index"] = str(serialized["index"])
return serialized

def _breakdown_report(self, request, report_class, model):
"""
Generate a breakdown report for the requested advertiser.

This powers the granular ``geo_report`` and ``publisher_report``
actions which break performance down by a single dimension
(country or publisher).
"""
# This will raise a 404 if the user doesn't have access to the advertiser
advertiser = self.get_object()
start_date, end_date = self._date_range(request)

queryset = model.objects.filter(
advertisement__flight__campaign__advertiser=advertiser,
date__gte=start_date,
)
if end_date:
queryset = queryset.filter(date__lte=end_date)

report = report_class(queryset)
report.generate()

return Response(
{
"total": self._serialize_report_row(report.total),
"results": [
self._serialize_report_row(result) for result in report.results
],
}
)

@action(detail=True, methods=["get"])
def geo_report(self, request, slug=None): # pylint: disable=unused-argument
"""Return a report of ad performance for this advertiser broken down by country."""
return self._breakdown_report(request, AdvertiserGeoReport, GeoImpression)

@action(detail=True, methods=["get"])
def publisher_report(self, request, slug=None): # pylint: disable=unused-argument
"""Return a report of ad performance for this advertiser broken down by publisher."""
return self._breakdown_report(request, AdvertiserPublisherReport, AdImpression)

@action(detail=True, methods=["get"])
def report(self, request, slug=None): # pylint: disable=unused-argument
"""Return a report of ad performance for this advertiser."""
# This will raise a 404 if the user doesn't have access to the advertiser
advertiser = self.get_object()
start_date, end_date = self._date_range(request)

queryset = AdImpression.objects.filter(
advertisement__flight__campaign__advertiser=advertiser
).filter(date__gte=start_date)
Expand Down
98 changes: 97 additions & 1 deletion adserver/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from ..models import Campaign
from ..models import Click
from ..models import Flight
from ..models import GeoImpression
from ..models import Offer
from ..models import Publisher
from ..models import PublisherGroup
Expand Down Expand Up @@ -901,6 +902,12 @@ def setUp(self):
self.advertiser2_report_url = reverse(
"api:advertisers-report", args=[self.advertiser2.slug]
)
self.advertiser1_geo_report_url = reverse(
"api:advertisers-geo-report", args=[self.advertiser1.slug]
)
self.advertiser1_publisher_report_url = reverse(
"api:advertisers-publisher-report", args=[self.advertiser1.slug]
)

def test_advertiser_access(self):
# User has access to advertiser1 but not advertiser2
Expand All @@ -912,7 +919,12 @@ def test_advertiser_access(self):
self.assertEqual(data["count"], 1)
self.assertEqual(data["results"][0]["slug"], self.advertiser1.slug)

for url in (self.advertiser1_detail_url, self.advertiser1_report_url):
for url in (
self.advertiser1_detail_url,
self.advertiser1_report_url,
self.advertiser1_geo_report_url,
self.advertiser1_publisher_report_url,
):
resp = self.client.get(url, content_type="application/json")
self.assertEqual(resp.status_code, 200, resp.content)

Expand Down Expand Up @@ -998,6 +1010,90 @@ def test_advertiser_report(self):
)
self.assertEqual(resp.status_code, 200, resp.content)

def test_advertiser_geo_report(self):
# No data yet
resp = self.client.get(
self.advertiser1_geo_report_url, content_type="application/json"
)
self.assertEqual(resp.status_code, 200, resp.content)
data = resp.json()
self.assertEqual(data["results"], [])
self.assertEqual(data["total"]["views"], 0)
self.assertEqual(data["total"]["clicks"], 0)

get(
GeoImpression,
advertisement=self.ad,
country="US",
date=timezone.now().date(),
views=100,
clicks=10,
)
get(
GeoImpression,
advertisement=self.ad,
country="CA",
date=timezone.now().date(),
views=50,
clicks=2,
)

resp = self.client.get(
self.advertiser1_geo_report_url, content_type="application/json"
)
self.assertEqual(resp.status_code, 200, resp.content)
data = resp.json()
self.assertEqual(data["total"]["views"], 150)
self.assertEqual(data["total"]["clicks"], 12)
self.assertEqual(len(data["results"]), 2)
# Ordered by views descending - the US row is first.
# The breakdown label is the country name in the "index" field.
self.assertEqual(data["results"][0]["index"], "United States of America")
self.assertEqual(data["results"][0]["views"], 100)
self.assertEqual(data["results"][0]["clicks"], 10)
# The flight CPC is 1.0 so cost == clicks
self.assertAlmostEqual(data["results"][0]["cost"], 10.0)

def test_advertiser_publisher_report(self):
# No data yet
resp = self.client.get(
self.advertiser1_publisher_report_url, content_type="application/json"
)
self.assertEqual(resp.status_code, 200, resp.content)
data = resp.json()
self.assertEqual(data["results"], [])
self.assertEqual(data["total"]["views"], 0)

self.ad.incr(VIEWS, self.publisher1)
self.ad.incr(VIEWS, self.publisher1)
self.ad.incr(CLICKS, self.publisher1)
self.ad.incr(VIEWS, self.publisher2)

resp = self.client.get(
self.advertiser1_publisher_report_url, content_type="application/json"
)
self.assertEqual(resp.status_code, 200, resp.content)
data = resp.json()
self.assertEqual(data["total"]["views"], 3)
self.assertEqual(data["total"]["clicks"], 1)
self.assertEqual(len(data["results"]), 2)
# Results are broken down by publisher, labeled in the "index" field
self.assertEqual(data["results"][0]["index"], str(self.publisher1))
self.assertEqual(data["results"][0]["views"], 2)
self.assertEqual(data["results"][0]["clicks"], 1)

# Respects the start_date filter
start_date = (timezone.now() + datetime.timedelta(days=3)).strftime("%Y-%m-%d")
resp = self.client.get(
self.advertiser1_publisher_report_url,
data={"start_date": start_date},
content_type="application/json",
)
self.assertEqual(resp.status_code, 200, resp.content)
data = resp.json()
self.assertEqual(data["results"], [])
self.assertEqual(data["total"]["views"], 0)


class PublisherApiTests(BaseApiTest):
def setUp(self):
Expand Down
Loading