Skip to content

Commit 01c1465

Browse files
authored
Merge pull request #126 from AvaCodeSolutions/feat/112/deliver-contents-job
#112 Content delivery job
2 parents 707fbc3 + a0bbe95 commit 01c1465

29 files changed

Lines changed: 833 additions & 76 deletions

django_email_learning/jobs/__init__.py

Whitespace-only changes.
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from django_email_learning.ports.delivery_queue_protocol import DeliveryQueueProtocol
2+
from django_email_learning.models import DeliverySchedule, DeliveryStatus
3+
from django_email_learning.services.command_models.send_lesson_command import (
4+
SendLessonCommand,
5+
LessonNotFoundError,
6+
)
7+
from django_email_learning.services.command_models.send_quiz_command import (
8+
SendQuizCommand,
9+
QuizNotFoundError,
10+
)
11+
from django.utils.module_loading import import_string
12+
from django.conf import settings
13+
import logging
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
class DeliverContentsJob:
19+
def __init__(self) -> None:
20+
self.delivery_queue: DeliveryQueueProtocol = self.get_delivery_queue()
21+
22+
def run(self) -> None:
23+
should_check_next = True
24+
while should_check_next:
25+
delivery_schedule = self.delivery_queue.next_task()
26+
if delivery_schedule is None:
27+
should_check_next = False
28+
else:
29+
self.process_delivery(delivery_schedule)
30+
31+
def get_delivery_queue(self) -> DeliveryQueueProtocol:
32+
try:
33+
return import_string(settings.DJANGO_EMAIL_LEARNING["DELIVERY_QUEUE"])
34+
except (AttributeError, KeyError):
35+
from django_email_learning.services.defaults.database_delivery_queue import (
36+
DatabaseDeliveryQueue,
37+
)
38+
39+
return DatabaseDeliveryQueue()
40+
41+
def process_delivery(self, delivery_schedule: DeliverySchedule) -> None:
42+
course_content = delivery_schedule.delivery.course_content
43+
if not course_content.is_published:
44+
logger.warning(
45+
f"CourseCcontent {course_content.id} is not published. Canceling the delivery."
46+
)
47+
delivery_schedule.status = DeliveryStatus.CANCELED
48+
delivery_schedule.save()
49+
return
50+
51+
if course_content.type == "lesson":
52+
is_delivered = self.send_lesson_content(delivery_schedule)
53+
if is_delivered:
54+
logger.info(
55+
f"Lesson content delivered for DeliverySchedule ID {delivery_schedule.id}. Scheduling next content."
56+
)
57+
next_delivery = delivery_schedule.delivery.schedule_next_delivery()
58+
if next_delivery:
59+
logger.info(
60+
f"Scheduled next delivery {next_delivery.id} for enrollment {delivery_schedule.delivery.enrollment.id}"
61+
)
62+
else:
63+
logger.info(
64+
f"No more content to schedule for enrollment {delivery_schedule.delivery.enrollment.id}"
65+
)
66+
# TODO: if the sent content was the last in the course, consider marking the enrollment as completed.
67+
delivery_schedule.delivery.enrollment.graduate()
68+
69+
elif course_content.type == "quiz":
70+
is_delivered = self.send_quiz_content(delivery_schedule)
71+
72+
# For quiz we don't schedule next content automatically, because the scheduling should be done after quiz completion.
73+
if is_delivered:
74+
logger.info(
75+
f"Quiz content delivered for DeliverySchedule ID {delivery_schedule.id}. Next content scheduling is deferred until quiz completion."
76+
)
77+
78+
def send_lesson_content(self, delivery_schedule: DeliverySchedule) -> bool:
79+
if not delivery_schedule.delivery.course_content.lesson:
80+
delivery_schedule.status = DeliveryStatus.CANCELED
81+
delivery_schedule.save()
82+
logger.error(
83+
f"DeliverySchedule ID {delivery_schedule.id} has no associated lesson. Canceling the delivery."
84+
)
85+
return False
86+
87+
try:
88+
command = SendLessonCommand(
89+
lesson_id=delivery_schedule.delivery.course_content.lesson.id,
90+
email=delivery_schedule.delivery.enrollment.learner.email,
91+
)
92+
command.execute()
93+
delivery_schedule.status = DeliveryStatus.DELIVERED
94+
delivery_schedule.save()
95+
return True
96+
97+
except LessonNotFoundError:
98+
logger.error(
99+
f"Lesson with ID {delivery_schedule.delivery.course_content.lesson.id} not found. Canceling the delivery."
100+
)
101+
delivery_schedule.status = DeliveryStatus.CANCELED
102+
delivery_schedule.save()
103+
except Exception as e:
104+
logger.exception(
105+
f"Failed to send lesson content for DeliverySchedule ID {delivery_schedule.id}: {str(e)}"
106+
)
107+
delivery_schedule.status = DeliveryStatus.SCHEDULED
108+
delivery_schedule.save()
109+
return False
110+
111+
def send_quiz_content(self, delivery_schedule: DeliverySchedule) -> bool:
112+
if not delivery_schedule.delivery.course_content.quiz:
113+
delivery_schedule.status = DeliveryStatus.CANCELED
114+
delivery_schedule.save()
115+
logger.error(
116+
f"DeliverySchedule ID {delivery_schedule.id} has no associated quiz. Canceling the delivery."
117+
)
118+
return False
119+
120+
try:
121+
if not delivery_schedule.link:
122+
link = delivery_schedule.generate_link()
123+
delivery_schedule.link = link
124+
delivery_schedule.save()
125+
126+
command = SendQuizCommand(
127+
quiz_id=delivery_schedule.delivery.course_content.quiz.id,
128+
email=delivery_schedule.delivery.enrollment.learner.email,
129+
link=delivery_schedule.link,
130+
)
131+
command.execute()
132+
delivery_schedule.status = DeliveryStatus.DELIVERED
133+
delivery_schedule.save()
134+
135+
return True
136+
except QuizNotFoundError:
137+
logger.error(
138+
f"Quiz with ID {delivery_schedule.delivery.course_content.quiz.id} not found. Canceling the delivery."
139+
)
140+
delivery_schedule.status = DeliveryStatus.CANCELED
141+
delivery_schedule.save()
142+
except Exception as e:
143+
logger.exception(
144+
f"Failed to send quiz content for DeliverySchedule ID {delivery_schedule.id}: {str(e)}"
145+
)
146+
delivery_schedule.status = DeliveryStatus.SCHEDULED
147+
delivery_schedule.save()
148+
return False

