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
4 changes: 2 additions & 2 deletions django_email_learning/jobs/deliver_contents_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def send_lesson_content(self, delivery_schedule: DeliverySchedule) -> bool:

try:
command = SendLessonCommand(
lesson_id=delivery_schedule.delivery.course_content.lesson.id,
content_id=delivery_schedule.delivery.course_content.id,
email=delivery_schedule.delivery.enrollment.learner.email,
)
command.execute()
Expand Down Expand Up @@ -143,7 +143,7 @@ def send_quiz_content(self, delivery_schedule: DeliverySchedule) -> bool:
delivery_schedule.save()

command = SendQuizCommand(
quiz_id=delivery_schedule.delivery.course_content.quiz.id,
content_id=delivery_schedule.delivery.course_content.id,
email=delivery_schedule.delivery.enrollment.learner.email,
link=delivery_schedule.link,
)
Expand Down
11 changes: 11 additions & 0 deletions django_email_learning/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,17 @@ def enrollments_count(self) -> dict[str, int]:
"total": total_count,
}

def generate_unsubscribe_link(self, email: str) -> str:
payload = {
"email": email,
"course_slug": self.slug,
"organization_id": self.organization.id,
}
token = jwt_service.generate_jwt(payload=payload)
unsubscribe_path = reverse("django_email_learning:personalised:unsubscribe")
link = f"{settings.DJANGO_EMAIL_LEARNING['SITE_BASE_URL']}{unsubscribe_path}?token={token}"
return link


class Lesson(models.Model):
title = models.CharField(max_length=200)
Expand Down
2 changes: 2 additions & 0 deletions django_email_learning/personalised/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django_email_learning.personalised.views import (
QuizPublicView,
VerifyEnrollmentView,
UnsubscribeView,
)

app_name = "django_email_learning"
Expand All @@ -11,4 +12,5 @@
path(
"verify-enrollment/", VerifyEnrollmentView.as_view(), name="verify_enrollment"
),
path("unsubscribe/", UnsubscribeView.as_view(), name="unsubscribe"),
]
111 changes: 78 additions & 33 deletions django_email_learning/personalised/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from django_email_learning.services.command_models.verify_enrollment_command import (
VerifyEnrollmentCommand,
)
from django_email_learning.services.command_models.unsubscribe_command import (
UnsubscribeCommand,
)
import uuid
import logging

Expand All @@ -35,17 +38,47 @@ def errr_response(
status=status_code,
)

def get_decoded_token(self, request) -> dict | HttpResponse: # type: ignore[no-untyped-def]
try:
token = request.GET["token"]
except KeyError as e:
return self.errr_response(
message=_("The link is not valid."),
exception=e,
status_code=400,
title=_("Invalid Link"),
)
try:
return jwt_service.decode_jwt(token=token)
except jwt_service.InvalidTokenException as e:
return self.errr_response(
message=_("The link is not valid."),
exception=e,
status_code=400,
title=_("Invalid Link"),
)
except jwt_service.ExpiredTokenException as e:
return self.errr_response(
message=_("The link has expired."),
exception=e,
status_code=410,
title=_("Expired Link"),
)


class QuizPublicView(View, ErrrorLoggingMixin):
template_name = "personalised/quiz_public.html"

def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-untyped-def]
try:
token = request.GET["token"]
decoded = jwt_service.decode_jwt(token=token)
question_ids = decoded.get("question_ids", [])
decoded_token = self.get_decoded_token(request)
if isinstance(decoded_token, HttpResponse):
return decoded_token # Return error response if token is invalid
question_ids = decoded_token.get("question_ids", [])
delivery = ContentDelivery.objects.get(
id=decoded["delivery_id"], hash_value=decoded["delivery_hash"]
id=decoded_token["delivery_id"],
hash_value=decoded_token["delivery_hash"],
)
enrolment = delivery.enrollment
if enrolment.status != EnrollmentStatus.ACTIVE:
Expand Down Expand Up @@ -113,37 +146,14 @@ def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-unty


class VerifyEnrollmentView(View, ErrrorLoggingMixin):
template_name = "personalised/verify_enrollment.html"
template_name = "personalised/command_result.html"

def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-untyped-def]
try:
token = request.GET["token"]
except KeyError as e:
return self.errr_response(
message=_("The verification link is not valid."),
exception=e,
status_code=400,
title=_("Invalid Link"),
)
try:
decoded = jwt_service.decode_jwt(token=token)
except jwt_service.InvalidTokenException as e:
return self.errr_response(
message=_("The verification link is not valid."),
exception=e,
status_code=400,
title=_("Invalid Link"),
)
except jwt_service.ExpiredTokenException as e:
return self.errr_response(
message=_("The verification link has expired."),
exception=e,
status_code=410,
title=_("Expired Link"),
)

enrollment_id = decoded["enrollment_id"]
verification_code = decoded["verification_code"]
decoded_token = self.get_decoded_token(request)
if isinstance(decoded_token, HttpResponse):
return decoded_token # Return error response if token is invalid
enrollment_id = decoded_token["enrollment_id"]
verification_code = decoded_token["verification_code"]

command = VerifyEnrollmentCommand(
command_name="verify_enrollment",
Expand All @@ -159,4 +169,39 @@ def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-unty
title=_("Verification Error"),
)

return self.render_to_response(context={"page_title": _("Enrollment Verified")})
return self.render_to_response(
context={
"page_title": _("Enrollment Verified"),
"success_message": _("Your enrollment has been successfully verified."),
}
)


class UnsubscribeView(View, ErrrorLoggingMixin):
template_name = "personalised/command_result.html"

