Commit 43c7a517 by Nimisha Asthagiri Committed by GitHub

Merge pull request #16219 from edx/ret/dynamic-pacing-email

Course Update emails (initial)
parents f0ee9ee8 741917e9
......@@ -54,9 +54,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
......
......@@ -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',
)
......@@ -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')
class CourseUpdateDoesNotExist(Exception):
pass
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'))
# -*- 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),
),
]
......@@ -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)
......@@ -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'
......@@ -23,6 +23,8 @@ from opaque_keys.edx.keys import CourseKey
from courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid
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 (
......@@ -31,6 +33,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__)
......@@ -44,6 +48,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)
......@@ -222,8 +227,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.
......@@ -263,7 +358,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:
......
{% 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 %}
<table width="100%" align="left" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<p>
{% blocktrans trimmed %}
Welcome to week {{ week_num }} of <strong>{{ course_name }}</strong>!
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
Here is what you can look forward to learning this week:
<p>{{ week_summary }}</p>
{% endblocktrans %}
</p>
<p>
<!-- email client support for style sheets is pretty spotty, so we have to inline all of these styles -->
<a
href="{{ course_url }}"
style="
color: #ffffff;
text-decoration: none;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
background-color: #005686;
border-top: 10px solid #005686;
border-bottom: 10px solid #005686;
border-right: 16px solid #005686;
border-left: 16px solid #005686;
display: inline-block;
">
<!-- old email clients require the use of the font tag :( -->
<font color="#ffffff"><b>{% trans "Resume your course now" %}</b></font>
</a>
</p>
</td>
</tr>
</table>
{% endblock %}
{% 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 %}
{% load i18n %}
{% blocktrans %}{{ course_name }} - Welcome to Week {{ week_num }} {% endblocktrans %}
{% load i18n %}
{% blocktrans trimmed %}
Dear {{ user_personal_address }},
{% endblocktrans %}
{% 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.
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" %} <{{ upsell_link }}>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment