From 63291a2812eb988d57a8070ce7d531ba6c4efc60 Mon Sep 17 00:00:00 2001 From: Marcella Maki Date: Tue, 21 Jul 2026 17:01:11 -0400 Subject: [PATCH] Add ability to send a notification email on review of community library submission --- contentcuration/contentcuration/models.py | 39 +++++++++++++++++++ .../submission_resolved_email.html | 31 +++++++++++++++ .../test_community_library_submission.py | 16 ++++++++ .../viewsets/community_library_submission.py | 1 + 4 files changed, 87 insertions(+) create mode 100644 contentcuration/contentcuration/templates/community_library/submission_resolved_email.html diff --git a/contentcuration/contentcuration/models.py b/contentcuration/contentcuration/models.py index 1f0c1ec8c5..26313755cd 100644 --- a/contentcuration/contentcuration/models.py +++ b/contentcuration/contentcuration/models.py @@ -48,6 +48,8 @@ from django.db.models.query_utils import DeferredAttribute from django.db.models.sql import Query from django.dispatch import receiver +from django.template.loader import render_to_string +from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext as _ from django_cte import CTEManager @@ -87,6 +89,7 @@ from contentcuration.db.models.manager import CustomManager from contentcuration.utils.cache import delete_public_channel_cache_keys from contentcuration.utils.parser import load_json_string +from contentcuration.utils.urls import canonical_url from contentcuration.viewsets.sync.constants import ALL_CHANGES from contentcuration.viewsets.sync.constants import ALL_TABLES from contentcuration.viewsets.sync.constants import PUBLISHABLE_CHANGE_TABLES @@ -3049,6 +3052,42 @@ def notify_update_to_channel_editors(self, exclude_user_id=None): User.notify_users(editors, date=self.date_updated) + def send_resolution_email(self): + """ + Send an email to the submission author letting them know their + Community Library submission has been resolved (approved or + rejected). + """ + is_approved = self.status == community_library_submission.STATUS_APPROVED + + if is_approved: + subject_text = _("Your Community Library submission has been approved") + else: + subject_text = _("Your Community Library submission requires changes") + + subject = render_to_string( + "registration/custom_email_subject.txt", + {"subject": subject_text}, + ) + subject = "".join(subject.splitlines()) + + message = render_to_string( + "community_library/submission_resolved_email.html", + { + "name": f"{self.author.first_name} {self.author.last_name}".strip(), + "channel": self.channel, + "channel_url": canonical_url( + reverse("channel", kwargs={"channel_id": self.channel.pk}) + ), + "approved": is_approved, + "feedback_notes": self.feedback_notes, + }, + ) + + self.author.email_user( + subject, message, settings.DEFAULT_FROM_EMAIL, html_message=message + ) + @classmethod def filter_view_queryset(cls, queryset, user): if user.is_anonymous: diff --git a/contentcuration/contentcuration/templates/community_library/submission_resolved_email.html b/contentcuration/contentcuration/templates/community_library/submission_resolved_email.html new file mode 100644 index 0000000000..0ca5e7a5da --- /dev/null +++ b/contentcuration/contentcuration/templates/community_library/submission_resolved_email.html @@ -0,0 +1,31 @@ + +{% load i18n %} + + + + + + + {% autoescape off %} +

{% blocktrans with name=name %}Hello {{ name }},{% endblocktrans %}

+ +

{% blocktrans with channel_name=channel.name %}{{ channel_name }}{% endblocktrans %} ({{ channel_url }})

+ + {% if approved %} +

{% translate "Available in Community Library" %}

+ {% else %} +

{% translate "Your previously submitted version needs changes. Make sure you have addressed all comments before resubmitting." %}

+ {% endif %} + + {% if feedback_notes %} +

{% translate "Notes from the reviewer" %}: {% blocktrans with notes=feedback_notes %}{{ notes }}{% endblocktrans %}

+ {% endif %} + +

+ {% translate "Thanks for using Kolibri Studio!" %} +
+ {% translate "The Learning Equality Team" %} +

+ {% endautoescape %} + + diff --git a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py index 4cd51fb2a1..4852a1afdb 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py +++ b/contentcuration/contentcuration/tests/viewsets/test_community_library_submission.py @@ -2,6 +2,7 @@ from unittest import mock import pytz +from django.core import mail from django.urls import reverse from contentcuration.constants import ( @@ -731,6 +732,13 @@ def test_resolve_submission__accept_correct(self, apply_task_mock): channel_id=self.submission.channel.id, ) + self.assertEqual(len(mail.outbox), 1) + sent_email = mail.outbox[0] + self.assertEqual(sent_email.to, [self.submission.author.email]) + self.assertIn("approved", sent_email.subject.lower()) + self.assertIn("available in community library", sent_email.body.lower()) + self.assertIn(self.submission.channel.name, sent_email.body) + @mock.patch( "contentcuration.viewsets.community_library_submission.apply_channel_changes_task" ) @@ -770,6 +778,14 @@ def test_resolve_submission__reject_correct(self, apply_task_mock): ) apply_task_mock.fetch_or_enqueue.assert_not_called() + self.assertEqual(len(mail.outbox), 1) + sent_email = mail.outbox[0] + self.assertEqual(sent_email.to, [self.submission.author.email]) + self.assertIn("requires changes", sent_email.subject.lower()) + self.assertIn("needs changes", sent_email.body.lower()) + self.assertIn(self.submission.channel.name, sent_email.body) + self.assertIn(self.feedback_notes, sent_email.body) + def test_resolve_submission__reject_missing_resolution_reason(self): self.client.force_authenticate(user=self.admin_user) metadata = self.resolve_reject_metadata.copy() diff --git a/contentcuration/contentcuration/viewsets/community_library_submission.py b/contentcuration/contentcuration/viewsets/community_library_submission.py index 33fc6f9a94..baa762cf40 100644 --- a/contentcuration/contentcuration/viewsets/community_library_submission.py +++ b/contentcuration/contentcuration/viewsets/community_library_submission.py @@ -346,6 +346,7 @@ def resolve(self, request, pk=None): ) submission.notify_update_to_channel_editors() + submission.send_resolution_email() if submission.status == community_library_submission_constants.STATUS_APPROVED: self._mark_previous_pending_submissions_as_superseded(submission)