django_email_learning/management/__init__.py

Whitespace-only changes.

django_email_learning/management/commands/__init__py

Whitespace-only changes.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from django.core.management.base import BaseCommand
2+
from django_email_learning.jobs.deliver_contents_job import DeliverContentsJob
3+
from django.core.management.base import CommandParser
4+
import logging
5+
6+
7+
class Command(BaseCommand):
8+
help = "Run the content delivery job to process scheduled content deliveries"
9+
10+
def add_arguments(self, parser: CommandParser) -> None:
11+
parser.add_argument(
12+
"--verbose",
13+
action="store_true",
14+
help="Enable verbose logging output",
15+
)
16+
17+
def handle(self, *args, **options) -> None: # type: ignore[no-untyped-def]
18+
# Configure logging based on verbosity
19+
if options["verbose"]:
20+
logging.basicConfig(
21+
level=logging.DEBUG,
22+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
23+
)
24+
else:
25+
logging.basicConfig(
26+
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
27+
)
28+
29+
logger = logging.getLogger(__name__)
30+
31+
try:
32+
self.stdout.write("Starting content delivery job...")
33+
logger.info("Starting DeliverContentsJob")
34+
35+
job = DeliverContentsJob()
36+
job.run()
37+
38+
self.stdout.write(
39+
self.style.SUCCESS("Content delivery job completed successfully")
40+
)
41+
logger.info("DeliverContentsJob completed successfully")
42+
43+
except KeyboardInterrupt:
44+
self.stdout.write(
45+
self.style.WARNING("Content delivery job interrupted by user")
46+
)
47+
logger.warning("DeliverContentsJob interrupted by user")
48+
49+
except Exception as e:
50+
self.stdout.write(
51+
self.style.ERROR(f"Content delivery job failed: {str(e)}")
52+
)
53+
logger.error(f"DeliverContentsJob failed: {str(e)}", exc_info=True)
54+
raise

django_email_learning/migrations/0001_initial.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 6.0.1 on 2026-01-08 17:24
1+
# Generated by Django 6.0.1 on 2026-01-09 11:59
22

33
import django.core.validators
44
import django.db.models.deletion
@@ -286,7 +286,37 @@ class Migration(migrations.Migration):
286286
db_index=True, default=django.utils.timezone.now
287287
),
288288
),
289-
("is_delivered", models.BooleanField(db_index=True, default=False)),
289+
("link", models.URLField(blank=True, null=True)),
290+
(
291+
"status",
292+
models.CharField(
293+
choices=[
294+
(
295+
django_email_learning.models.DeliveryStatus[
296+
"SCHEDULED"
297+
],
298+
"Scheduled",
299+
),
300+
(
301+
django_email_learning.models.DeliveryStatus[
302+
"PROCESSING"
303+
],
304+
"Processing",
305+
),
306+
(
307+
django_email_learning.models.DeliveryStatus[
308+
"DELIVERED"
309+
],
310+
"Delivered",
311+
),
312+
],
313+
db_index=True,
314+
default=django_email_learning.models.DeliveryStatus[
315+
"SCHEDULED"
316+
],
317+
max_length=50,
318+
),
319+
),
290320
(
291321
"delivery",
292322
models.ForeignKey(

django_email_learning/migrations/0002_deliveryschedule_link.py

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)