Skip to content

Commit bd1c8f8

Browse files
authored
Merge pull request #177 from AvaCodeSolutions/feat/167/course-stats
Feat/167/course stats
2 parents db59e37 + 3d00ba6 commit bd1c8f8

18 files changed

Lines changed: 558 additions & 45 deletions

File tree

django_email_learning/models.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,28 @@
2727
logger = logging.getLogger(__name__)
2828

2929

30+
class EnrollmentStatus(StrEnum):
31+
UNVERIFIED = "unverified"
32+
ACTIVE = "active"
33+
COMPLETED = "completed"
34+
DEACTIVATED = "deactivated"
35+
36+
37+
class DeactivationReason(StrEnum):
38+
CANCELED = "canceled"
39+
BLOCKED = "blocked"
40+
FAILED = "failed"
41+
INACTIVE = "inactive"
42+
43+
44+
class DeliveryStatus(StrEnum):
45+
SCHEDULED = "scheduled"
46+
PROCESSING = "processing"
47+
DELIVERED = "delivered"
48+
CANCELED = "canceled"
49+
BLOCKED = "blocked"
50+
51+
3052
def is_domain_or_ip(value: str) -> None:
3153
"""
3254
Validate if the given value is a valid domain name or IP address.
@@ -149,6 +171,29 @@ def delete(
149171
)
150172
return super().delete(using, keep_parents)
151173

174+
@property
175+
def enrollments_count(self) -> dict[str, int]:
176+
unverified_count = self.enrollment_set.filter(
177+
status=EnrollmentStatus.UNVERIFIED
178+
).count()
179+
active_count = self.enrollment_set.filter(
180+
status=EnrollmentStatus.ACTIVE
181+
).count()
182+
completed_count = self.enrollment_set.filter(
183+
status=EnrollmentStatus.COMPLETED
184+
).count()
185+
deactivated_count = self.enrollment_set.filter(
186+
status=EnrollmentStatus.DEACTIVATED
187+
).count()
188+
total_count = self.enrollment_set.count()
189+
return {
190+
EnrollmentStatus.UNVERIFIED: unverified_count,
191+
EnrollmentStatus.ACTIVE: active_count,
192+
EnrollmentStatus.COMPLETED: completed_count,
193+
EnrollmentStatus.DEACTIVATED: deactivated_count,
194+
"total": total_count,
195+
}
196+
152197

153198
class Lesson(models.Model):
154199
title = models.CharField(max_length=200)
@@ -332,28 +377,6 @@ def save(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
332377
super().save(*args, **kwargs)
333378

334379

335-
class EnrollmentStatus(StrEnum):
336-
UNVERIFIED = "unverified"
337-
ACTIVE = "active"
338-
COMPLETED = "completed"
339-
DEACTIVATED = "deactivated"
340-
341-
342-
class DeactivationReason(StrEnum):
343-
CANCELED = "canceled"
344-
BLOCKED = "blocked"
345-
FAILED = "failed"
346-
INACTIVE = "inactive"
347-
348-
349-
class DeliveryStatus(StrEnum):
350-
SCHEDULED = "scheduled"
351-
PROCESSING = "processing"
352-
DELIVERED = "delivered"
353-
CANCELED = "canceled"
354-
BLOCKED = "blocked"
355-
356-
357380
class Enrollment(models.Model):
358381
state_transitions = {
359382
EnrollmentStatus.UNVERIFIED: [

django_email_learning/platform/api/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ class CourseResponse(BaseModel):
112112
organization_id: int
113113
imap_connection_id: Optional[int]
114114
enabled: bool
115+
enrollments_count: dict[str, int]
115116

116117
model_config = ConfigDict(from_attributes=True)
117118

django_email_learning/platform/api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django_email_learning.platform.api.views import (
44
CourseView,
55
EnrollmentView,
6+
EnrollmentsStatisticsView,
67
FileView,
78
ImapConnectionView,
89
OrganizationsView,
@@ -64,6 +65,11 @@
6465
EnrollmentView.as_view(),
6566
name="enrollment_view",
6667
),
68+
path(
69+
"organizations/<int:organization_id>/courses/<int:course_id>/enrollments/statistics/",
70+
EnrollmentsStatisticsView.as_view(),
71+
name="enrollments_statistics_view",
72+
),
6773
path(
6874
"organizations/<int:organization_id>/file/",
6975
FileView.as_view(),

django_email_learning/platform/api/views.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
from django.utils.decorators import method_decorator
33
from django.views.decorators.csrf import ensure_csrf_cookie
44
from django.db.utils import IntegrityError
5+
from django.db.models.functions import TruncDate
6+
from django.db.models import Count
57
from django.http import JsonResponse
68
from django.core.exceptions import ValidationError as DjangoValidationError
79
from django.db import models, transaction
810
from django.core.files.storage import default_storage
911
from django.utils import timezone
12+
from datetime import timedelta
1013
from pydantic import ValidationError
1114

1215
from django_email_learning.platform.api import serializers
@@ -563,6 +566,32 @@ def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-unty
563566
return JsonResponse({"error": e.json()}, status=400)
564567

565568

569+
@method_decorator(accessible_for(roles={"admin", "editor", "viewer"}), name="get")
570+
class EnrollmentsStatisticsView(View):
571+
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
572+
course_id = kwargs["course_id"]
573+
a_week_ago = timezone.now() - timedelta(days=7)
574+
enrollments = (
575+
Enrollment.objects.filter(course_id=course_id, enrolled_at__gte=a_week_ago)
576+
.annotate(created_date=TruncDate("enrolled_at"))
577+
.values(
578+
"created_date",
579+
)
580+
.annotate(count=Count("id"))
581+
.order_by("created_date")
582+
)
583+
dates = [a_week_ago.date() + timedelta(days=i) for i in range(8)]
584+
enrollments_dict = {
585+
enrollment["created_date"]: enrollment["count"]
586+
for enrollment in enrollments
587+
}
588+
stats = [
589+
{"date": date.isoformat(), "count": enrollments_dict.get(date, 0)}
590+
for date in dates
591+
]
592+
return JsonResponse({"statistics": stats}, status=200)
593+
594+
566595
class RootView(View):
567596
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
568597
return JsonResponse({"message": "Email Learning API is running."}, status=200)

django_email_learning/templates/platform/course.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
6060
"lesson_title_required": "{% translate 'Lesson title is required.' %}",
6161
"lesson_content_required": "{% translate 'Lesson content is required.' %}",
6262
"delete_content_confirmation": "{% translate 'Are you sure you want to delete the content: CONTENT_TITLE?' %}",
63+
"unverified": "{% translate 'Unverified' %}",
64+
"active": "{% translate 'Active' %}",
65+
"deactivated": "{% translate 'Deactivated' %}",
66+
"completed": "{% translate 'Completed' %}",
67+
"enrollments_distribution": "{% translate 'Enrollments Distribution' %}",
68+
"weekly_enrollments": "{% translate 'Weekly Enrollments' %}",
6369
}
6470
</script>
6571
{% vite_asset 'platform/course/Course.jsx' %}

django_email_learning/templates/platform/courses.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"port_required_helper_text": "{% translate 'The port is required.' %}",
4747
"invalid_port_helper_text": "{% translate 'The port must be a valid number.' %}",
4848
"invalid_email_helper_text": "{% translate 'The email must be a valid email address.' %}",
49+
"total_enrollments": "{% translate 'Total Enrollments' %}",
4950
}
5051
</script>
5152
{% vite_asset 'platform/courses/Courses.jsx' %}

0 commit comments

Comments
 (0)