Commit 004ca480 by Tyler Hallada

Add tests for upgrade_reminder

Fix test_templates

By passing the date pre-formatted to ace.

Address PR comments

Fix linting

Add TODO comment about using LoggerAdapter

More lint fixes

Fix upgrade_reminder task from silently failing
parent 1bcd3a45
......@@ -15,6 +15,8 @@ from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
LOG = logging.getLogger(__name__)
# TODO: consider using a LoggerAdapter instead of this mixin:
# https://docs.python.org/2/library/logging.html#logging.LoggerAdapter
class PrefixedDebugLoggerMixin(object):
def __init__(self, *args, **kwargs):
self.log_prefix = self.__class__.__name__
......
......@@ -11,7 +11,7 @@ from openedx.core.djangoapps.schedules.management.commands import (
SendEmailBaseCommand,
BinnedSchedulesBaseResolver
)
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory, ScheduleFactory
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
......@@ -96,7 +96,6 @@ class TestBinnedSchedulesBaseResolver(CacheIsolationTestCase):
assert not exclude_orgs
assert org_list == expected_org_list
# factory_boy doesn't make sense at all
@ddt.unpack
@ddt.data(
(None, []),
......
......@@ -61,7 +61,7 @@ class TestSendRecurringNudge(CacheIsolationTestCase):
retry=False,
)
mock_schedule_bin.apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_time), -3, 23, [], True, None),
(self.site_config.site.id, serialize(test_time), -3, tasks.RECURRING_NUDGE_NUM_BINS - 1, [], True, None),
retry=False,
)
self.assertFalse(mock_ace.send.called)
......
import datetime
import itertools
from copy import deepcopy
from unittest import skipUnless
import attr
import ddt
import pytz
from django.conf import settings
from edx_ace.channel import ChannelType
from edx_ace.test_utils import StubPolicy, patch_channels, patch_policies
from edx_ace.utils.date import serialize
from mock import Mock, patch
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import CourseLocator
from openedx.core.djangoapps.schedules import tasks
from openedx.core.djangoapps.schedules.management.commands import send_upgrade_reminder as reminder
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory, ScheduleFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
from student.tests.factories import UserFactory
@ddt.ddt
@skip_unless_lms
@skipUnless('openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS,
"Can't test schedules if the app isn't installed")
class TestUpgradeReminder(CacheIsolationTestCase):
# pylint: disable=protected-access
def setUp(self):
super(TestUpgradeReminder, self).setUp()
ScheduleFactory.create(upgrade_deadline=datetime.datetime(2017, 8, 1, 15, 44, 30, tzinfo=pytz.UTC))
ScheduleFactory.create(upgrade_deadline=datetime.datetime(2017, 8, 1, 17, 34, 30, tzinfo=pytz.UTC))
ScheduleFactory.create(upgrade_deadline=datetime.datetime(2017, 8, 2, 15, 34, 30, tzinfo=pytz.UTC))
site = SiteFactory.create()
self.site_config = SiteConfigurationFactory.create(site=site)
ScheduleConfigFactory.create(site=self.site_config.site)
@patch.object(reminder, 'UpgradeReminderResolver')
def test_handle(self, mock_resolver):
test_time = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
reminder.Command().handle(date='2017-08-01', site_domain_name=self.site_config.site.domain)
mock_resolver.assert_called_with(self.site_config.site, test_time)
mock_resolver().send.assert_any_call(2, None)
@patch.object(tasks, 'ace')
@patch.object(reminder, 'upgrade_reminder_schedule_bin')
def test_resolver_send(self, mock_schedule_bin, mock_ace):
current_time = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
test_time = current_time + datetime.timedelta(days=2)
ScheduleFactory.create(upgrade_deadline=datetime.datetime(2017, 8, 3, 15, 34, 30, tzinfo=pytz.UTC))
reminder.UpgradeReminderResolver(self.site_config.site, current_time).send(2)
self.assertFalse(mock_schedule_bin.called)
mock_schedule_bin.apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_time), 2, 0, [], True, None),
retry=False,
)
mock_schedule_bin.apply_async.assert_any_call(
(self.site_config.site.id, serialize(test_time), 2, tasks.UPGRADE_REMINDER_NUM_BINS - 1, [], True, None),
retry=False,
)
self.assertFalse(mock_ace.send.called)
@ddt.data(1, 10, 100)
@patch.object(tasks, 'ace')
@patch.object(tasks, '_upgrade_reminder_schedule_send')
def test_schedule_bin(self, schedule_count, mock_schedule_send, mock_ace):
schedules = [
ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 18, 44, 30, tzinfo=pytz.UTC),
enrollment__user=UserFactory.create(),
enrollment__course__id=CourseLocator('edX', 'toy', 'Bin')
) for _ in range(schedule_count)
]
test_time = datetime.datetime(2017, 8, 3, 18, tzinfo=pytz.UTC)
test_time_str = serialize(test_time)
with self.assertNumQueries(25):
for b in range(tasks.UPGRADE_REMINDER_NUM_BINS):
tasks.upgrade_reminder_schedule_bin(
self.site_config.site.id, target_day_str=test_time_str, day_offset=2, bin_num=b,
org_list=[schedules[0].enrollment.course.org],
)
self.assertEqual(mock_schedule_send.apply_async.call_count, schedule_count)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks, '_upgrade_reminder_schedule_send')
def test_no_course_overview(self, mock_schedule_send):
schedule = ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 20, 34, 30, tzinfo=pytz.UTC),
)
schedule.enrollment.course_id = CourseKey.from_string('edX/toy/Not_2012_Fall')
schedule.enrollment.save()
test_time = datetime.datetime(2017, 8, 3, 20, tzinfo=pytz.UTC)
test_time_str = serialize(test_time)
with self.assertNumQueries(25):
for b in range(tasks.UPGRADE_REMINDER_NUM_BINS):
tasks.upgrade_reminder_schedule_bin(
self.site_config.site.id, target_day_str=test_time_str, day_offset=2, bin_num=b,
org_list=[schedule.enrollment.course.org],
)
# There is no database constraint that enforces that enrollment.course_id points
# to a valid CourseOverview object. However, in that case, schedules isn't going
# to attempt to address it, and will instead simply skip those users.
# This happens 'transparently' because django generates an inner-join between
# enrollment and course_overview, and thus will skip any rows where course_overview
# is null.
self.assertEqual(mock_schedule_send.apply_async.call_count, 0)
@patch.object(tasks, 'ace')
def test_delivery_disabled(self, mock_ace):
ScheduleConfigFactory.create(site=self.site_config.site, deliver_upgrade_reminder=False)
mock_msg = Mock()
tasks._upgrade_reminder_schedule_send(self.site_config.site.id, mock_msg)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks, 'ace')
@patch.object(reminder, 'upgrade_reminder_schedule_bin')
def test_enqueue_disabled(self, mock_schedule_bin, mock_ace):
ScheduleConfigFactory.create(site=self.site_config.site, enqueue_upgrade_reminder=False)
current_time = datetime.datetime(2017, 8, 1, tzinfo=pytz.UTC)
reminder.UpgradeReminderResolver(self.site_config.site, current_time).send(3)
self.assertFalse(mock_schedule_bin.called)
self.assertFalse(mock_schedule_bin.apply_async.called)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks, 'ace')
@patch.object(tasks, '_upgrade_reminder_schedule_send')
@ddt.data(
((['filtered_org'], False, 1)),
((['filtered_org'], True, 2))
)
@ddt.unpack
def test_site_config(self, org_list, exclude_orgs, expected_message_count, mock_schedule_send, mock_ace):
filtered_org = 'filtered_org'
unfiltered_org = 'unfiltered_org'
site1 = SiteFactory.create(domain='foo1.bar', name='foo1.bar')
limited_config = SiteConfigurationFactory.create(values={'course_org_filter': [filtered_org]}, site=site1)
site2 = SiteFactory.create(domain='foo2.bar', name='foo2.bar')
unlimited_config = SiteConfigurationFactory.create(values={'course_org_filter': []}, site=site2)
for config in (limited_config, unlimited_config):
ScheduleConfigFactory.create(site=config.site)
user1 = UserFactory.create(id=tasks.UPGRADE_REMINDER_NUM_BINS)
user2 = UserFactory.create(id=tasks.UPGRADE_REMINDER_NUM_BINS * 2)
ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 17, 44, 30, tzinfo=pytz.UTC),
enrollment__course__org=filtered_org,
enrollment__user=user1,
)
ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 17, 44, 30, tzinfo=pytz.UTC),
enrollment__course__org=unfiltered_org,
enrollment__user=user1,
)
ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 17, 44, 30, tzinfo=pytz.UTC),
enrollment__course__org=unfiltered_org,
enrollment__user=user2,
)
test_time = datetime.datetime(2017, 8, 3, 17, tzinfo=pytz.UTC)
test_time_str = serialize(test_time)
with self.assertNumQueries(2):
tasks.upgrade_reminder_schedule_bin(
limited_config.site.id, target_day_str=test_time_str, day_offset=2, bin_num=0,
org_list=org_list, exclude_orgs=exclude_orgs,
)
self.assertEqual(mock_schedule_send.apply_async.call_count, expected_message_count)
self.assertFalse(mock_ace.send.called)
@patch.object(tasks, 'ace')
@patch.object(tasks, '_upgrade_reminder_schedule_send')
def test_multiple_enrollments(self, mock_schedule_send, mock_ace):
user = UserFactory.create()
schedules = [
ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC),
enrollment__user=user,
enrollment__course__id=CourseLocator('edX', 'toy', 'Course{}'.format(course_num))
)
for course_num in (1, 2, 3)
]
test_time = datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC)
test_time_str = serialize(test_time)
with self.assertNumQueries(2):
tasks.upgrade_reminder_schedule_bin(
self.site_config.site.id, target_day_str=test_time_str, day_offset=2,
bin_num=user.id % tasks.UPGRADE_REMINDER_NUM_BINS,
org_list=[schedules[0].enrollment.course.org],
)
self.assertEqual(mock_schedule_send.apply_async.call_count, 3)
self.assertFalse(mock_ace.send.called)
@ddt.data(*itertools.product((1, 10, 100), (2, 10)))
@ddt.unpack
def test_templates(self, message_count, day):
user = UserFactory.create()
schedules = [
ScheduleFactory.create(
upgrade_deadline=datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC),
enrollment__user=user,
enrollment__course__id=CourseLocator('edX', 'toy', 'Course{}'.format(course_num))
)
for course_num in range(message_count)
]
test_time = datetime.datetime(2017, 8, 3, 19, tzinfo=pytz.UTC)
test_time_str = serialize(test_time)
patch_policies(self, [StubPolicy([ChannelType.PUSH])])
mock_channel = Mock(
name='test_channel',
channel_type=ChannelType.EMAIL
)
patch_channels(self, [mock_channel])
sent_messages = []
templates_override = deepcopy(settings.TEMPLATES)
templates_override[0]['OPTIONS']['string_if_invalid'] = "TEMPLATE WARNING - MISSING VARIABLE [%s]"
with self.settings(TEMPLATES=templates_override):
with patch.object(tasks, '_upgrade_reminder_schedule_send') as mock_schedule_send:
mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args)
with self.assertNumQueries(2):
tasks.upgrade_reminder_schedule_bin(
self.site_config.site.id, target_day_str=test_time_str, day_offset=day,
bin_num=user.id % tasks.UPGRADE_REMINDER_NUM_BINS,
org_list=[schedules[0].enrollment.course.org],
)
self.assertEqual(len(sent_messages), message_count)
for args in sent_messages:
tasks._upgrade_reminder_schedule_send(*args)
self.assertEqual(mock_channel.deliver.call_count, message_count)
for (_name, (_msg, email), _kwargs) in mock_channel.deliver.mock_calls:
for template in attr.astuple(email):
self.assertNotIn("TEMPLATE WARNING", template)
......@@ -8,8 +8,9 @@ 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 F, Min, Prefetch
from django.db.models import F, Min
from django.db.utils import DatabaseError
from django.utils.formats import dateformat, get_format
from edx_ace import ace
from edx_ace.message import Message
......@@ -17,7 +18,6 @@ from edx_ace.recipient import Recipient
from edx_ace.utils.date import deserialize
from opaque_keys.edx.keys import CourseKey
from course_modes.models import CourseMode
from edxmako.shortcuts import marketing_link
from openedx.core.djangoapps.schedules.message_type import ScheduleMessageType
from openedx.core.djangoapps.schedules.models import Schedule, ScheduleConfig
......@@ -27,7 +27,6 @@ from openedx.core.djangoapps.schedules.template_context import (
encode_urls_in_dict,
get_base_template_context
)
from openedx.core.djangoapps.user_api.models import UserPreference
LOG = logging.getLogger(__name__)
......@@ -232,9 +231,7 @@ def _recurring_nudge_schedules_for_bin(target_day, bin_num, org_list, exclude_or
class UpgradeReminder(ScheduleMessageType):
def __init__(self, day, *args, **kwargs):
super(UpgradeReminder, self).__init__(*args, **kwargs)
self.name = "upgradereminder".format(day)
pass
@task(ignore_result=True, routing_key=ROUTING_KEY)
......@@ -242,7 +239,7 @@ def upgrade_reminder_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 = UpgradeReminder(abs(day_offset))
msg_type = UpgradeReminder()
for (user, language, context) in _upgrade_reminder_schedules_for_bin(target_day, bin_num, org_list, exclude_orgs):
msg = msg_type.personalize(
......@@ -267,30 +264,35 @@ def _upgrade_reminder_schedule_send(site_id, msg_str):
def _upgrade_reminder_schedules_for_bin(target_day, bin_num, org_list, exclude_orgs=False):
schedules = Schedule.objects.select_related(
'enrollment__user__profile',
'enrollment__course',
).prefetch_related(
Prefetch(
'enrollment__course__modes',
queryset=CourseMode.objects.filter(mode_slug=CourseMode.VERIFIED),
to_attr='verified_modes'
),
Prefetch(
'enrollment__user__preferences',
queryset=UserPreference.objects.filter(key='time_zone'),
to_attr='tzprefs'
),
beginning_of_day = target_day.replace(hour=0, minute=0, second=0)
users = User.objects.filter(
courseenrollment__schedule__upgrade_deadline__gte=beginning_of_day,
courseenrollment__schedule__upgrade_deadline__lt=beginning_of_day + datetime.timedelta(days=1),
courseenrollment__is_active=True,
).annotate(
id_mod=F('enrollment__user__id') % UPGRADE_REMINDER_NUM_BINS
first_schedule=Min('courseenrollment__schedule__upgrade_deadline')
).annotate(
id_mod=F('id') % UPGRADE_REMINDER_NUM_BINS
).filter(
id_mod=bin_num
).filter(
upgrade_deadline__year=target_day.year,
upgrade_deadline__month=target_day.month,
upgrade_deadline__day=target_day.day,
)
schedules = Schedule.objects.select_related(
'enrollment__user__profile',
'enrollment__course',
).filter(
enrollment__user__in=users,
upgrade_deadline__gte=beginning_of_day,
upgrade_deadline__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")
......@@ -299,7 +301,6 @@ def _upgrade_reminder_schedules_for_bin(target_day, bin_num, org_list, exclude_o
user = enrollment.user
course_id_str = str(enrollment.course_id)
course = enrollment.course
# TODO: group by schedule and user like recurring nudge
course_id_strs = [course_id_str]
......@@ -309,7 +310,14 @@ def _upgrade_reminder_schedules_for_bin(target_day, bin_num, org_list, exclude_o
template_context.update({
'student_name': user.profile.name,
'user_personal_address': user.profile.name if user.profile.name else user.username,
'user_schedule_upgrade_deadline_time': schedule.upgrade_deadline,
'user_schedule_upgrade_deadline_time': dateformat.format(
schedule.upgrade_deadline,
get_format(
'DATE_FORMAT',
lang=first_schedule.enrollment.course.language,
use_l10n=True
)
),
'course_name': first_schedule.enrollment.course.display_name,
'course_url': absolute_url(reverse('course_root', args=[str(first_schedule.enrollment.course_id)])),
......@@ -318,4 +326,4 @@ def _upgrade_reminder_schedules_for_bin(target_day, bin_num, org_list, exclude_o
'course_ids': course_id_strs,
})
yield (user, course.language, template_context)
yield (user, first_schedule.enrollment.course.language, template_context)
......@@ -2,15 +2,15 @@
{% load i18n %}
{% block preview_text %}
{% if courses|length > 1 %}
{% if course_ids|length > 1 %}
{% blocktrans trimmed %}
We hope you are enjoying {{ course_name }}, and other courses on edX.org.
Upgrade by {{ user_schedule_upgrade_deadline_time|date:"l, F dS, Y" }} to get a shareable certificate!
Upgrade by {{ user_schedule_upgrade_deadline_time }} to get a shareable certificate!
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
We hope you are enjoying {{ course_name }}.
Upgrade by {{ user_schedule_upgrade_deadline_time|date:"l, F dS, Y" }} to get a shareable certificate!
Upgrade by {{ user_schedule_upgrade_deadline_time }} to get a shareable certificate!
{% endblocktrans %}
{% endif %}
{% endblock %}
......@@ -22,16 +22,15 @@
<h1>{% trans "Upgrade now" %}</h1>
<p>
{% if courses|length > 1 %}
{% if course_ids|length > 1 %}
{% blocktrans trimmed %}
We hope you are enjoying <strong>{{ course_name }}</strong>, and other courses on edX.org.
{{ user_schedule_upgrade_deadline_time|date }}
Upgrade by {{ user_schedule_upgrade_deadline_time|date:"l, F dS, Y" }} to get a shareable certificate!
Upgrade by {{ user_schedule_upgrade_deadline_time }} to get a shareable certificate!
{% endblocktrans %}
{% else %}
{% blocktrans trimmed %}
We hope you are enjoying <strong>{{ course_name }}</strong>.
Upgrade by {{ user_schedule_upgrade_deadline_time|date:"l, F dS, Y" }} to get a shareable certificate!
Upgrade by {{ user_schedule_upgrade_deadline_time }} to get a shareable certificate!
{% endblocktrans %}
{% endif %}
</p>
......@@ -39,7 +38,7 @@
<p>
<!-- email client support for style sheets is pretty spotty, so we have to inline all of these styles -->
<a
{% if courses|length > 1 %}
{% if course_ids|length > 1 %}
href="{{ dashboard_url }}"
{% else %}
href="{{ course_url }}"
......
......@@ -4,17 +4,17 @@
Dear {{ user_personal_address }},
{% endblocktrans %}
{% if courses|length > 1 %}
{% if course_ids|length > 1 %}
{% blocktrans trimmed %}
We hope you are enjoying {{ course_name }}, and other courses on edX.org.
Upgrade by {{ user_schedule_upgrade_deadline_time|date:"l, F dS, Y" }} to get a shareable certificate!
Upgrade by {{ user_schedule_upgrade_deadline_time }} to get a shareable certificate!
{% endblocktrans %}
{% trans "Upgrade now at" %} <{{ dashboard_url }}>
{% else %}
{% blocktrans trimmed %}
We hope you are enjoying {{ course_name }}.
Upgrade by {{ user_schedule_upgrade_deadline_time|date:"l, F dS, Y" }} to get a shareable certificate!
Upgrade by {{ user_schedule_upgrade_deadline_time }} to get a shareable certificate!
{% endblocktrans %}
{% trans "Upgrade now at" %} <{{ course_url }}>
......
{% if courses|length > 1 %}
{% if course_ids|length > 1 %}
{{ platform_name }}
{% else %}
{{ course_name }}
......
{% load i18n %}
{% if courses|length > 1 %}
{% if course_ids|length > 1 %}
{% blocktrans %}Only two days left to upgrade on {{ platform_name }}!{% endblocktrans %}
{% else %}
{% blocktrans %}Only two days left to upgrade in {{course_name}} !{% endblocktrans %}
......
......@@ -23,3 +23,5 @@ class ScheduleConfigFactory(factory.DjangoModelFactory):
create_schedules = True
enqueue_recurring_nudge = True
deliver_recurring_nudge = True
enqueue_upgrade_reminder = True
deliver_upgrade_reminder = True
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