Skip to content

Commit

Permalink
Add campaigns view (#93)
Browse files Browse the repository at this point in the history
- Add get all campaigns endpoint
- Add get campaign by resource id
  • Loading branch information
moisses89 authored May 15, 2024
1 parent 5077ddd commit fc5ac8c
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 7 deletions.
4 changes: 4 additions & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
"",
include("safe_locking_service.locking_events.urls", namespace="locking_events"),
),
path(
"",
include("safe_locking_service.campaigns.urls", namespace="locking_campaigns"),
),
]

urlpatterns = swagger_urlpatterns + [
Expand Down
27 changes: 27 additions & 0 deletions safe_locking_service/campaigns/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from rest_framework import serializers

from safe_locking_service.campaigns.models import Campaign


class ActivityMetadataSerializer(serializers.Serializer):
name = serializers.CharField()
description = serializers.CharField()
max_points = serializers.IntegerField()


class CampaignSerializer(serializers.Serializer):
resource_id = serializers.SerializerMethodField()
name = serializers.CharField()
description = serializers.CharField()
start_date = serializers.DateTimeField()
end_date = serializers.DateTimeField()
last_updated = serializers.SerializerMethodField()
activities_metadata = ActivityMetadataSerializer(
many=True, source="activity_metadata"
)

def get_resource_id(self, obj: Campaign):
return obj.uuid

def get_last_updated(self, obj: Campaign):
return obj.last_updated
33 changes: 27 additions & 6 deletions safe_locking_service/campaigns/tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
import factory
from django.utils import timezone

from factory import Faker, LazyFunction, SubFactory
from factory.django import DjangoModelFactory

from ..models import Campaign
from ..models import ActivityMetadata, Campaign, Period


class CampaignFactory(DjangoModelFactory):
class Meta:
model = Campaign

name = factory.Faker("catch_phrase")
description = factory.Faker("bs")
start_date = factory.Faker("date_time")
end_date = factory.Faker("date_time")
name = Faker("catch_phrase")
description = Faker("bs")
start_date = LazyFunction(timezone.now)
end_date = LazyFunction(timezone.now)


class PeriodFactory(DjangoModelFactory):
class Meta:
model = Period

campaign = SubFactory(CampaignFactory)
start_date = LazyFunction(lambda: timezone.now().date())
end_date = LazyFunction(lambda: timezone.now().date())


class ActivityMetadataFactory(DjangoModelFactory):
class Meta:
model = ActivityMetadata

campaign = SubFactory(CampaignFactory)
name = Faker("catch_phrase")
description = Faker("bs")
max_points = Faker("pyint")
163 changes: 163 additions & 0 deletions safe_locking_service/campaigns/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import uuid
from datetime import timedelta

from django.test import TestCase
from django.urls import reverse
from django.utils import timezone

from rest_framework import status

from ...campaigns.tests.factories import (
ActivityMetadataFactory,
CampaignFactory,
PeriodFactory,
)
from ...utils.timestamp_helper import get_formated_timestamp


class TestCampaignViews(TestCase):
def test_empty_campaigns_view(self):
url = reverse("v1:locking_campaigns:list-campaigns")
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
# No campaigns
response_json = response.json()
self.assertEqual(response_json["count"], 0)

def test_campaign_view(self):
url = reverse("v1:locking_campaigns:list-campaigns")
# Add campaign, one activity and 2 periods
campaign_expected = CampaignFactory()
activity = ActivityMetadataFactory(campaign=campaign_expected)
previous_day = timezone.now().date() - timedelta(days=1)
PeriodFactory(
campaign=campaign_expected, start_date=previous_day, end_date=previous_day
)
period_last = PeriodFactory(campaign=campaign_expected)
response = self.client.get(url, format="json")
response_json = response.json()
self.assertEqual(len(response_json["results"]), 1)
campaign_response = response_json["results"][0]
self.assertEqual(
campaign_response.get("resourceId"), str(campaign_expected.uuid)
)
self.assertEqual(campaign_response.get("name"), campaign_expected.name)
self.assertEqual(
campaign_response.get("description"), campaign_expected.description
)
self.assertEqual(
campaign_response.get("startDate"),
get_formated_timestamp(campaign_expected.start_date),
)
self.assertEqual(
campaign_response.get("endDate"),
get_formated_timestamp(campaign_expected.end_date),
)

# LastUpdated should be the end_date of the last period
self.assertEqual(
campaign_response.get("lastUpdated"),
get_formated_timestamp(period_last.end_date),
)
self.assertEqual(len(campaign_response.get("activitiesMetadata")), 1)
self.assertEqual(
campaign_response.get("activitiesMetadata")[0]["name"], activity.name
)

def test_no_activities_periods_campaigns_view(self):
# Add a campaign without activities and without period
url = reverse("v1:locking_campaigns:list-campaigns")
campaign_expected = CampaignFactory()
response = self.client.get(url, format="json")
response_json = response.json()
self.assertEqual(len(response_json["results"]), 1)
campaign_response = response_json["results"][0]
self.assertEqual(
campaign_response.get("resourceId"), str(campaign_expected.uuid)
)
self.assertEqual(campaign_response.get("name"), campaign_expected.name)
self.assertEqual(
campaign_response.get("description"), campaign_expected.description
)
self.assertEqual(
campaign_response.get("startDate"),
get_formated_timestamp(campaign_expected.start_date),
)
self.assertEqual(
campaign_response.get("endDate"),
get_formated_timestamp(campaign_expected.end_date),
)
self.assertIsNone(campaign_response.get("lastUpdated"))
self.assertIsInstance(campaign_response.get("activitiesMetadata"), list)
self.assertEqual(len(campaign_response.get("activitiesMetadata")), 0)

def test_sort_campaign_view(self):
url = reverse("v1:locking_campaigns:list-campaigns")
previous_day = timezone.now().date() - timedelta(days=1)
CampaignFactory(start_date=previous_day, end_date=previous_day)
# Last campaign should be at the beginning
last_campaign = CampaignFactory()
response = self.client.get(url, format="json")
response_json = response.json()
self.assertEqual(len(response_json["results"]), 2)
campaign_response = response_json["results"][0]
self.assertEqual(campaign_response.get("resourceId"), str(last_campaign.uuid))
self.assertEqual(campaign_response.get("name"), last_campaign.name)
self.assertEqual(
campaign_response.get("description"), last_campaign.description
)
self.assertEqual(
campaign_response.get("startDate"),
get_formated_timestamp(last_campaign.start_date),
)
self.assertEqual(
campaign_response.get("endDate"),
get_formated_timestamp(last_campaign.end_date),
)
self.assertIsNone(campaign_response.get("lastUpdated"))
self.assertIsInstance(campaign_response.get("activitiesMetadata"), list)
self.assertEqual(len(campaign_response.get("activitiesMetadata")), 0)

def test_retrieve_campaign_view(self):
resource_id = uuid.uuid4()
response = self.client.get(
reverse("v1:locking_campaigns:retrieve-campaign", args=(resource_id,)),
format="json",
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

campaign_expected = CampaignFactory()
activity_expected = ActivityMetadataFactory(campaign=campaign_expected)
period_expected = PeriodFactory(campaign=campaign_expected)
resource_id = campaign_expected.uuid

response = self.client.get(
reverse("v1:locking_campaigns:retrieve-campaign", args=(resource_id,)),
format="json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
campaign_response = response.json()
self.assertEqual(
campaign_response.get("resourceId"), str(campaign_expected.uuid)
)
self.assertEqual(campaign_response.get("name"), campaign_expected.name)
self.assertEqual(
campaign_response.get("description"), campaign_expected.description
)
self.assertEqual(
campaign_response.get("startDate"),
get_formated_timestamp(campaign_expected.start_date),
)
self.assertEqual(
campaign_response.get("endDate"),
get_formated_timestamp(campaign_expected.end_date),
)
self.assertEqual(
campaign_response.get("lastUpdated"),
get_formated_timestamp(period_expected.end_date),
)
self.assertEqual(len(campaign_response.get("activitiesMetadata")), 1)
self.assertEqual(
campaign_response.get("activitiesMetadata")[0]["name"],
activity_expected.name,
)
14 changes: 14 additions & 0 deletions safe_locking_service/campaigns/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.urls import path

from . import views

app_name = "campaigns"

urlpatterns = [
path("campaigns/", views.CampaignsView.as_view(), name="list-campaigns"),
path(
"campaigns/<str:resource_id>/",
views.RetrieveCampaignView.as_view(),
name="retrieve-campaign",
),
]
62 changes: 62 additions & 0 deletions safe_locking_service/campaigns/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from django.db.models import Max
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

from rest_framework import status
from rest_framework.generics import ListAPIView, RetrieveAPIView
from rest_framework.response import Response

from safe_locking_service.campaigns.serializers import CampaignSerializer
from safe_locking_service.locking_events.pagination import SmallPagination

from .models import Campaign


class CampaignsView(ListAPIView):
"""
Returns a paginated list of campaigns.
"""

pagination_class = SmallPagination
serializer_class = CampaignSerializer

def get_queryset(self):
return (
Campaign.objects.prefetch_related("activity_metadata")
.annotate(last_updated=Max("periods__end_date"))
.order_by("-start_date")
)

@method_decorator(cache_page(1 * 60)) # 1 minute
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.serializer_class(queryset, many=True)
paginated_data = self.paginate_queryset(serializer.data)
return self.get_paginated_response(paginated_data)


class RetrieveCampaignView(RetrieveAPIView):
"""
Returns a campaign for the provided campaign_id.
"""

serializer_class = CampaignSerializer

def get_queryset(self, resource_id: int):
return (
Campaign.objects.filter(uuid=resource_id)
.prefetch_related("activity_metadata")
.annotate(last_updated=Max("periods__end_date"))
)

@method_decorator(cache_page(1 * 60)) # 1 minute
def get(self, request, *args, **kwargs):
resource_id = kwargs["resource_id"]
queryset = self.get_queryset(resource_id)
if not queryset:
return Response(
status=status.HTTP_404_NOT_FOUND,
)

serializer = self.serializer_class(queryset[0])
return Response(status=status.HTTP_200_OK, data=serializer.data)
4 changes: 3 additions & 1 deletion safe_locking_service/locking_events/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
Uint96Field,
)

from safe_locking_service.utils.timestamp_helper import get_formated_timestamp


class LeaderBoardRow(TypedDict):
position: int
Expand Down Expand Up @@ -101,7 +103,7 @@ def get_serialized_timestamp(self) -> str:
:return: serialized timestamp
"""
return self.timestamp.isoformat().replace("+00:00", "Z")
return get_formated_timestamp(self.timestamp)

class Meta:
abstract = True
Expand Down
10 changes: 10 additions & 0 deletions safe_locking_service/utils/timestamp_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import datetime


def get_formated_timestamp(timestamp: datetime):
"""
:param timestamp:
:return: return formatted timestamp YYYY-MM-DDTHH:MM:SSZ
"""
return timestamp.isoformat().replace("+00:00", "Z")

0 comments on commit fc5ac8c

Please sign in to comment.