Skip to content

Commit

Permalink
feat: add watchers functionality to the courses (#4013)
Browse files Browse the repository at this point in the history
  • Loading branch information
AfaqShuaib09 authored Jul 18, 2023
1 parent ae69e29 commit 50dcc9a
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 5 deletions.
5 changes: 4 additions & 1 deletion course_discovery/apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,9 @@ class CourseSerializer(TaggitSerializer, MinimalCourseSerializer):
location_restriction = CourseLocationRestrictionSerializer(required=False)
in_year_value = ProductValueSerializer(required=False)
product_source = serializers.SlugRelatedField(required=False, slug_field='slug', queryset=Source.objects.all())
watchers = serializers.ListField(
child=serializers.EmailField(), allow_empty=True, allow_null=True, required=False
)

def to_representation(self, instance):
"""
Expand Down Expand Up @@ -1354,7 +1357,7 @@ class Meta(MinimalCourseSerializer.Meta):
'url_slug_history', 'url_redirects', 'course_run_statuses', 'editors', 'collaborators', 'skill_names',
'skills', 'organization_short_code_override', 'organization_logo_override_url',
'enterprise_subscription_inclusion', 'geolocation', 'location_restriction', 'in_year_value',
'product_source', 'data_modified_timestamp', 'excluded_from_search', 'excluded_from_seo'
'product_source', 'data_modified_timestamp', 'excluded_from_search', 'excluded_from_seo', 'watchers',
)
read_only_fields = ('enterprise_subscription_inclusion', 'product_source', 'data_modified_timestamp')
extra_kwargs = {
Expand Down
3 changes: 2 additions & 1 deletion course_discovery/apps/api/tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ def get_expected_data(cls, course, course_skill, request):
'location_restriction': CourseLocationRestrictionSerializer(
course.location_restriction
).data,
'in_year_value': ProductValueSerializer(course.in_year_value).data
'in_year_value': ProductValueSerializer(course.in_year_value).data,
'watchers': [],
})

return expected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1754,7 +1754,8 @@ def test_patch_non_review_fields_does_not_reset_run_status(self):
'42YAAAAASUVORK5CYII=',
'video': {'src': 'https://new-videos-r-us/watch?t_s=5'},
'geolocation': {'location_name': 'Antarctica', 'lng': '32.86', 'lat': '34.21'},
'location_restriction': {'restriction_type': 'blocklist', 'countries': ['AL'], 'states': ['AZ']}
'location_restriction': {'restriction_type': 'blocklist', 'countries': ['AL'], 'states': ['AZ']},
'watchers': ['[email protected]']
}
response = self.client.patch(url, patch_data, format='json')
assert response.status_code == 200
Expand All @@ -1764,6 +1765,7 @@ def test_patch_non_review_fields_does_not_reset_run_status(self):
official_course_run.refresh_from_db()
assert draft_course_run.status == CourseRunStatus.Reviewed
assert official_course_run.status == CourseRunStatus.Reviewed
assert draft_course.watchers == ['[email protected]']

@responses.activate
def test_patch_published(self):
Expand Down
40 changes: 40 additions & 0 deletions course_discovery/apps/course_metadata/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from opaque_keys.edx.keys import CourseKey

from course_discovery.apps.core.models import User
from course_discovery.apps.course_metadata.choices import CourseRunStatus
from course_discovery.apps.publisher.choices import InternalUserRole
from course_discovery.apps.publisher.constants import LEGAL_TEAM_GROUP_NAME
from course_discovery.apps.publisher.utils import is_email_notification_enabled
Expand Down Expand Up @@ -220,6 +221,45 @@ def send_email_for_legal_review(course_run):
send_email_to_legal(course_run, 'course_metadata/email/legal_review', subject)


def send_email_to_notify_course_watchers(course, course_run_publish_date, course_run_status):
"""
Send email to the watchers of the course when the course run is scheduled or published.
Arguments:
course (Object): Course object
course_run_publish_date (datetime): Course run publish date
course_run_status (str): Course run status
"""
subject = _('Course URL for {title}').format(title=course.title)
context = {
'course_name': course.title,
'marketing_service_name': settings.MARKETING_SERVICE_NAME,
'course_publish_date': course_run_publish_date.strftime("%m/%d/%Y"),
'is_course_published': course_run_status == CourseRunStatus.Published,
'course_marketing_url': course.marketing_url,
'course_preview_url': course.preview_url,
}
to_users = course.watchers
txt_template = 'course_metadata/email/watchers_course_url.txt'
html_template = 'course_metadata/email/watchers_course_url.html'
template = get_template(txt_template)
plain_content = template.render(context)
template = get_template(html_template)
html_content = template.render(context)

email_msg = EmailMultiAlternatives(
subject, plain_content, settings.PUBLISHER_FROM_EMAIL, to_users
)
email_msg.attach_alternative(html_content, 'text/html')

try:
email_msg.send()
except Exception as exc: # pylint: disable=broad-except
logger.exception(
f'Failed to send email notification with subject "{subject}" to users {to_users}. Error: {exc}'
)


def send_email_for_internal_review(course_run):
""" Send email when a course run is submitted for internal review.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.2.20 on 2023-07-17 08:04

from django.db import migrations
import multi_email_field.fields


class Migration(migrations.Migration):

dependencies = [
('course_metadata', '0328_additionalmetadata_display_on_org_page'),
]

operations = [
migrations.AddField(
model_name='course',
name='watchers',
field=multi_email_field.fields.MultiEmailField(blank=True, default=[], help_text='The list of email addresses that will be notified if any of the course runs is published or scheduled.', verbose_name='Watchers'),
),
migrations.AddField(
model_name='historicalcourse',
name='watchers',
field=multi_email_field.fields.MultiEmailField(blank=True, default=[], help_text='The list of email addresses that will be notified if any of the course runs is published or scheduled.', verbose_name='Watchers'),
),
]
28 changes: 26 additions & 2 deletions course_discovery/apps/course_metadata/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from elasticsearch_dsl.query import Q as ESDSLQ
from localflavor.us.us_states import CONTIGUOUS_STATES
from model_utils import FieldTracker
from multi_email_field.fields import MultiEmailField
from multiselectfield import MultiSelectField
from opaque_keys.edx.keys import CourseKey
from parler.models import TranslatableModel, TranslatedFieldsModel
Expand Down Expand Up @@ -1378,13 +1379,23 @@ class Course(DraftModelMixin, PkSearchableMixin, CachedMixin, TimeStampedModel):
help_text=_('If checked, the About Page will have a meta tag with noindex value')
)

