From 2de0595c981ce73cbc73c448dcce8b5f55e8a565 Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 15 Feb 2025 20:23:59 +0300 Subject: [PATCH] feat: added stats endpoints --- .../services/backend/api/v1/stats/__init__.py | 0 .../services/backend/api/v1/stats/schemas.py | 20 +++ .../services/backend/api/v1/stats/tests.py | 133 ++++++++++++++++++ .../services/backend/api/v1/stats/views.py | 77 ++++++++++ 4 files changed, 230 insertions(+) create mode 100644 solution/services/backend/api/v1/stats/__init__.py create mode 100644 solution/services/backend/api/v1/stats/schemas.py create mode 100644 solution/services/backend/api/v1/stats/tests.py create mode 100644 solution/services/backend/api/v1/stats/views.py diff --git a/solution/services/backend/api/v1/stats/__init__.py b/solution/services/backend/api/v1/stats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/services/backend/api/v1/stats/schemas.py b/solution/services/backend/api/v1/stats/schemas.py new file mode 100644 index 0000000..905efee --- /dev/null +++ b/solution/services/backend/api/v1/stats/schemas.py @@ -0,0 +1,20 @@ +from ninja import Schema + + +class Stat(Schema): + impressions_count: int + clicks_count: int + conversion: float + spent_impressions: float + spent_clicks: float + spent_total: float + + +class DailyStat(Schema): + impressions_count: int + clicks_count: int + conversion: float + spent_impressions: float + spent_clicks: float + spent_total: float + date: int diff --git a/solution/services/backend/api/v1/stats/tests.py b/solution/services/backend/api/v1/stats/tests.py new file mode 100644 index 0000000..2e60b8d --- /dev/null +++ b/solution/services/backend/api/v1/stats/tests.py @@ -0,0 +1,133 @@ +import uuid +from django.test import TestCase, Client, override_settings +from http import HTTPStatus as status +from apps.campaign.models import Advertiser, Campaign + + +class AdvertiserCampaignTestCase(TestCase): + @override_settings( + CACHES={ + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + } + } + ) + def setUp(self): + self.client = Client() + self.advertiser = Advertiser.objects.create(name="Test Advertiser") + self.campaign = Campaign.objects.create( + advertiser=self.advertiser, + impressions_limit=0, + clicks_limit=0, + cost_per_impression=0, + cost_per_click=0, + ad_title="title", + ad_text="text", + start_date=0, + end_date=0, + ) + + self.campaigns_prefix = "/stats/campaigns" + self.advertisers_prefix = "/stats/advertisers" + + def test_get_campaign_statistics_invalid_uuid(self): + response = self.client.get(f"{self.campaigns_prefix}/invalid-uuid") + + self.assertEqual(response.status_code, status.BAD_REQUEST) + + def test_get_campaign_statistics_campaign_not_found(self): + non_existent_campaign_id = uuid.uuid4() + response = self.client.get( + f"{self.campaigns_prefix}/{non_existent_campaign_id}" + ) + + self.assertEqual(response.status_code, status.NOT_FOUND) + + def test_get_campaign_statistics_success(self): + response = self.client.get( + f"{self.campaigns_prefix}/{self.campaign.id}" + ) + + self.assertEqual(response.status_code, status.OK) + self.assertIsInstance(response.json(), dict) + + def test_get_daily_campaign_statistics_invalid_uuid(self): + response = self.client.get( + f"{self.campaigns_prefix}/invalid-uuid/daily" + ) + + self.assertEqual(response.status_code, status.BAD_REQUEST) + + def test_get_daily_campaign_statistics_campaign_not_found(self): + non_existent_campaign_id = uuid.uuid4() + response = self.client.get( + f"{self.campaigns_prefix}/{non_existent_campaign_id}/daily" + ) + + self.assertEqual(response.status_code, status.NOT_FOUND) + + @override_settings( + CACHES={ + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + } + } + ) + def test_get_daily_campaign_statistics_success(self): + response = self.client.get( + f"{self.campaigns_prefix}/{self.campaign.id}/daily" + ) + + self.assertEqual(response.status_code, status.OK) + self.assertIsInstance(response.json(), list) + + def test_get_advertiser_statistics_invalid_uuid(self): + response = self.client.get(f"{self.advertisers_prefix}/invalid-uuid") + + self.assertEqual(response.status_code, status.BAD_REQUEST) + + def test_get_advertiser_statistics_not_found(self): + non_existent_advertiser_id = uuid.uuid4() + response = self.client.get( + f"{self.advertisers_prefix}/{non_existent_advertiser_id}" + ) + + self.assertEqual(response.status_code, status.NOT_FOUND) + + def test_get_advertiser_statistics_success(self): + response = self.client.get( + f"{self.advertisers_prefix}/{self.advertiser.id}" + ) + + self.assertEqual(response.status_code, status.OK) + self.assertIsInstance(response.json(), dict) + + def test_get_daily_advertiser_statistics_invalid_uuid(self): + response = self.client.get( + f"{self.advertisers_prefix}/invalid-uuid/campaigns/daily" + ) + + self.assertEqual(response.status_code, status.BAD_REQUEST) + + def test_get_daily_advertiser_statistics_advertiser_not_found(self): + non_existent_advertiser_id = uuid.uuid4() + response = self.client.get( + f"{self.advertisers_prefix}/{non_existent_advertiser_id}/campaigns/daily" + ) + + self.assertEqual(response.status_code, status.NOT_FOUND) + + @override_settings( + CACHES={ + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + } + } + ) + def test_get_daily_advertiser_statistics_success(self): + response = self.client.get( + f"{self.advertisers_prefix}/{self.advertiser.id}/campaigns/daily" + ) + + self.assertEqual(response.status_code, status.OK) + self.assertIsInstance(response.json(), list) diff --git a/solution/services/backend/api/v1/stats/views.py b/solution/services/backend/api/v1/stats/views.py new file mode 100644 index 0000000..c1eae63 --- /dev/null +++ b/solution/services/backend/api/v1/stats/views.py @@ -0,0 +1,77 @@ +from http import HTTPStatus as status +from typing import Any +from uuid import UUID + +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from ninja import Router + +from api.v1 import schemas as global_schemas +from api.v1.stats import schemas +from apps.campaign.models import Advertiser, Campaign + +router = Router(tags=["stats"]) + + +@router.get( + "/campaigns/{campaign_id}", + response={ + status.OK: schemas.Stat, + status.BAD_REQUEST: global_schemas.BadRequestError, + status.NOT_FOUND: global_schemas.NotFoundError, + }, +) +def get_campaign_statistics( + request: HttpRequest, campaign_id: UUID +) -> tuple[status, dict[str, Any]]: + campaign = get_object_or_404(Campaign, id=campaign_id) + + return status.OK, campaign.get_statistics() + + +@router.get( + "/campaigns/{campaign_id}/daily", + response={ + status.OK: list[schemas.DailyStat], + status.BAD_REQUEST: global_schemas.BadRequestError, + status.NOT_FOUND: global_schemas.NotFoundError, + }, +) +def get_daily_campaign_statistics( + request: HttpRequest, campaign_id: UUID +) -> tuple[status, dict[str, Any]]: + campaign = get_object_or_404(Campaign, id=campaign_id) + + return status.OK, campaign.get_daily_statistics() + + +@router.get( + "/advertisers/{advertiser_id}", + response={ + status.OK: schemas.Stat, + status.BAD_REQUEST: global_schemas.BadRequestError, + status.NOT_FOUND: global_schemas.NotFoundError, + }, +) +def get_advertiser_statistics( + request: HttpRequest, advertiser_id: UUID +) -> tuple[status, dict[str, Any]]: + advertiser = get_object_or_404(Advertiser, id=advertiser_id) + + return status.OK, advertiser.get_statistics() + + +@router.get( + "/advertisers/{advertiser_id}/campaigns/daily", + response={ + status.OK: list[schemas.DailyStat], + status.BAD_REQUEST: global_schemas.BadRequestError, + status.NOT_FOUND: global_schemas.NotFoundError, + }, +) +def get_daily_advertiser_statistics( + request: HttpRequest, advertiser_id: UUID +) -> tuple[status, dict[str, Any]]: + advertiser = get_object_or_404(Advertiser, id=advertiser_id) + + return status.OK, advertiser.get_daily_statistics()