Skip to content

Commit

Permalink
Merge pull request #71 from akvo/feature/70-build-statistics-page
Browse files Browse the repository at this point in the history
Feature/70 build statistics page
  • Loading branch information
ifirmawan authored Oct 11, 2024
2 parents a39f7d0 + 22186ed commit 3984a5a
Show file tree
Hide file tree
Showing 42 changed files with 832 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import timedelta
from django.core.management import BaseCommand
from django.db.models import Count, F
from django.utils import timezone
from api.v1.v1_users.models import SystemUser
from api.v1.v1_sessions.models import (
PATSession,
Expand Down Expand Up @@ -125,8 +126,19 @@ def handle(self, *args, **options):
countries=countries,
sector=sector,
date=fake.date_this_month(before_today=False),
context=fake.paragraph()
context=fake.paragraph(),
)

current_year = timezone.now().year
years = [current_year, current_year - 1, current_year - 2]
created_at = timezone.make_aware(
timezone.datetime(
random.choice(years),
random.randint(1, 12),
random.randint(1, 28)
)
)
pat_session.created_at = created_at
pat_session.other_sector = other_sector
pat_session.save()

Expand Down
24 changes: 24 additions & 0 deletions backend/api/v1/v1_sessions/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,3 +638,27 @@ def get_organization_name(self, instance: Participant):
class Meta:
model = Participant
fields = ["id", "full_name", "email", "role", "organization_name"]


class TotalSessionPerMonthSerializer(serializers.Serializer):
total_sessions = serializers.IntegerField()
month = serializers.DateField()

class Meta:
fields = ["total_sessions", "month"]


class TotalSessionCompletedSerializer(serializers.Serializer):
total_completed = serializers.IntegerField()
total_completed_last_30_days = serializers.IntegerField()

class Meta:
fields = ["total_completed", "total_completed_last_30_days"]


class TotalSessionPerLast3YearsSerializer(serializers.Serializer):
total_sessions = serializers.ListField()
year = serializers.IntegerField()

class Meta:
fields = ["total_sessions", "year"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from datetime import timedelta
from django.test import TestCase
from django.core.management import call_command
from django.db.models import Count, DateField
from django.db.models.functions import TruncMonth
from django.test.utils import override_settings
from django.utils import timezone
from api.v1.v1_sessions.models import PATSession
from api.v1.v1_users.models import SystemUser
from api.v1.v1_users.tests.mixins import ProfileTestHelperMixin


@override_settings(USE_TZ=False)
class SessionStatisticsEndpointTestCase(TestCase, ProfileTestHelperMixin):
def setUp(self):
call_command("fake_users_seeder", "--test", True, "--repeat", 15)
self.admin = SystemUser.objects.create_superuser(
full_name="Super Admin",
email="admin@akvo.org",
gender=1,
account_purpose=2,
country="EN",
password="Secret123!",
)
self.token = self.get_auth_token(
email="admin@akvo.org",
password="Secret123!",
)
call_command("fake_sessions_seeder", "--test", True, "--repeat", 15)

def test_get_total_session_completed(self):
req = self.client.get(
"/api/v1/admin/sessions/completed",
content_type="application/json",
HTTP_AUTHORIZATION=f"Bearer {self.token}"
)
self.assertEqual(req.status_code, 200)
res = req.json()
total_completed = PATSession.objects.filter(
closed_at__isnull=False
).count()
self.assertEqual(res["total_completed"], total_completed)
total_completed_last_30_days = PATSession.objects.filter(
closed_at__isnull=False,
closed_at__gte=timezone.now() - timedelta(days=30)
).count()
self.assertEqual(
res["total_completed_last_30_days"],
total_completed_last_30_days
)

def test_get_total_session_per_month(self):
req = self.client.get(
"/api/v1/admin/sessions/per-month",
content_type="application/json",
HTTP_AUTHORIZATION=f"Bearer {self.token}"
)
self.assertEqual(req.status_code, 200)
res = req.json()
total_sessions_per_month = PATSession.objects.annotate(
month=TruncMonth("created_at", output_field=DateField())
).values("month").annotate(total_sessions=Count("id"))
sessions_per_month = []
for session in total_sessions_per_month:
sessions_per_month.append({
"month": session["month"].strftime("%Y-%m-%d"),
"total_sessions": session["total_sessions"]
})
self.assertEqual(len(res), len(sessions_per_month))

def test_get_total_session_per_last_3_years(self):
req = self.client.get(
"/api/v1/admin/sessions/per-last-3-years",
content_type="application/json",
HTTP_AUTHORIZATION=f"Bearer {self.token}"
)
self.assertEqual(req.status_code, 200)
res = req.json()
self.assertEqual(len(res), 3)
self.assertEqual(len(res[0]["total_sessions"]), 12)
15 changes: 15 additions & 0 deletions backend/api/v1/v1_sessions/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
participant_join_session,
delete_decision,
participant_list,
total_session_completed,
total_session_per_month,
total_session_per_last_3_years,
)

