Files
edx-platform/openedx/core/djangoapps/schedules/tasks.py
2017-09-21 13:41:51 -04:00

176 lines
6.6 KiB
Python

import datetime
from itertools import groupby
import logging
from urlparse import urlparse
from celery.task import task
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db.models import Min
from django.db.utils import DatabaseError
from django.utils.http import urlquote
from edx_ace import ace
from edx_ace.message import Message
from edx_ace.recipient import Recipient
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.message_type import ScheduleMessageType
from openedx.core.djangoapps.schedules.models import Schedule, ScheduleConfig
LOG = logging.getLogger(__name__)
ROUTING_KEY = getattr(settings, 'ACE_ROUTING_KEY', None)
KNOWN_RETRY_ERRORS = ( # Errors we expect occasionally that could resolve on retry
DatabaseError,
ValidationError,
)
@task(bind=True, default_retry_delay=30, routing_key=ROUTING_KEY)
def update_course_schedules(self, **kwargs):
course_key = CourseKey.from_string(kwargs['course_id'])
new_start_date = deserialize(kwargs['new_start_date_str'])
new_upgrade_deadline = deserialize(kwargs['new_upgrade_deadline_str'])
try:
Schedule.objects.filter(enrollment__course_id=course_key).update(
start=new_start_date,
upgrade_deadline=new_upgrade_deadline
)
except Exception as exc: # pylint: disable=broad-except
if not isinstance(exc, KNOWN_RETRY_ERRORS):
LOG.exception("Unexpected failure: task id: %s, kwargs=%s".format(self.request.id, kwargs))
raise self.retry(kwargs=kwargs, exc=exc)
class RecurringNudge(ScheduleMessageType):
def __init__(self, day, *args, **kwargs):
super(RecurringNudge, self).__init__(*args, **kwargs)
self.name = "recurringnudge_day{}".format(day)
@task(ignore_result=True, routing_key=ROUTING_KEY)
def recurring_nudge_schedule_hour(
site_id, day, target_hour_str, org_list, exclude_orgs=False, override_recipient_email=None,
):
target_hour = deserialize(target_hour_str)
msg_type = RecurringNudge(day)
for (user, language, context) in _recurring_nudge_schedules_for_hour(target_hour, org_list, exclude_orgs):
msg = msg_type.personalize(
Recipient(
user.username,
override_recipient_email or user.email,
),
language,
context,
)
_recurring_nudge_schedule_send.apply_async((site_id, str(msg)), retry=False)
@task(ignore_result=True, routing_key=ROUTING_KEY)
def _recurring_nudge_schedule_send(site_id, msg_str):
site = Site.objects.get(pk=site_id)
if not ScheduleConfig.current(site).deliver_recurring_nudge:
LOG.debug('Recurring Nudge: Message delivery disabled for site %s', site.domain)
return
msg = Message.from_string(msg_str)
LOG.debug('Recurring Nudge: Sending message = %s', msg_str)
ace.send(msg)
def _recurring_nudge_schedules_for_hour(target_hour, org_list, exclude_orgs=False):
beginning_of_day = target_hour.replace(hour=0, minute=0, second=0)
users = User.objects.filter(
courseenrollment__schedule__start__gte=beginning_of_day,
courseenrollment__schedule__start__lt=beginning_of_day + datetime.timedelta(days=1),
courseenrollment__is_active=True,
).annotate(
first_schedule=Min('courseenrollment__schedule__start')
).filter(
first_schedule__gte=target_hour,
first_schedule__lt=target_hour + datetime.timedelta(minutes=60)
)
schedules = Schedule.objects.select_related(
'enrollment__user__profile',
'enrollment__course',
).filter(
enrollment__user__in=users,
start__gte=beginning_of_day,
start__lt=beginning_of_day + datetime.timedelta(days=1),
enrollment__is_active=True,
).order_by('enrollment__user__id')
if org_list is not None:
if exclude_orgs:
schedules = schedules.exclude(enrollment__course__org__in=org_list)
else:
schedules = schedules.filter(enrollment__course__org__in=org_list)
if "read_replica" in settings.DATABASES:
schedules = schedules.using("read_replica")
LOG.debug('Scheduled Nudge: Query = %r', schedules.query.sql_with_params())
dashboard_relative_url = reverse('dashboard')
for (user, user_schedules) in groupby(schedules, lambda s: s.enrollment.user):
user_schedules = list(user_schedules)
course_id_strs = [str(schedule.enrollment.course_id) for schedule in user_schedules]
def absolute_url(relative_path):
return u'{}{}'.format(settings.LMS_ROOT_URL, urlquote(relative_path))
first_schedule = user_schedules[0]
template_context = {
'student_name': user.profile.name,
'course_name': first_schedule.enrollment.course.display_name,
'course_url': absolute_url(reverse('course_root', args=[str(first_schedule.enrollment.course_id)])),
# This is used by the bulk email optout policy
'course_ids': course_id_strs,
# Platform information
'homepage_url': encode_url(marketing_link('ROOT')),
'dashboard_url': absolute_url(dashboard_relative_url),
'template_revision': settings.EDX_PLATFORM_REVISION,
'platform_name': settings.PLATFORM_NAME,
'contact_mailing_address': settings.CONTACT_MAILING_ADDRESS,
'social_media_urls': encode_urls_in_dict(getattr(settings, 'SOCIAL_MEDIA_FOOTER_URLS', {})),
'mobile_store_urls': encode_urls_in_dict(getattr(settings, 'MOBILE_STORE_URLS', {})),
}
yield (user, first_schedule.enrollment.course.language, template_context)
def encode_url(url):
# Sailthru has a bug where URLs that contain "+" characters in their path components are misinterpreted
# when GA instrumentation is enabled. We need to percent-encode the path segments of all URLs that are
# injected into our templates to work around this issue.
parsed_url = urlparse(url)
modified_url = parsed_url._replace(path=urlquote(parsed_url.path))
return modified_url.geturl()
def absolute_url(relative_path):
root = settings.LMS_ROOT_URL.rstrip('/')
relative_path = relative_path.lstrip('/')
return encode_url(u'{root}/{path}'.format(root=root, path=relative_path))
def encode_urls_in_dict(mapping):
urls = {}
for key, value in mapping.iteritems():
urls[key] = encode_url(value)
return urls