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
11 changes: 10 additions & 1 deletion django_email_learning/jobs/deliver_contents_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,16 @@ def run(self) -> None:
job_execution.finished_at = timezone.now()
job_execution.save()
else:
self.process_delivery(delivery_schedule)
try:
self.process_delivery(delivery_schedule)
except Exception as e:
# Unhandled exception during delivery processing should not crash the job.
# We log the error and mark the delivery as blocked to prevent further attempts until manual intervention.
delivery_schedule.status = DeliveryStatus.BLOCKED
delivery_schedule.save()
logger.exception(
f"Error processing delivery schedule: {str(e)}. Continuing with next task."
)

def get_delivery_queue(self) -> DeliveryQueueProtocol:
try:
Expand Down
10 changes: 10 additions & 0 deletions django_email_learning/personalised/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,16 @@ def get(self, request, *args, **kwargs) -> HttpResponse: # type: ignore[no-unty
decoded_token = self.get_decoded_token(request)
if isinstance(decoded_token, HttpResponse):
return decoded_token # Return error response if token is invalid
if request.GET.get("confirm") != "true":
return self.render_to_response(
context={
"page_title": _("Confirm Unsubscription"),
"confirmation_message": _(
"Are you sure you want to unsubscribe from our mailing list?"
),
"confirm_url": f"{request.path}?token={request.GET.get('token')}&confirm=true",
}
)
command = UnsubscribeCommand(
email=decoded_token["email"],
course_slug=decoded_token["course_slug"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def execute(self) -> None:
return

enrollments = Enrollment.objects.filter(learner=learner, course=course).exclude(
status=EnrollmentStatus.DEACTIVATED
status__in=[EnrollmentStatus.DEACTIVATED, EnrollmentStatus.COMPLETED]
)
if not enrollments.exists():
self.logger.warning(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
{% block head_script %}
<script>
const success_message = "{{ success_message|escapejs }}";
const confirmation_message = "{{ confirmation_message|escapejs }}";
const confirm_url = "{{ confirm_url|escapejs }}";
const localeMessages = {
"Confirm": "{% translate 'Confirm' %}",
}
</script>
{% vite_asset 'personalised/command_result/CommandResult.jsx' %}
{% endblock %}
6 changes: 4 additions & 2 deletions frontend/personalised/command_result/CommandResult.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Alert, Box } from '@mui/material';
import { Alert, Box, Button } from '@mui/material';
import render from '../../src/render.jsx';
import Layout from '../../public/components/Layout.jsx';


const CommandResult = () => {
return <Layout>
{ !error_message ?<Alert severity='success' sx={{ maxWidth: 800, margin: '0 auto', backgroundColor: "background.light" }}>
{ !error_message && !confirmation_message ?<Alert severity='success' sx={{ maxWidth: 800, margin: '0 auto', backgroundColor: "background.light" }}>
{success_message}
</Alert> : confirmation_message ? <Alert severity="warning" sx={{ maxWidth: 800, margin: '20px auto' }}>
{confirmation_message}<Box mt={2}><Button href={confirm_url} variant='contained' mt={2}>{localeMessages["Confirm"]}</Button></Box>
</Alert> : <Alert severity="error" sx={{ maxWidth: 800, margin: '20px auto' }}>
{error_message} (ref: {ref})
</Alert>}
Expand Down
31 changes: 31 additions & 0 deletions tests/jobs/test_deliver_content_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,34 @@ def test_deliver_contents_job_reschedules_failed_delivery_and_increments_attempt
assert delivery_schedule.status == DeliveryStatus.SCHEDULED
assert delivery_schedule.failed_attempts == 2
assert delivery_schedule.time > original_time


def test_unhandled_exception_during_delivery_processing(
db, delivery_queue_mock, enrollment, course_lesson_content
):
# Create mock DeliverySchedule object
enrollment.status = EnrollmentStatus.ACTIVE
enrollment.save()

delivery = ContentDelivery.objects.create(
enrollment=enrollment, course_content=course_lesson_content
)

delivery_schedule = DeliverySchedule.objects.create(delivery=delivery)

# Add task to the mock delivery queue
delivery_queue_mock.add_task(delivery_schedule)

job = DeliverContentsJob()

# Patch the process_delivery method to always raise an exception
with patch.object(
DeliverContentsJob,
"process_delivery",
side_effect=Exception("Simulated processing failure"),
):
job.run()

# After running the job, the delivery schedule should be in BLOCKED status due to unhandled exception
delivery_schedule.refresh_from_db()
assert delivery_schedule.status == DeliveryStatus.BLOCKED
26 changes: 25 additions & 1 deletion tests/personalised/test_views/test_unsubscribe_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_unsubscribe_valid_token(command, enrollment, anonymous_client):
}
)

response = anonymous_client.get(f"{URL}?token={token}")
response = anonymous_client.get(f"{URL}?token={token}&confirm=true")

assert command.return_value.execute.called
assert response.status_code == 200
Expand All @@ -28,6 +28,30 @@ def test_unsubscribe_valid_token(command, enrollment, anonymous_client):
)


@patch("django_email_learning.personalised.views.UnsubscribeCommand")
def test_unsubscribe_valid_token_confirmation(command, enrollment, anonymous_client):
token = jwt_service.generate_jwt(
{
"email": enrollment.learner.email,
"course_slug": enrollment.course.slug,
"organization_id": enrollment.course.organization.id,
}
)

response = anonymous_client.get(f"{URL}?token={token}")

assert not command.return_value.execute.called
assert response.status_code == 200
assert "page_title" in response.context
assert response.context["page_title"] == "Confirm Unsubscription"
assert (
response.context["confirmation_message"]
== "Are you sure you want to unsubscribe from our mailing list?"
)
assert "confirm_url" in response.context
assert response.context["confirm_url"] == f"{URL}?token={token}&confirm=true"


def test_unsubscribe_invalid_token(anonymous_client):
response = anonymous_client.get(f"{URL}?token=invalidtoken")
assert response.status_code == 400
Expand Down
4 changes: 3 additions & 1 deletion tests/services/test_jwt_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ def test_jwt_service_invalid_token():
)
# Create an invalid token by altering the signature
invalid_token = jwt.encode(
payload_copy, "INVALID_SECRET", algorithm=jwt_service.ALGORITHM
payload_copy,
"INVALID_LONG_SECRET_KEY_FOR_TESTING_PURPOSES_ONLY",
algorithm=jwt_service.ALGORITHM,
)

with pytest.raises(jwt_service.InvalidTokenException):
Expand Down