urlpatterns = [
Expand Down Expand Up @@ -48,4 +51,16 @@
r"^(?P<version>(v1))/session/(?P<session_id>[0-9]+)/participants",
participant_list
),
re_path(
r"^(?P<version>(v1))/admin/sessions/completed",
total_session_completed
),
re_path(
r"^(?P<version>(v1))/admin/sessions/per-month",
total_session_per_month
),
re_path(
r"^(?P<version>(v1))/admin/sessions/per-last-3-years",
total_session_per_last_3_years
),
]
100 changes: 99 additions & 1 deletion backend/api/v1/v1_sessions/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import timedelta
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
extend_schema,
Expand All @@ -14,7 +15,9 @@
permission_classes,
)
from rest_framework.generics import get_object_or_404
from django.db.models import Q
from django.utils import timezone
from django.db.models import Q, Count, DateField
from django.db.models.functions import TruncMonth, ExtractYear
from django.conf import settings
from api.v1.v1_sessions.models import (
PATSession,
Expand All @@ -41,11 +44,16 @@
ParticipantDecisionSerializer,
ParticipantCommentSerializer,
ParticipantSerializer,
TotalSessionPerMonthSerializer,
TotalSessionCompletedSerializer,
TotalSessionPerLast3YearsSerializer,
)
from utils.custom_pagination import Pagination
from utils.custom_serializer_fields import validate_serializers_message
from utils.default_serializers import DefaultResponseSerializer
from utils.email_helper import send_email, EmailTypes
from api.v1.v1_users.permissions import IsSuperuser
from collections import defaultdict


class PATSessionAddListView(APIView):
Expand Down Expand Up @@ -544,3 +552,93 @@ def participant_list(request, session_id, version):
queryset = Participant.objects.filter(session_id=session_id)
serializer = ParticipantSerializer(queryset, many=True)
return Response(data=serializer.data, status=status.HTTP_200_OK)


@extend_schema(
responses={200: TotalSessionPerMonthSerializer(many=True)},
tags=["Statistics"],
summary="Get total sessions per month",
)
@api_view(["GET"])
@permission_classes([IsAuthenticated, IsSuperuser])
def total_session_per_month(request, version):
queryset = PATSession.objects.all()
total_sessions_per_month = queryset.annotate(
month=TruncMonth(
"created_at", output_field=DateField()
)
).values("month").annotate(
total_sessions=Count("id")
).order_by("month")

serializer = TotalSessionPerMonthSerializer(
instance=total_sessions_per_month,
many=True
)
return Response(
data=serializer.data,
status=status.HTTP_200_OK
)


@extend_schema(
responses={200: TotalSessionCompletedSerializer},
tags=["Statistics"],
summary="Get total sessions completed",
)
@api_view(["GET"])
@permission_classes([IsAuthenticated, IsSuperuser])
def total_session_completed(request, version):
total_completed = PATSession.objects.filter(
closed_at__isnull=False
).count()
total_completed_last_30_days = PATSession.objects.filter(
closed_at__isnull=False,
closed_at__gte=timezone.now() - timedelta(days=30)
).count()
return Response(
data={
"total_completed": total_completed,
"total_completed_last_30_days": total_completed_last_30_days
},
status=status.HTTP_200_OK
)


