-
-
Notifications
You must be signed in to change notification settings - Fork 303
Add ability to send a notification email on review of community libra… #6050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: hotfixes
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -86,7 +88,9 @@ | |
| from contentcuration.db.models.manager import CustomContentNodeTreeManager | ||
| from contentcuration.db.models.manager import CustomManager | ||
| from contentcuration.utils.cache import delete_public_channel_cache_keys | ||
| from contentcuration.utils.messages import get_messages | ||
| 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 +3053,53 @@ 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 | ||
|
|
||
| community_strings = get_messages().get("CommunityChannelsStrings", {}) | ||
| status_message = ( | ||
| community_strings["approvedStatus"] | ||
| if is_approved | ||
| else community_strings["flaggedStatus"] | ||
| ) | ||
| subject_text = "{}: {}".format( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: The subject is a user-visible sentence assembled from two independently-translated fragments and a hard-coded |
||
| community_strings["communityLibrarySubmissionLabel"], status_message | ||
| ) | ||
|
|
||
| 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": self.author.get_full_name(), | ||
| "channel": self.channel, | ||
| "channel_url": canonical_url( | ||
| reverse("channel", kwargs={"channel_id": self.channel.pk}) | ||
| ), | ||
| "approved": is_approved, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: |
||
| "status_message": ( | ||
| community_strings["availableStatus"] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: The approval email says "Available in Community Library" before the channel actually is. |
||
| if is_approved | ||
| else community_strings["needsChangesPrimaryInfo"] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: These strings carry a translator This is also the first backend use of |
||
| ), | ||
| "feedback_notes_label": community_strings["feedbackNotesLabel"], | ||
| "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: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <!DOCTYPE html> | ||
| {% load i18n %} | ||
| <html> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: No {% get_current_language as LANG %}{% get_current_language_bidi as LANG_BIDI %}
<html lang="{{ LANG }}" dir="{% if LANG_BIDI %}rtl{% else %}ltr{% endif %}"> |
||
| <head> | ||
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| </head> | ||
| <body> | ||
| {% autoescape off %} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Nothing in this template emits markup from context — |
||
| <p>{% blocktrans with name=name %}Hello {{ name }},{% endblocktrans %}</p> | ||
|
|
||
| <p><a href="{{ channel_url }}" target="_blank">{% blocktrans with channel_name=channel.name %}{{ channel_name }}{% endblocktrans %}</a> ({{ channel_url }})</p> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Resolved — addressed in the current code. nitpick: |
||
|
|
||
| <p>{{ status_message }}</p> | ||
|
|
||
| {% if feedback_notes %} | ||
| <p>{{ feedback_notes_label }}: {{ feedback_notes }}</p> | ||
| {% endif %} | ||
|
|
||
| <p> | ||
| {% translate "Thanks for using Kolibri Studio!" %} | ||
| <br> | ||
| {% translate "The Learning Equality Team" %} | ||
| </p> | ||
| {% endautoescape %} | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Resolved — addressed in the current code. praise: Both paths assert against the real locmem mail backend — recipient, subject, body copy, channel name, and feedback notes — rather than mocking the mail layer. Solid end-to-end coverage of the new path. |
||
| sent_email = mail.outbox[0] | ||
| self.assertEqual(sent_email.to, [self.submission.author.email]) | ||
| self.assertIn("approved", sent_email.subject.lower()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: These literals are the current values of Two cheap additions while you're here: assert |
||
| 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("needs 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() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -358,4 +358,6 @@ def resolve(self, request, pk=None): | |
| published_version.id | ||
| ) | ||
|
|
||
| submission.send_resolution_email() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. blocking: A mail failure here 500s a resolution that has already committed, and the admin can't retry.
Same path swallows the Moving the send into |
||
|
|
||
| return Response(self.serialize_object()) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blocking: The email is rendered in the reviewing admin's language, not the recipient's.
get_messages()keys offto_locale(get_language()), and the{% translate %}tags in the template resolve against the same active language.send_resolution_email()runs synchronously inside the admin'sresolverequest, andKolibriStudioLocaleMiddlewareis installed (settings.py:133), so the active locale is whatever the reviewer's session/Accept-Languagesays. A reviewer browsing Studio in Spanish sends the author a Spanish email.Studio's other transactional emails avoid this by accident — they render inside Celery tasks (
utils/publish.py,tasks.py) where no request locale is active, so they fall back toLANGUAGE_CODE.Userhas no language preference field, so the recipient's language isn't knowable today; wrapping the subject/body render intranslation.override(settings.LANGUAGE_CODE)would at least make this deterministic and consistent with the rest of Studio's mail.