watchers = MultiEmailField(
blank=True,
verbose_name=_("Watchers"),
help_text=_(
"The list of email addresses that will be notified if any of the course runs "
"is published or scheduled."
)
)

# Changing these fields at the course level will not trigger re-reviews
# on related course runs that are already in the scheduled state
STATUS_CHANGE_EXEMPT_FIELDS = [
'additional_metadata',
'geolocation',
'in_year_value',
'location_restriction',
'watchers'
]

everything = CourseQuerySet.as_manager()
Expand Down Expand Up @@ -1497,11 +1508,21 @@ def original_image_url(self):
def marketing_url(self):
url = None
if self.partner.marketing_site_url_root:
path = self.get_course_marketing_url_path()
path = self.get_course_url_path()
url = urljoin(self.partner.marketing_site_url_root, path)

return url

@property
def preview_url(self):
""" Returns the preview url for the course. """
url = None
if self.partner.marketing_site_url_root and self.active_url_slug:
path = self.get_course_url_path()
url = urljoin(self.partner.marketing_site_url_root, f'preview/{path}')

return url

@property
def active_url_slug(self):
""" Official rows just return whatever slug is active, draft rows will first look for an associated active
Expand All @@ -1513,7 +1534,7 @@ def active_url_slug(self):
active_url = self.official_version.url_slug_history.filter(is_active_on_draft=True).first()
return getattr(active_url, 'url_slug', None)

def get_course_marketing_url_path(self):
def get_course_url_path(self):
"""
Returns marketing url path for course based on active url slug
"""
Expand Down Expand Up @@ -2546,6 +2567,9 @@ def handle_status_change(self, send_emails):

if send_emails and email_method:
email_method(self)
if (self.course.watchers and (self.status in [CourseRunStatus.Reviewed, CourseRunStatus.Published])):
self.refresh_from_db()
emails.send_email_to_notify_course_watchers(self.course, self.go_live_date, self.status)

def _check_enterprise_subscription_inclusion(self):
if not self.course.enterprise_subscription_inclusion:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% extends "course_metadata/email/email_base.html" %}
{% load i18n %}
{% load django_markup %}
{% block body %}

<p>
{% if is_course_published %}
{% blocktrans %}
Course '{{course_name}}' has been successfully reviewed by edX and is now published on {{course_publish_date}}. To see live course, go here: <a href="{{course_marketing_url}}">{{course_marketing_url}}</a>
{% endblocktrans %}
{% else %}
{% blocktrans %}
Course '{{course_name}}' has been successfully reviewed by edX and is now ready for publication. The course will be published on {{course_publish_date}}.
To see a preview of the Course About Page, go here: <a href="{{course_marketing_url}}">{{course_marketing_url}}</a>
{% endblocktrans %}
{% endif %}
</p>

<p>
<strong>Note: The changes will be live on http://edx.org once {{marketing_service_name}} build runs.
The average turnaround time is between 24 to 48 hours.</strong>
</p>

<!-- End Message Body -->
{% endblock body %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% load i18n %}

{% if is_course_published %}
{% blocktrans %}
Course '{{course_name}}' has been successfully reviewed by edX and is now published on {{course_publish_date}}. To see live course, go here: {{course_marketing_url}}
{% endblocktrans %}
{% else %}
{% blocktrans %}
Course '{{course_name}}' has been successfully reviewed by edX and is now ready for publication. The course will be published on {{course_publish_date}}.
To see a preview of the Course About Page, go here: {{course_preview_url}}
{% endblocktrans %}
{% endif %}
Note: The changes will be live on http://edx.org once {{marketing_service_name}} build runs.
The average turnaround time is between 24 to 48 hours.
27 changes: 27 additions & 0 deletions course_discovery/apps/course_metadata/tests/test_emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.conf import settings
from django.contrib.auth.models import Group
from django.core import mail
from django.template.loader import render_to_string
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from testfixtures import LogCapture, StringComparison
Expand Down Expand Up @@ -147,6 +148,32 @@ def test_send_email_for_legal_review(self):
],
)

def test_send_email_to_notify_course_watchers(self):
"""
Verify that send_email_to_notify_course_watchers's happy path works as expected
"""
test_course_run = CourseRunFactory(course=self.course, status=CourseRunStatus.Published)
test_course_run.go_live_date = datetime.datetime.now()
self.course.watchers = ['[email protected]']
self.course.save()
emails.send_email_to_notify_course_watchers(self.course, test_course_run.go_live_date, test_course_run.status)
email = mail.outbox[0]

assert email.to == self.course.watchers
assert str(email.subject) == f'Course URL for {self.course.title}'
assert len(mail.outbox) == 1
assert email.alternatives[0][1] == 'text/html'

expected_content = render_to_string('course_metadata/email/watchers_course_url.html', {
'is_course_published': True,
'course_name': self.course.title,
'course_publish_date': test_course_run.go_live_date.strftime("%m/%d/%Y"),
'course_marketing_url': self.course.marketing_url,
'marketing_service_name': settings.MARKETING_SERVICE_NAME,
})
# Compare the expected template content with the email body
assert email.alternatives[0][0] == expected_content

def test_send_email_for_internal_review(self):
"""
Verify that send_email_for_internal_review's happy path works as expected
Expand Down
Loading

0 comments on commit 50dcc9a

Please sign in to comment.