def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-untyped-def]
decoded_token = self.get_decoded_token(request)
if isinstance(decoded_token, HttpResponse):
return decoded_token # Return error response if token is invalid
command = UnsubscribeCommand(
email=decoded_token["email"],
course_slug=decoded_token["course_slug"],
organization_id=decoded_token["organization_id"],
)
try:
command.execute()
except Exception as e:
return self.errr_response(
message=_("An error occurred during unsubscription."),
exception=e,
title=_("Unsubscription Error"),
)
return self.render_to_response(
context={
"page_title": _("Unsubscribed"),
"success_message": _(
"You have been successfully unsubscribed from our mailing list."
),
}
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django_email_learning.services.command_models.abstract_command import (
AbstractCommand,
)
from django_email_learning.models import Lesson
from django_email_learning.models import Lesson, CourseContent
from django_email_learning.services.email_sender_service import EmailSenderService
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
Expand All @@ -14,26 +14,33 @@ class LessonNotFoundError(Exception):

class SendLessonCommand(AbstractCommand):
command_name: Literal["send_lesson"] = "send_lesson"
lesson_id: int
content_id: int
email: str

def execute(self) -> None:
content = CourseContent.objects.get(id=self.content_id)
if not content.lesson:
raise LessonNotFoundError(
f"CourseContent with ID {self.content_id} has no associated lesson"
)
self.logger.info(
f"Sending lesson with ID {self.lesson_id} to email {self.email}"
f"Sending lesson with ID {content.lesson.id} to email {self.email}"
)

try:
lesson = Lesson.objects.get(id=self.lesson_id)
lesson = Lesson.objects.get(id=content.lesson.id)
except Lesson.DoesNotExist:
raise LessonNotFoundError(f"Lesson with ID {self.lesson_id} not found")
raise LessonNotFoundError(f"Lesson with ID {content.lesson.id} not found")

subject = lesson.title
context = {
"lesson": lesson,
"unsubscribe_link": content.course.generate_unsubscribe_link(self.email),
}
payload = render_to_string("emails/lesson.txt", context)

email_service = EmailSenderService()

email_message = EmailMultiAlternatives(
subject=subject,
body=payload,
Expand Down
18 changes: 13 additions & 5 deletions django_email_learning/services/command_models/send_quiz_command.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django_email_learning.services.command_models.abstract_command import (
AbstractCommand,
)
from django_email_learning.models import Quiz
from django_email_learning.models import Quiz, CourseContent
from django_email_learning.services.email_sender_service import EmailSenderService
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
Expand All @@ -16,20 +16,28 @@ class SendQuizCommand(AbstractCommand):
command_name: Literal["send_quiz"] = "send_quiz"
link: str
email: str
quiz_id: int
content_id: int

def execute(self) -> None:
self.logger.info(f"Sending quiz with ID {self.quiz_id} to email {self.email}")
content = CourseContent.objects.get(id=self.content_id)
if not content.quiz:
raise QuizNotFoundError(
f"CourseContent with ID {self.content_id} has no associated quiz"
)
self.logger.info(
f"Sending quiz with ID {content.quiz.id} to email {self.email}"
)

try:
quiz = Quiz.objects.get(id=self.quiz_id)
quiz = Quiz.objects.get(id=content.quiz.id)
except Quiz.DoesNotExist:
raise QuizNotFoundError(f"Quiz with ID {self.quiz_id} not found")
raise QuizNotFoundError(f"Quiz with ID {content.quiz.id} not found")

subject = quiz.title
context = {
"quiz": quiz,
"link": self.link,
"unsubscribe_link": content.course.generate_unsubscribe_link(self.email),
}
payload = render_to_string("emails/quiz.txt", context)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django_email_learning.models import (
Course,
Enrollment,
DeliveryStatus,
Learner,
EnrollmentStatus,
DeactivationReason,
Expand Down Expand Up @@ -54,6 +55,12 @@ def execute(self) -> None:
)
return

for enrollment in enrollments:
for delivery in enrollment.content_deliveries.all():
delivery.delivery_schedules.filter(
status=DeliveryStatus.SCHEDULED
).update(status=DeliveryStatus.CANCELED)

enrollments.update(
status=EnrollmentStatus.DEACTIVATED,
deactivation_reason=DeactivationReason.CANCELED,
Expand Down
6 changes: 5 additions & 1 deletion django_email_learning/templates/emails/lesson.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends "emails/base.html" %}

{% load i18n %}
{% block extra_styles %}
pre {
background-color: #f4f4f4;
Expand All @@ -13,4 +13,8 @@
<h2 style="color: #2c3e50; margin-top: 0; text-align: center; border-bottom: 2px solid {{ brand_color }}; padding-bottom: 15px;">{{ lesson.title }}</h2>

{{ lesson.content|safe }}

<p style="font-size: 0.9em; color: #7f8c8d; text-align: center; margin-top: 30px;">
{% blocktranslate %}If you wish to unsubscribe from these course, please <a href="{{ unsubscribe_link }}" style="color: {{ brand_color }}; text-decoration: none;">click here</a>{% endblocktranslate %}.
</p>
{% endblock %}
3 changes: 3 additions & 0 deletions django_email_learning/templates/emails/lesson.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{% load i18n %}
{{ lesson.title }}

{{ lesson.content|striptags }}

{% translate "Unsubscribe link:" %} {{ unsubscribe_link }}
3 changes: 3 additions & 0 deletions django_email_learning/templates/emails/quiz.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ <h2>{{ quiz.title }}</h2>
</a>
</div>
<p>{% translate "Best of luck!" %}</p>
<p style="font-size: 0.9em; color: #7f8c8d; text-align: center; margin-top: 30px;">
{% blocktranslate %}If you wish to unsubscribe from these course, please <a href="{{ unsubscribe_link }}" style="color: {{ brand_color }}; text-decoration: none;">click here</a>{% endblocktranslate %}.
</p>
{% endblock %}
2 changes: 2 additions & 0 deletions django_email_learning/templates/emails/quiz.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@

{% translate "Please click the link below to access and submit your quiz:" %}
{{ link }}

{% translate "Unsubscribe link:" %} {{ unsubscribe_link }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends "personalised/base.html" %}
{% load i18n %}
{% load django_vite %}
{% block head_script %}
<script>
const success_message = "{{ success_message|escapejs }}";
</script>
{% vite_asset 'personalised/command_result/CommandResult.jsx' %}
{% endblock %}

This file was deleted.

7 changes: 0 additions & 7 deletions docs/source/introduction.rst
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
Introduction
============

.. image::
https://github.com/AvaCodeSolutions/django-email-learning/raw/master/assets/Django2@2x.png
:alt: Django Email Learning
:align: center
:width: 200 px


Django Email Learning is an open-source Django application by `AvaCode Solutions`_ designed to help educators,
mentors, and developers create and manage automated learning paths.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import render from '../../src/render.jsx';
import Layout from '../../public/components/Layout.jsx';


const Verification = () => {
const CommandResult = () => {
return <Layout>
{ !error_message ?<Alert severity='success' sx={{ maxWidth: 800, margin: '0 auto', backgroundColor: "background.light" }}>
{ localeMessages['verify_enrollment_success'] }
{success_message}
</Alert> : <Alert severity="error" sx={{ maxWidth: 800, margin: '20px auto' }}>
{error_message} (ref: {ref})
</Alert>}
</Layout>
}

render({children: <Verification />});
render({children: <CommandResult />});
Loading