Skip to content

Commit 412629e

Browse files
committed
feat: #55 metrics protocol
1 parent aa789e8 commit 412629e

15 files changed

Lines changed: 450 additions & 2 deletions

django_email_learning/jobs/deliver_contents_job.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,14 @@ def get_delivery_queue(self) -> DeliveryQueueProtocol:
6262
settings, "DJANGO_EMAIL_LEARNING", {}
6363
)
6464
try:
65-
return import_string(DJANGO_EMAIL_LEARNING_SETTINGS["DELIVERY_QUEUE"])
65+
configured_delivery_queue = import_string(
66+
DJANGO_EMAIL_LEARNING_SETTINGS["DELIVERY_QUEUE"]
67+
)
68+
return (
69+
configured_delivery_queue()
70+
if isinstance(configured_delivery_queue, type)
71+
else configured_delivery_queue
72+
)
6673
except KeyError:
6774
from django_email_learning.services.defaults.database_delivery_queue import (
6875
DatabaseDeliveryQueue,

django_email_learning/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
MinLengthValidator,
1818
)
1919
from django_email_learning.services.email_sender_service import EmailSenderService
20+
from django_email_learning.services.metrics_service import MetricsService
2021
from django.core.mail import EmailMultiAlternatives
2122
from django.template.loader import render_to_string
2223
from django.core.exceptions import ImproperlyConfigured
@@ -60,6 +61,9 @@ class DeliveryStatus(StrEnum):
6061
BLOCKED = "blocked"
6162

6263

