Skip to content

Commit 59864de

Browse files
authored
Merge pull request #302 from AvaCodeSolutions/feat/299/show-progress-in-lesson-emails
feat: #299 add progress bar at the end of the lessons email
2 parents d904d14 + ad9595e commit 59864de

5 files changed

Lines changed: 100 additions & 2 deletions

File tree

django_email_learning/models.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from django.contrib.auth.models import User
2929
from django.utils import timezone
3030
from django.utils.translation import gettext as _
31+
from django.utils.translation import ngettext
3132
from datetime import timedelta
3233
from django_email_learning.services import jwt_service
3334

@@ -399,6 +400,25 @@ def title(self) -> str:
399400
return self.quiz.title
400401
return "Untitled Content"
401402

403+
def human_readable_waiting_period(self) -> str:
404+
if self.waiting_period < 60:
405+
return ngettext(
406+
"%(count)d second", "%(count)d seconds", self.waiting_period
407+
) % {"count": self.waiting_period}
408+
elif self.waiting_period < 3600:
409+
minutes = self.waiting_period // 60
410+
return ngettext("%(count)d minute", "%(count)d minutes", minutes) % {
411+
"count": minutes
412+
}
413+
elif self.waiting_period < 86400:
414+
hours = self.waiting_period // 3600
415+
return ngettext("%(count)d hour", "%(count)d hours", hours) % {
416+
"count": hours
417+
}
418+
else:
419+
days = self.waiting_period // 86400
420+
return ngettext("%(count)d day", "%(count)d days", days) % {"count": days}
421+
402422
def _validate_content(self) -> None:
403423
if self.type == "lesson" and not self.lesson:
404424
raise ValidationError("Lesson must be provided for lesson content.")
@@ -417,6 +437,16 @@ def save(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
417437
self.full_clean()
418438
super().save(*args, **kwargs)
419439

440+
def get_next(self) -> Optional["CourseContent"]:
441+
next_content = (
442+
CourseContent.objects.filter(
443+
course=self.course, is_published=True, priority__gt=self.priority
444+
)
445+
.order_by("priority")
446+
.first()
447+
)
448+
return next_content
449+
420450
class Meta:
421451
constraints = [
422452
models.UniqueConstraint(

django_email_learning/services/command_models/send_lesson_command.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
from django_email_learning.services.command_models.abstract_command import (
22
AbstractCommand,
33
)
4-
from django_email_learning.models import Lesson, CourseContent
4+
from django_email_learning.models import (
5+
Lesson,
6+
CourseContent,
7+
Enrollment,
8+
EnrollmentStatus,
9+
)
510
from django_email_learning.services.email_sender_service import EmailSenderService
611
from django_email_learning.services.metrics_service import MetricsService
712
from django.core.mail import EmailMultiAlternatives
@@ -35,9 +40,21 @@ def execute(self) -> None:
3540
raise LessonNotFoundError(f"Lesson with ID {content.lesson.id} not found")
3641

3742
subject = lesson.title
43+
enrollment = Enrollment.objects.filter(
44+
course=content.course,
45+
learner__email=self.email,
46+
status=EnrollmentStatus.ACTIVE,
47+
).first()
48+
if not enrollment:
49+
progress = 0
50+
else:
51+
progress = enrollment.progress_percentage
52+
next_content = content.get_next()
3853
context = {
3954
"lesson": lesson,
4055
"unsubscribe_link": content.course.generate_unsubscribe_link(self.email),
56+
"progress": progress,
57+
"next_content": next_content,
4158
}
4259
payload = render_to_string("emails/lesson.txt", context)
4360

django_email_learning/templates/emails/base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@
176176
border: 0;
177177
margin: 0 auto;
178178
padding: 0 10px !important;
179+
width: 100% !important;
179180
}
180181
{% block extra_styles %}{% endblock %}
181182
</style>

django_email_learning/templates/emails/lesson.html

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,58 @@
99
overflow-x: auto;
1010
margin: 15px 0 30px;
1111
}
12+
.progress-wrapper {
13+
margin-top: 40px;
14+
padding: 20px 0;
15+
border-top: 1px solid #ddd;
16+
}
17+
.progress-label {
18+
display: block;
19+
margin-bottom: 10px;
20+
font-size: 14px;
21+
color: #555;
22+
}
23+
.progress-container {
24+
width: 100%;
25+
background-color: #e0e0e0;
26+
border-radius: 5px;
27+
overflow: hidden;
28+
}
29+
.progress-bar {
30+
height: 20px;
31+
background-color: #4caf50;
32+
width: 0;
33+
transition: width 0.5s ease-in-out;
34+
background-image: linear-gradient(to right, #7070e6, #4dd2cb);
35+
}
1236
{% endblock %}
1337

1438
{% block content %}
1539
<h2 class="email-title">{{ lesson.title }}</h2>
1640

1741
{{ lesson.content|safe }}
1842

43+
<div class="progress-wrapper">
44+
{% block lesson_extras %}
45+
{% if next_content %}
46+
<h3>{% blocktranslate %}What’s coming up next?{% endblocktranslate %}</h3>
47+
{% if next_content.lesson %}
48+
<p>{% blocktranslate with next_lesson_title=next_content.lesson.title delay=next_content.human_readable_waiting_period title=lesson.title %}
49+
Now that you've mastered {{ title }}, we’ll be moving on to {{ next_lesson_title }}. Keep an eye out for that email—it will hit your inbox in {{ delay }}.
50+
{% endblocktranslate %}</p>
51+
{% elif next_content.quiz %}
52+
<p>{% blocktranslate with quiz_title=next_content.quiz.title delay=next_content.human_readable_waiting_period title=lesson.title %}
53+
Now that you've mastered {{ title }}, it’s time to put that knowledge to the test! Your next email will be the {{ quiz_title }}. Look out for it hitting your inbox in {{ delay }}.
54+
{% endblocktranslate %}</p>
55+
{% endif %}
56+
{% endif %}
57+
{% endblock %}
58+
<span class="progress-label">{% blocktranslate %}<b>Course Progress: {{ progress }}%</b>{% endblocktranslate %}</span>
59+
<div class="progress-container">
60+
<div class="progress-bar" style="width: {{ progress }}%;"></div>
61+
</div>
62+
</div>
63+
1964
<p class="email-muted-note">
2065
{% blocktranslate %}If you wish to unsubscribe from this course, please <a href="{{ unsubscribe_link }}" class="color-brand" style="text-decoration: none;">click here</a>{% endblocktranslate %}.
2166
</p>

django_service/views.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.views.generic import TemplateView
2-
from django_email_learning.models import Lesson, Quiz
2+
from django_email_learning.models import Lesson, Quiz, CourseContent
33
from django_email_learning.platform.views import CourseView
44
from django_email_learning.platform.serializers import WebComponent
55

@@ -39,6 +39,9 @@ def get_template_names(self) -> list[str]:
3939
def get_context_data(self, **kwargs): # type: ignore[no-untyped-def]
4040
lesson = Lesson.objects.first()
4141
quiz = Quiz.objects.first()
42+
content = (
43+
CourseContent.objects.filter(lesson=lesson).first() if lesson else None
44+
)
4245
return {
4346
"course_title": "Example Course",
4447
"organization_name": "Example Organization",
@@ -52,4 +55,6 @@ def get_context_data(self, **kwargs): # type: ignore[no-untyped-def]
5255
"domain": "example.com",
5356
"uid": "sampleuid",
5457
"token": "sampletoken",
58+
"progress": 40,
59+
"next_content": content.get_next() if content else None,
5560
}

0 commit comments

Comments
 (0)