From 741917e92b03f34447c4a1570acd37d5cb5dbac4 Mon Sep 17 00:00:00 2001 From: Nimisha Asthagiri Date: Thu, 12 Oct 2017 14:01:41 -0400 Subject: [PATCH] Course Update emails --- common/djangoapps/student/models.py | 2 - openedx/core/djangoapps/schedules/admin.py | 9 +- openedx/core/djangoapps/schedules/config.py | 6 ++ .../core/djangoapps/schedules/exceptions.py | 2 + .../management/commands/send_course_update.py | 14 +++ .../migrations/0005_auto_20171010_1722.py | 24 +++++ openedx/core/djangoapps/schedules/models.py | 2 + .../core/djangoapps/schedules/resolvers.py | 18 +++- openedx/core/djangoapps/schedules/tasks.py | 99 ++++++++++++++++++- .../edx_ace/courseupdate/email/body.html | 51 ++++++++++ .../edx_ace/courseupdate/email/body.txt | 9 ++ .../edx_ace/courseupdate/email/from_name.txt | 1 + .../edx_ace/courseupdate/email/head.html | 1 + .../edx_ace/courseupdate/email/subject.txt | 3 + .../edx_ace/upgradereminder/email/body.txt | 10 +- 15 files changed, 236 insertions(+), 15 deletions(-) create mode 100644 openedx/core/djangoapps/schedules/exceptions.py create mode 100644 openedx/core/djangoapps/schedules/management/commands/send_course_update.py create mode 100644 openedx/core/djangoapps/schedules/migrations/0005_auto_20171010_1722.py create mode 100644 openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/body.html create mode 100644 openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/body.txt create mode 100644 openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/from_name.txt create mode 100644 openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/head.html create mode 100644 openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/subject.txt diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 459342a493..03ee2e52c4 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -55,9 +55,7 @@ from courseware.models import DynamicUpgradeDeadlineConfiguration, CourseDynamic from enrollment.api import _default_course_mode from openedx.core.djangoapps.content.course_overviews.models import CourseOverview -from openedx.core.djangoapps.schedules.models import ScheduleConfig from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from openedx.core.djangoapps.theming.helpers import get_current_site from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, NoneToEmptyManager from track import contexts from util.milestones_helpers import is_entrance_exams_enabled diff --git a/openedx/core/djangoapps/schedules/admin.py b/openedx/core/djangoapps/schedules/admin.py index c4dcc313f6..d1485e519e 100644 --- a/openedx/core/djangoapps/schedules/admin.py +++ b/openedx/core/djangoapps/schedules/admin.py @@ -30,6 +30,9 @@ class ScheduleAdmin(admin.ModelAdmin): @admin.register(models.ScheduleConfig) class ScheduleConfigAdmin(admin.ModelAdmin): search_fields = ('site',) - list_display = ('site', 'create_schedules', 'enqueue_recurring_nudge', - 'deliver_recurring_nudge', 'enqueue_upgrade_reminder', - 'deliver_upgrade_reminder') + list_display = ( + 'site', 'create_schedules', + 'enqueue_recurring_nudge', 'deliver_recurring_nudge', + 'enqueue_upgrade_reminder', 'deliver_upgrade_reminder', + 'enqueue_course_update', 'deliver_course_update', + ) diff --git a/openedx/core/djangoapps/schedules/config.py b/openedx/core/djangoapps/schedules/config.py index d072c41519..0ae579e396 100644 --- a/openedx/core/djangoapps/schedules/config.py +++ b/openedx/core/djangoapps/schedules/config.py @@ -9,4 +9,10 @@ CREATE_SCHEDULE_WAFFLE_FLAG = CourseWaffleFlag( flag_undefined_default=False ) +COURSE_UPDATE_WAFFLE_FLAG = CourseWaffleFlag( + waffle_namespace=WAFFLE_FLAG_NAMESPACE, + flag_name=u'send_updates_for_course', + flag_undefined_default=False +) + DEBUG_MESSAGE_WAFFLE_FLAG = WaffleFlag(WAFFLE_FLAG_NAMESPACE, u'enable_debugging') diff --git a/openedx/core/djangoapps/schedules/exceptions.py b/openedx/core/djangoapps/schedules/exceptions.py new file mode 100644 index 0000000000..335ad8b726 --- /dev/null +++ b/openedx/core/djangoapps/schedules/exceptions.py @@ -0,0 +1,2 @@ +class CourseUpdateDoesNotExist(Exception): + pass diff --git a/openedx/core/djangoapps/schedules/management/commands/send_course_update.py b/openedx/core/djangoapps/schedules/management/commands/send_course_update.py new file mode 100644 index 0000000000..7846fc5ea5 --- /dev/null +++ b/openedx/core/djangoapps/schedules/management/commands/send_course_update.py @@ -0,0 +1,14 @@ +from openedx.core.djangoapps.schedules.management.commands import SendEmailBaseCommand +from openedx.core.djangoapps.schedules.resolvers import CourseUpdateResolver + + +class Command(SendEmailBaseCommand): + resolver_class = CourseUpdateResolver + + def __init__(self, *args, **kwargs): + super(Command, self).__init__(*args, **kwargs) + self.log_prefix = 'Upgrade Reminder' + + def send_emails(self, resolver, *args, **options): + for day_offset in xrange(-7, -77, -7): + resolver.send(day_offset, options.get('override_recipient_email')) diff --git a/openedx/core/djangoapps/schedules/migrations/0005_auto_20171010_1722.py b/openedx/core/djangoapps/schedules/migrations/0005_auto_20171010_1722.py new file mode 100644 index 0000000000..1f617874c9 --- /dev/null +++ b/openedx/core/djangoapps/schedules/migrations/0005_auto_20171010_1722.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('schedules', '0004_auto_20170922_1428'), + ] + + operations = [ + migrations.AddField( + model_name='scheduleconfig', + name='deliver_course_update', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='scheduleconfig', + name='enqueue_course_update', + field=models.BooleanField(default=False), + ), + ] diff --git a/openedx/core/djangoapps/schedules/models.py b/openedx/core/djangoapps/schedules/models.py index 558da756bb..f27aaed2c7 100644 --- a/openedx/core/djangoapps/schedules/models.py +++ b/openedx/core/djangoapps/schedules/models.py @@ -37,3 +37,5 @@ class ScheduleConfig(ConfigurationModel): deliver_recurring_nudge = models.BooleanField(default=False) enqueue_upgrade_reminder = models.BooleanField(default=False) deliver_upgrade_reminder = models.BooleanField(default=False) + enqueue_course_update = models.BooleanField(default=False) + deliver_course_update = models.BooleanField(default=False) diff --git a/openedx/core/djangoapps/schedules/resolvers.py b/openedx/core/djangoapps/schedules/resolvers.py index bc86e1c5d8..681b1da83e 100644 --- a/openedx/core/djangoapps/schedules/resolvers.py +++ b/openedx/core/djangoapps/schedules/resolvers.py @@ -8,8 +8,10 @@ from openedx.core.djangoapps.schedules.tasks import ( DEFAULT_NUM_BINS, RECURRING_NUDGE_NUM_BINS, UPGRADE_REMINDER_NUM_BINS, + COURSE_UPDATE_NUM_BINS, recurring_nudge_schedule_bin, - upgrade_reminder_schedule_bin + upgrade_reminder_schedule_bin, + course_update_schedule_bin, ) from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin from openedx.core.djangoapps.site_configuration.models import SiteConfiguration @@ -118,3 +120,17 @@ class UpgradeReminderResolver(BinnedSchedulesBaseResolver): def __init__(self, *args, **kwargs): super(UpgradeReminderResolver, self).__init__(*args, **kwargs) self.log_prefix = 'Upgrade Reminder' + + +class CourseUpdateResolver(BinnedSchedulesBaseResolver): + """ + Send a message to all users whose schedule started at ``self.current_date`` + ``day_offset`` and the + course has updates. + """ + async_send_task = course_update_schedule_bin + num_bins = COURSE_UPDATE_NUM_BINS + enqueue_config_var = 'enqueue_course_update' + + def __init__(self, *args, **kwargs): + super(CourseUpdateResolver, self).__init__(*args, **kwargs) + self.log_prefix = 'Course Update' diff --git a/openedx/core/djangoapps/schedules/tasks.py b/openedx/core/djangoapps/schedules/tasks.py index c9c7a7968b..1538c2b2d9 100644 --- a/openedx/core/djangoapps/schedules/tasks.py +++ b/openedx/core/djangoapps/schedules/tasks.py @@ -20,6 +20,8 @@ from edx_ace.utils.date import deserialize from opaque_keys.edx.keys import CourseKey from edxmako.shortcuts import marketing_link +from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG +from openedx.core.djangoapps.schedules.exceptions import CourseUpdateDoesNotExist from openedx.core.djangoapps.schedules.message_type import ScheduleMessageType from openedx.core.djangoapps.schedules.models import Schedule, ScheduleConfig from openedx.core.djangoapps.schedules.template_context import ( @@ -28,6 +30,8 @@ from openedx.core.djangoapps.schedules.template_context import ( encode_urls_in_dict, get_base_template_context ) +from request_cache.middleware import request_cached +from xmodule.modulestore.django import modulestore LOG = logging.getLogger(__name__) @@ -41,6 +45,7 @@ KNOWN_RETRY_ERRORS = ( # Errors we expect occasionally that could resolve on re DEFAULT_NUM_BINS = 24 RECURRING_NUDGE_NUM_BINS = DEFAULT_NUM_BINS UPGRADE_REMINDER_NUM_BINS = DEFAULT_NUM_BINS +COURSE_UPDATE_NUM_BINS = DEFAULT_NUM_BINS @task(bind=True, default_retry_delay=30, routing_key=ROUTING_KEY) @@ -309,8 +314,98 @@ def _upgrade_reminder_schedules_for_bin(site, target_day, bin_num, org_list, exc yield (user, first_schedule.enrollment.course.language, template_context) +class CourseUpdate(ScheduleMessageType): + pass + + +@task(ignore_result=True, routing_key=ROUTING_KEY) +def course_update_schedule_bin( + site_id, target_day_str, day_offset, bin_num, org_list, exclude_orgs=False, override_recipient_email=None, +): + target_day = deserialize(target_day_str) + msg_type = CourseUpdate() + + for (user, language, context) in _course_update_schedules_for_bin( + Site.objects.get(id=site_id), + target_day, + day_offset, + bin_num, + org_list, + exclude_orgs + ): + msg = msg_type.personalize( + Recipient( + user.username, + override_recipient_email or user.email, + ), + language, + context, + ) + _course_update_schedule_send.apply_async((site_id, str(msg)), retry=False) + + +@task(ignore_result=True, routing_key=ROUTING_KEY) +def _course_update_schedule_send(site_id, msg_str): + site = Site.objects.get(pk=site_id) + if not ScheduleConfig.current(site).deliver_course_update: + return + + msg = Message.from_string(msg_str) + ace.send(msg) + + +def _course_update_schedules_for_bin(site, target_day, day_offset, bin_num, org_list, exclude_orgs=False): + week_num = abs(day_offset) / 7 + beginning_of_day = target_day.replace(hour=0, minute=0, second=0) + schedules = get_schedules_with_target_date_by_bin_and_orgs( + schedule_date_field='start', + target_date=beginning_of_day, + bin_num=bin_num, + num_bins=COURSE_UPDATE_NUM_BINS, + org_list=org_list, + exclude_orgs=exclude_orgs, + order_by='enrollment__course', + ) + + LOG.debug('Course Update: Query = %r', schedules.query.sql_with_params()) + + for schedule in schedules: + enrollment = schedule.enrollment + try: + week_summary = get_course_week_summary(enrollment.course_id, week_num) + except CourseUpdateDoesNotExist: + continue + + user = enrollment.user + course_id_str = str(enrollment.course_id) + + template_context = get_base_template_context(site) + template_context.update({ + 'student_name': user.profile.name, + 'user_personal_address': user.profile.name if user.profile.name else user.username, + 'course_name': schedule.enrollment.course.display_name, + 'course_url': absolute_url(site, reverse('course_root', args=[str(schedule.enrollment.course_id)])), + 'week_num': week_num, + 'week_summary': week_summary, + + # This is used by the bulk email optout policy + 'course_ids': [course_id_str], + }) + + yield (user, schedule.enrollment.course.language, template_context) + + +@request_cached +def get_course_week_summary(course_id, week_num): + if COURSE_UPDATE_WAFFLE_FLAG.is_enabled(course_id): + course = modulestore().get_course(course_id) + return course.week_summary(week_num) + else: + raise CourseUpdateDoesNotExist() + + def get_schedules_with_target_date_by_bin_and_orgs(schedule_date_field, target_date, bin_num, num_bins=DEFAULT_NUM_BINS, - org_list=None, exclude_orgs=False): + org_list=None, exclude_orgs=False, order_by='enrollment__user__id'): """ Returns Schedules with the target_date, related to Users whose id matches the bin_num, and filtered by org_list. @@ -348,7 +443,7 @@ def get_schedules_with_target_date_by_bin_and_orgs(schedule_date_field, target_d enrollment__user__in=users, enrollment__is_active=True, **schedule_date_equals_target_date_filter - ).order_by('enrollment__user__id') + ).order_by(order_by) if org_list is not None: if exclude_orgs: diff --git a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/body.html b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/body.html new file mode 100644 index 0000000000..e04f056cc3 --- /dev/null +++ b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/body.html @@ -0,0 +1,51 @@ +{% extends 'schedules/edx_ace/common/base_body.html' %} +{% load i18n %} +{% load static %} + +{% block preview_text %} + {% blocktrans trimmed %} + Welcome to week {{ week_num }} of our {{ course_name }} course! + {% endblocktrans %} +{% endblock %} + +{% block content %} + + + + +
+

+ {% blocktrans trimmed %} + Welcome to week {{ week_num }} of {{ course_name }}! + {% endblocktrans %} +

+

+ {% blocktrans trimmed %} + Here is what you can look forward to learning this week: +

{{ week_summary }}

+ {% endblocktrans %} +

+ +

+ + + + {% trans "Resume your course now" %} + +

+
+{% endblock %} diff --git a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/body.txt b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/body.txt new file mode 100644 index 0000000000..3a3109ae7f --- /dev/null +++ b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/body.txt @@ -0,0 +1,9 @@ +{% load i18n %} + +{% blocktrans trimmed %} +Welcome to week {{ week_num }} of our {{ course_name }} course! + +Here is what you can look forward to learning this week: +{{ week_summary }} + +{% endblocktrans %} diff --git a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/from_name.txt b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/from_name.txt new file mode 100644 index 0000000000..08e86bddd0 --- /dev/null +++ b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/from_name.txt @@ -0,0 +1 @@ +{{ course_name }} diff --git a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/head.html b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/head.html new file mode 100644 index 0000000000..588357ec65 --- /dev/null +++ b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/head.html @@ -0,0 +1 @@ +{% extends 'schedules/edx_ace/common/base_head.html' %} diff --git a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/subject.txt b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/subject.txt new file mode 100644 index 0000000000..a288ce8a35 --- /dev/null +++ b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/courseupdate/email/subject.txt @@ -0,0 +1,3 @@ +{% load i18n %} + +{% blocktrans %}{{ course_name }} - Welcome to Week {{ week_num }} {% endblocktrans %} diff --git a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.txt b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.txt index 3bbf29c78e..9b0fb9b8bb 100644 --- a/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.txt +++ b/openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.txt @@ -1,14 +1,10 @@ {% load i18n %} {% blocktrans trimmed %} -Dear {{ user_personal_address }}, -{% endblocktrans %} +We hope you are enjoying learning with us so far in {{ course_name }}! A verified certificate +will allow you to highlight your new knowledge and skills. It's official, and easily shareable. -{% blocktrans trimmed %} - We hope you are enjoying learning with us so far in {{ course_name }}! A verified certificate - will allow you to highlight your new knowledge and skills. It's official, and easily shareable. - - Upgrade by {{ user_schedule_upgrade_deadline_time }}. +Upgrade by {{ user_schedule_upgrade_deadline_time }}. {% endblocktrans %} {% trans "Upgrade now at" %} <{{ course_url }}>