64+
METRIC_SERVICE = MetricsService()
65+
66+
6367
def is_domain_or_ip(value: str) -> None:
6468
"""
6569
Validate if the given value is a valid domain name or IP address.
@@ -557,6 +561,10 @@ def graduate(self) -> None:
557561
)
558562
self.status = EnrollmentStatus.COMPLETED
559563
self.final_state_at = timezone.now()
564+
METRIC_SERVICE.user_completed_course(
565+
course_slug=self.course.slug,
566+
organization_id=self.course.organization.id,
567+
)
560568
logger.info(
561569
f"Learner ID {self.learner.id} has completed the course {self.course.title}."
562570
)
@@ -609,6 +617,11 @@ def fail(self) -> None:
609617
self.status = EnrollmentStatus.DEACTIVATED
610618
self.deactivation_reason = DeactivationReason.FAILED
611619
self.final_state_at = timezone.now()
620+
METRIC_SERVICE.user_enrollment_deactivated(
621+
course_slug=self.course.slug,
622+
organization_id=self.course.organization.id,
623+
reason=DeactivationReason.FAILED,
624+
)
612625
logger.info(
613626
f"Learner ID {self.learner.id} has failed the course {self.course.title}."
614627
)

django_email_learning/personalised/api/views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
QuizSubmissionRequest,
66
QuestionResponse,
77
)
8+
from django_email_learning.services.metrics_service import MetricsService
89
from django_email_learning.services import jwt_service
910
from django.utils.translation import gettext as _
1011
from django_email_learning.models import (
@@ -19,6 +20,8 @@
1920
import json
2021
import logging
2122

23+
METRIC_SERVICE = MetricsService()
24+
2225
logger = logging.getLogger(__name__)
2326

2427

@@ -113,6 +116,12 @@ def post(self, request, *args, **kwargs): # type: ignore[no-untyped-def]
113116
)
114117
delivery.repeat_delivery_in_days(1)
115118

119+
METRIC_SERVICE.quiz_submitted(
120+
course_slug=enrolment.course.slug,
121+
organization_id=enrolment.course.organization.id,
122+
quiz_id=quiz.id,
123+
is_passed=passed,
124+
)
116125
return JsonResponse(
117126
{
118127
"score": score,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import Protocol
2+
from django_email_learning.models import DeactivationReason
3+
4+
5+
class MetricRecorderProtocol(Protocol):
6+
def user_enrolled_in_course(self, course_slug: str, organization_id: int) -> None:
7+
...
8+
9+
def quiz_sent(self, course_slug: str, organization_id: int, quiz_id: int) -> None:
10+
...
11+
12+
def lesson_sent(
13+
self, course_slug: str, organization_id: int, lesson_id: int
14+
) -> None:
15+
...
16+
17+
def user_enrollment_activated(self, course_slug: str, organization_id: int) -> None:
18+
...
19+
20+
def user_enrollment_deactivated(
21+
self, course_slug: str, organization_id: int, reason: DeactivationReason
22+
) -> None:
23+
...
24+
25+
def user_completed_course(self, course_slug: str, organization_id: int) -> None:
26+
...
27+
28+
def quiz_submitted(
29+
self, course_slug: str, organization_id: int, quiz_id: int, is_passed: bool
30+
) -> None:
31+
...
32+
33+
def method_executed(self, method_name: str, execution_time: int) -> None:
34+
...

django_email_learning/services/command_models/enroll_command.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from django_email_learning.services.utils import mask_email
2121
from django_email_learning.services.email_sender_service import EmailSenderService
2222
from django_email_learning.services import jwt_service
23+
from django_email_learning.services.metrics_service import MetricsService
2324
from django.core.mail import EmailMultiAlternatives
2425
from django.template.loader import render_to_string
2526
from django.utils.translation import gettext as _
@@ -36,6 +37,7 @@ class EnrollCommand(AbstractCommand):
3637
no_verification: bool = False
3738

3839
def execute(self) -> None:
40+
metric_service = MetricsService()
3941
# Check if the email is blocked
4042
if BlockedEmail.objects.filter(email=self.email).exists():
4143
self.logger.info(
@@ -88,6 +90,8 @@ def execute(self) -> None:
8890
f"Enrollment Successful: Learner ID {learner.id} enrolled in course '{self.course_slug}'. Enrollment ID: {enrollment.id}"
8991
)
9092

93+
metric_service.user_enrolled_in_course(self.course_slug, self.organization_id)
94+
9195
if self.no_verification:
9296
self.logger.info(
9397
f"Verification email skipped for Enrollment ID: {enrollment.id} as per command parameter"

django_email_learning/services/command_models/send_lesson_command.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
)
44
from django_email_learning.models import Lesson, CourseContent
55
from django_email_learning.services.email_sender_service import EmailSenderService
6+
from django_email_learning.services.metrics_service import MetricsService
67
from django.core.mail import EmailMultiAlternatives
78
from django.template.loader import render_to_string
89
from typing import Literal
@@ -18,6 +19,7 @@ class SendLessonCommand(AbstractCommand):
1819
email: str
1920

2021
def execute(self) -> None:
22+
metric_service = MetricsService()
2123
content = CourseContent.objects.get(id=self.content_id)
2224
if not content.lesson:
2325
raise LessonNotFoundError(
@@ -52,3 +54,8 @@ def execute(self) -> None:
5254
)
5355

5456
email_service.send(email_message)
57+
metric_service.lesson_sent(
58+
course_slug=content.course.slug,
59+
organization_id=content.course.organization.id,
60+
lesson_id=lesson.id,
61+
)

django_email_learning/services/command_models/send_quiz_command.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
)
44
from django_email_learning.models import Quiz, CourseContent
55
from django_email_learning.services.email_sender_service import EmailSenderService
6+
from django_email_learning.services.metrics_service import MetricsService
67
from django.core.mail import EmailMultiAlternatives
78
from django.template.loader import render_to_string
89
from typing import Literal
@@ -19,6 +20,7 @@ class SendQuizCommand(AbstractCommand):
1920
content_id: int
2021

2122
def execute(self) -> None:
23+
metric_service = MetricsService()
2224
content = CourseContent.objects.get(id=self.content_id)
2325
if not content.quiz:
2426
raise QuizNotFoundError(
@@ -53,3 +55,8 @@ def execute(self) -> None:
5355
)
5456

5557
email_service.send(email_message)
58+
metric_service.quiz_sent(
59+
course_slug=content.course.slug,
60+
organization_id=content.course.organization.id,
61+
quiz_id=quiz.id,
62+
)

django_email_learning/services/command_models/unsubscribe_command.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
DeactivationReason,
99
)
1010
from django.utils import timezone
11+
from django_email_learning.services.metrics_service import MetricsService
1112
from django_email_learning.services.command_models.abstract_command import (
1213
AbstractCommand,
1314
)
@@ -23,6 +24,7 @@ class UnsubscribeCommand(AbstractCommand):
2324
organization_id: int
2425

2526
def execute(self) -> None:
27+
metric_service = MetricsService()
2628
try:
2729
course = Course.objects.get(
2830
slug=self.course_slug, organization_id=self.organization_id
@@ -66,3 +68,8 @@ def execute(self) -> None:
6668
deactivation_reason=DeactivationReason.CANCELED,
6769
final_state_at=timezone.now(),
6870
)
71+
metric_service.user_enrollment_deactivated(
72+
course_slug=course.slug,
73+
organization_id=course.organization.id,
74+
reason=DeactivationReason.CANCELED,
75+
)

django_email_learning/services/command_models/verify_enrollment_command.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
InvalidVerificationCodeError,
1212
)
1313
from django_email_learning.services.email_sender_service import EmailSenderService
14+
from django_email_learning.services.metrics_service import MetricsService
1415
from django.core.mail import EmailMultiAlternatives
1516
from django.template.loader import render_to_string
1617
from django.utils.translation import gettext as _
@@ -24,6 +25,7 @@ class VerifyEnrollmentCommand(AbstractCommand):
2425
verification_code: str = Field(..., pattern=r"^\d{6}$")
2526

2627
def execute(self) -> None:
28+
metric_service = MetricsService()
2729
try:
2830
enrollment = Enrollment.objects.get(
2931
id=self.enrollment_id, status=EnrollmentStatus.UNVERIFIED
@@ -58,6 +60,9 @@ def execute(self) -> None:
5860
self.logger.info(
5961
f"Content Delivery Scheduled: First content delivery scheduled for Enrollment ID {self.enrollment_id}"
6062
)
63+
metric_service.user_enrollment_activated(
64+
enrollment.course.slug, enrollment.course.organization.id
65+
)
6166

6267
# Send confirmation email
6368
email_service = EmailSenderService()
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from django_email_learning.ports.metric_recorder_protocol import MetricRecorderProtocol
2+
from django_email_learning.models import DeactivationReason
3+
import logging
4+
5+
logger = logging.getLogger(__name__)
6+
7+
8+
class LogBasedMetricRecorder(MetricRecorderProtocol):
9+
def user_enrolled_in_course(self, course_slug: str, organization_id: int) -> None:
10+
logger.info(
11+
"User enrolled",
12+
extra={"course_slug": course_slug, "organization_id": organization_id},
13+
)
14+
15+
def quiz_sent(self, course_slug: str, organization_id: int, quiz_id: int) -> None:
16+
logger.info(
17+
"Quiz sent",
18+
extra={
19+
"course_slug": course_slug,
20+
"organization_id": organization_id,
21+
"quiz_id": quiz_id,
22+
},
23+
)
24+
25+
def lesson_sent(
26+
self, course_slug: str, organization_id: int, lesson_id: int
27+
) -> None:
28+
logger.info(
29+
"Lesson sent",
30+
extra={
31+
"course_slug": course_slug,
32+
"organization_id": organization_id,
33+
"lesson_id": lesson_id,
34+
},
35+
)
36+
37+
def user_enrollment_activated(self, course_slug: str, organization_id: int) -> None:
38+
logger.info(
39+
"User enrollment activated",
40+
extra={"course_slug": course_slug, "organization_id": organization_id},
41+
)
42+
43+
def user_enrollment_deactivated(
44+
self, course_slug: str, organization_id: int, reason: DeactivationReason
45+
) -> None:
46+
logger.info(
47+
"User enrollment deactivated",
48+
extra={
49+
"course_slug": course_slug,
50+
"organization_id": organization_id,
51+
"reason": reason,
52+
},
53+
)
54+
55+
def user_completed_course(self, course_slug: str, organization_id: int) -> None:
56+
logger.info(
57+
"User completed course",
58+
extra={"course_slug": course_slug, "organization_id": organization_id},
59+
)
60+
61+
def quiz_submitted(
62+
self, course_slug: str, organization_id: int, quiz_id: int, is_passed: bool
63+
) -> None:
64+
logger.info(
65+
"Quiz submitted",
66+
extra={
67+
"course_slug": course_slug,
68+
"organization_id": organization_id,
69+
"quiz_id": quiz_id,
70+
"is_passed": is_passed,
71+
},
72+
)
73+
74+
def method_executed(self, method_name: str, execution_time: int) -> None:
75+
logger.info(
76+
"Method executed",
77+
extra={"method_name": method_name, "execution_time": execution_time},
78+
)

0 commit comments

Comments
 (0)