@extend_schema(
responses={200: TotalSessionPerLast3YearsSerializer(many=True)},
tags=["Statistics"],
summary="Get total sessions per last 3 years",
)
@api_view(["GET"])
@permission_classes([IsAuthenticated, IsSuperuser])
def total_session_per_last_3_years(request, version):
queryset = PATSession.objects.all()
current_year = timezone.now().year
three_years_ago = current_year - 2

dataset = queryset.filter(created_at__year__gte=three_years_ago) \
.annotate(
month=TruncMonth("created_at"),
year=ExtractYear("created_at")
).values("year", "month") \
.annotate(total_sessions=Count("id")) \
.order_by("year", "month")

years_data = defaultdict(lambda: {"year": 0, "total_sessions": [0] * 12})

for entry in dataset:
year = entry["year"]
month = entry["month"].month - 1 # Adjust to 0-based index
total = entry["total_sessions"]

years_data[year]["year"] = year
years_data[year]["total_sessions"][month] = total

result = sorted(years_data.values(), key=lambda x: x["year"])

serializer = TotalSessionPerLast3YearsSerializer(instance=result, many=True)
return Response(
data=serializer.data,
status=status.HTTP_200_OK
)
13 changes: 13 additions & 0 deletions backend/api/v1/v1_users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,16 @@ def validate_token(self, value):
if not SystemUser.objects.filter(reset_password_code=value).exists():
raise serializers.ValidationError("Invalid token")
return value


class UserStatisticsSerializer(serializers.Serializer):
total_users = serializers.IntegerField()
total_users_last_30_days = serializers.IntegerField()
total_users_per_account_purpose = serializers.DictField()

class Meta:
fields = [
"total_users",
"total_users_last_30_days",
"total_users_per_account_purpose",
]
62 changes: 62 additions & 0 deletions backend/api/v1/v1_users/tests/tests_users_statistics_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from django.test import TestCase
from django.core.management import call_command
from django.test.utils import override_settings
from django.utils import timezone
from django.db.models import Count
from datetime import timedelta
from api.v1.v1_users.models import SystemUser
from api.v1.v1_users.constants import AccountPurpose
from api.v1.v1_users.tests.mixins import ProfileTestHelperMixin


@override_settings(USE_TZ=False)
class UsersStatisticsTestCase(TestCase, ProfileTestHelperMixin):
def setUp(self):
call_command("fake_users_seeder", "--test", True, "--repeat", 15)
self.admin = SystemUser.objects.create_superuser(
full_name="Super Admin",
email="admin@akvo.org",
gender=1,
account_purpose=2,
country="EN",
password="Secret123!",
)
self.token = self.get_auth_token(
email="admin@akvo.org",
password="Secret123!",
)

def test_get_users_statistics(self):
req = self.client.get(
"/api/v1/admin/statistics/users",
content_type="application/json",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
)
self.assertEqual(req.status_code, 200)
res = req.json()
self.assertEqual(res["total_users"], 16)
total_users_last_30_days = SystemUser.objects.filter(
date_joined__gte=timezone.now() - timedelta(days=30)
).count()
self.assertEqual(
res["total_users_last_30_days"],
total_users_last_30_days
)

total_users_per_account_purpose = SystemUser.objects.values(
"account_purpose"
).annotate(
total=Count("account_purpose")
)
account_purpose_total = []
for total in total_users_per_account_purpose:
category_name = AccountPurpose.FieldStr[total["account_purpose"]]
account_purpose_total.append({
"account_purpose": category_name,
"total": total["total"]
})

self.assertEqual(
res["total_users_per_account_purpose"],
account_purpose_total
)
5 changes: 5 additions & 0 deletions backend/api/v1/v1_users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
reset_password,
ProfileView,
ManageUsersViewSet,
get_users_statistics,
)

urlpatterns = [
Expand All @@ -33,4 +34,8 @@
"delete": "destroy"
})
),
re_path(
r"^(?P<version>(v1))/admin/statistics/users",
get_users_statistics
),
]
Loading

0 comments on commit 3984a5a

Please sign in to comment.