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 %} + + + + |
+