Skip to content
Merged
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
67 changes: 45 additions & 22 deletions django_email_learning/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@
logger = logging.getLogger(__name__)


class EnrollmentStatus(StrEnum):
UNVERIFIED = "unverified"
ACTIVE = "active"
COMPLETED = "completed"
DEACTIVATED = "deactivated"


class DeactivationReason(StrEnum):
CANCELED = "canceled"
BLOCKED = "blocked"
FAILED = "failed"
INACTIVE = "inactive"


class DeliveryStatus(StrEnum):
SCHEDULED = "scheduled"
PROCESSING = "processing"
DELIVERED = "delivered"
CANCELED = "canceled"
BLOCKED = "blocked"


def is_domain_or_ip(value: str) -> None:
"""
Validate if the given value is a valid domain name or IP address.
Expand Down Expand Up @@ -149,6 +171,29 @@ def delete(
)
return super().delete(using, keep_parents)

@property
def enrollments_count(self) -> dict[str, int]:
unverified_count = self.enrollment_set.filter(
status=EnrollmentStatus.UNVERIFIED
).count()
active_count = self.enrollment_set.filter(
status=EnrollmentStatus.ACTIVE
).count()
completed_count = self.enrollment_set.filter(
status=EnrollmentStatus.COMPLETED
).count()
deactivated_count = self.enrollment_set.filter(
status=EnrollmentStatus.DEACTIVATED
).count()
total_count = self.enrollment_set.count()
return {
EnrollmentStatus.UNVERIFIED: unverified_count,
EnrollmentStatus.ACTIVE: active_count,
EnrollmentStatus.COMPLETED: completed_count,
EnrollmentStatus.DEACTIVATED: deactivated_count,
"total": total_count,
}


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


class EnrollmentStatus(StrEnum):
UNVERIFIED = "unverified"
ACTIVE = "active"
COMPLETED = "completed"
DEACTIVATED = "deactivated"


class DeactivationReason(StrEnum):
CANCELED = "canceled"
BLOCKED = "blocked"
FAILED = "failed"
INACTIVE = "inactive"


class DeliveryStatus(StrEnum):
SCHEDULED = "scheduled"
PROCESSING = "processing"
DELIVERED = "delivered"
CANCELED = "canceled"
BLOCKED = "blocked"


class Enrollment(models.Model):
state_transitions = {
EnrollmentStatus.UNVERIFIED: [
Expand Down
1 change: 1 addition & 0 deletions django_email_learning/platform/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ class CourseResponse(BaseModel):
organization_id: int
imap_connection_id: Optional[int]
enabled: bool
enrollments_count: dict[str, int]

model_config = ConfigDict(from_attributes=True)

Expand Down
6 changes: 6 additions & 0 deletions django_email_learning/platform/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django_email_learning.platform.api.views import (
CourseView,
EnrollmentView,
EnrollmentsStatisticsView,
FileView,
ImapConnectionView,
OrganizationsView,
Expand Down Expand Up @@ -64,6 +65,11 @@
EnrollmentView.as_view(),
name="enrollment_view",
),
path(
"organizations/<int:organization_id>/courses/<int:course_id>/enrollments/statistics/",
EnrollmentsStatisticsView.as_view(),
name="enrollments_statistics_view",
),
path(
"organizations/<int:organization_id>/file/",
FileView.as_view(),
Expand Down
29 changes: 29 additions & 0 deletions django_email_learning/platform/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from django.db.utils import IntegrityError
from django.db.models.functions import TruncDate
from django.db.models import Count
from django.http import JsonResponse
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import models, transaction
from django.core.files.storage import default_storage
from django.utils import timezone
from datetime import timedelta
from pydantic import ValidationError

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


@method_decorator(accessible_for(roles={"admin", "editor", "viewer"}), name="get")
class EnrollmentsStatisticsView(View):
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
course_id = kwargs["course_id"]
a_week_ago = timezone.now() - timedelta(days=7)
enrollments = (
Enrollment.objects.filter(course_id=course_id, enrolled_at__gte=a_week_ago)
.annotate(created_date=TruncDate("enrolled_at"))
.values(
"created_date",
)
.annotate(count=Count("id"))
.order_by("created_date")
)
dates = [a_week_ago.date() + timedelta(days=i) for i in range(8)]
enrollments_dict = {
enrollment["created_date"]: enrollment["count"]
for enrollment in enrollments
}
stats = [
{"date": date.isoformat(), "count": enrollments_dict.get(date, 0)}
for date in dates
]
return JsonResponse({"statistics": stats}, status=200)


class RootView(View):
def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
return JsonResponse({"message": "Email Learning API is running."}, status=200)
6 changes: 6 additions & 0 deletions django_email_learning/templates/platform/course.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@
"lesson_title_required": "{% translate 'Lesson title is required.' %}",
"lesson_content_required": "{% translate 'Lesson content is required.' %}",
"delete_content_confirmation": "{% translate 'Are you sure you want to delete the content: CONTENT_TITLE?' %}",
"unverified": "{% translate 'Unverified' %}",
"active": "{% translate 'Active' %}",
"deactivated": "{% translate 'Deactivated' %}",
"completed": "{% translate 'Completed' %}",
"enrollments_distribution": "{% translate 'Enrollments Distribution' %}",
"weekly_enrollments": "{% translate 'Weekly Enrollments' %}",
}
</script>
{% vite_asset 'platform/course/Course.jsx' %}
Expand Down
1 change: 1 addition & 0 deletions django_email_learning/templates/platform/courses.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"port_required_helper_text": "{% translate 'The port is required.' %}",
"invalid_port_helper_text": "{% translate 'The port must be a valid number.' %}",
"invalid_email_helper_text": "{% translate 'The email must be a valid email address.' %}",
"total_enrollments": "{% translate 'Total Enrollments' %}",
}
</script>
{% vite_asset 'platform/courses/Courses.jsx' %}
Expand Down
Loading