Unverified Commit d4af6ec0 by Gabe Mulley Committed by GitHub

Merge pull request #16294 from edx/thallada/ret-schedule-experience

Support multiple dynamic pacing experiences
parents 6614ca82 7fd643fa
...@@ -1686,9 +1686,13 @@ class CourseEnrollment(models.Model): ...@@ -1686,9 +1686,13 @@ class CourseEnrollment(models.Model):
""" """
if not self._course_overview: if not self._course_overview:
try: try:
self._course_overview = CourseOverview.get_from_id(self.course_id) self._course_overview = self.course
except (CourseOverview.DoesNotExist, IOError): except CourseOverview.DoesNotExist:
self._course_overview = None log.info('Course Overviews: unable to find course overview for enrollment, loading from modulestore.')
try:
self._course_overview = CourseOverview.get_from_id(self.course_id)
except (CourseOverview.DoesNotExist, IOError):
self._course_overview = None
return self._course_overview return self._course_overview
@cached_property @cached_property
...@@ -1717,7 +1721,14 @@ class CourseEnrollment(models.Model): ...@@ -1717,7 +1721,14 @@ class CourseEnrollment(models.Model):
# When course modes expire they aren't found any more and None would be returned. # When course modes expire they aren't found any more and None would be returned.
# Replicate that behavior here by returning None if the personalized deadline is in the past. # Replicate that behavior here by returning None if the personalized deadline is in the past.
if datetime.now(UTC) >= self.dynamic_upgrade_deadline: if datetime.now(UTC) >= self.dynamic_upgrade_deadline:
log.debug('Schedules: Returning None since dynamic upgrade deadline has already passed.')
return None return None
if self.verified_mode is None:
log.debug('Schedules: Returning None for dynamic upgrade deadline since the course does not have a '
'verified mode.')
return None
return self.dynamic_upgrade_deadline return self.dynamic_upgrade_deadline
return self.course_upgrade_deadline return self.course_upgrade_deadline
...@@ -1733,12 +1744,7 @@ class CourseEnrollment(models.Model): ...@@ -1733,12 +1744,7 @@ class CourseEnrollment(models.Model):
Returns: Returns:
datetime|None datetime|None
""" """
try: if not self.course_overview.self_paced:
course_overview = self.course
except CourseOverview.DoesNotExist:
course_overview = self.course_overview
if not course_overview.self_paced:
return None return None
if not DynamicUpgradeDeadlineConfiguration.is_enabled(): if not DynamicUpgradeDeadlineConfiguration.is_enabled():
......
...@@ -12,6 +12,8 @@ from opaque_keys.edx.keys import CourseKey ...@@ -12,6 +12,8 @@ from opaque_keys.edx.keys import CourseKey
from pytz import UTC from pytz import UTC
from course_modes.models import CourseMode from course_modes.models import CourseMode
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from student.models import ( from student.models import (
CourseAccessRole, CourseAccessRole,
CourseEnrollment, CourseEnrollment,
...@@ -126,9 +128,33 @@ class CourseEnrollmentFactory(DjangoModelFactory): ...@@ -126,9 +128,33 @@ class CourseEnrollmentFactory(DjangoModelFactory):
model = CourseEnrollment model = CourseEnrollment
user = factory.SubFactory(UserFactory) user = factory.SubFactory(UserFactory)
course = factory.SubFactory(
'openedx.core.djangoapps.content.course_overviews.tests.factories.CourseOverviewFactory', @classmethod
) def _create(cls, model_class, *args, **kwargs):
manager = cls._get_manager(model_class)
course_kwargs = {}
for key in kwargs.keys():
if key.startswith('course__'):
course_kwargs[key.split('__')[1]] = kwargs.pop(key)
if 'course' not in kwargs:
course_id = kwargs.get('course_id')
course_overview = None
if course_id is not None:
if isinstance(course_id, basestring):
course_id = CourseKey.from_string(course_id)
course_kwargs.setdefault('id', course_id)
try:
course_overview = CourseOverview.get_from_id(course_id)
except CourseOverview.DoesNotExist:
pass
if course_overview is None:
course_overview = CourseOverviewFactory(**course_kwargs)
kwargs['course'] = course_overview
return manager.create(*args, **kwargs)
class CourseAccessRoleFactory(DjangoModelFactory): class CourseAccessRoleFactory(DjangoModelFactory):
......
...@@ -111,6 +111,8 @@ class CertificateDashboardMessageDisplayTest(CertificateDisplayTestBase): ...@@ -111,6 +111,8 @@ class CertificateDashboardMessageDisplayTest(CertificateDisplayTestBase):
Tests the certificates messages for a course in the dashboard. Tests the certificates messages for a course in the dashboard.
""" """
ENABLED_SIGNALS = ['course_published']
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(CertificateDashboardMessageDisplayTest, cls).setUpClass() super(CertificateDashboardMessageDisplayTest, cls).setUpClass()
......
...@@ -12,8 +12,9 @@ from django.conf import settings ...@@ -12,8 +12,9 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.client import Client, RequestFactory from django.test.client import Client, RequestFactory
from django.test.utils import override_settings from django.test.utils import override_settings
from util.date_utils import strftime_localized from util.date_utils import strftime_localized
from mock import Mock, patch from mock import patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from certificates.api import get_certificate_url from certificates.api import get_certificate_url
...@@ -73,6 +74,9 @@ class CommonCertificatesTestCase(ModuleStoreTestCase): ...@@ -73,6 +74,9 @@ class CommonCertificatesTestCase(ModuleStoreTestCase):
""" """
Common setUp and utility methods for Certificate tests Common setUp and utility methods for Certificate tests
""" """
ENABLED_SIGNALS = ['course_published']
def setUp(self): def setUp(self):
super(CommonCertificatesTestCase, self).setUp() super(CommonCertificatesTestCase, self).setUp()
self.client = Client() self.client = Client()
......
...@@ -405,16 +405,19 @@ def verified_upgrade_deadline_link(user, course=None, course_id=None): ...@@ -405,16 +405,19 @@ def verified_upgrade_deadline_link(user, course=None, course_id=None):
ecommerce_service = EcommerceService() ecommerce_service = EcommerceService()
if ecommerce_service.is_enabled(user): if ecommerce_service.is_enabled(user):
if course is not None and isinstance(course, CourseOverview): course_mode = CourseMode.verified_mode_for_course(course_id)
course_mode = course.modes.get(mode_slug=CourseMode.VERIFIED) if course_mode is not None:
return ecommerce_service.get_checkout_page_url(course_mode.sku)
else: else:
course_mode = CourseMode.objects.get( raise CourseModeNotFoundException('Cannot generate a verified upgrade link without a valid verified mode'
course_id=course_id, mode_slug=CourseMode.VERIFIED ' for course {}'.format(unicode(course_id)))
)
return ecommerce_service.get_checkout_page_url(course_mode.sku)
return reverse('verify_student_upgrade_and_verify', args=(course_id,)) return reverse('verify_student_upgrade_and_verify', args=(course_id,))
class CourseModeNotFoundException(Exception):
pass
def verified_upgrade_link_is_valid(enrollment=None): def verified_upgrade_link_is_valid(enrollment=None):
""" """
Return whether this enrollment can be upgraded. Return whether this enrollment can be upgraded.
......
...@@ -29,6 +29,7 @@ class TestViewAuth(EnterpriseTestConsentRequired, ModuleStoreTestCase, LoginEnro ...@@ -29,6 +29,7 @@ class TestViewAuth(EnterpriseTestConsentRequired, ModuleStoreTestCase, LoginEnro
""" """
ACCOUNT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')] ACCOUNT_INFO = [('view@test.com', 'foo'), ('view2@test.com', 'foo')]
ENABLED_SIGNALS = ['course_published']
@staticmethod @staticmethod
def _reverse_urls(names, course): def _reverse_urls(names, course):
......
...@@ -807,7 +807,12 @@ class ViewsTestCase(ModuleStoreTestCase): ...@@ -807,7 +807,12 @@ class ViewsTestCase(ModuleStoreTestCase):
CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, course_id=course) CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, course_id=course)
# Enroll user in the course # Enroll user in the course
enrollment = CourseEnrollmentFactory(course_id=course, user=self.user, mode=CourseMode.AUDIT) # Don't use the CourseEnrollmentFactory since it ensures a CourseOverview is available
enrollment = CourseEnrollment.objects.create(
course_id=course,
user=self.user,
mode=CourseMode.AUDIT,
)
self.assertEqual(enrollment.course_overview, None) self.assertEqual(enrollment.course_overview, None)
......
...@@ -403,15 +403,15 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase): ...@@ -403,15 +403,15 @@ class SingleThreadQueryCountTestCase(ForumsEnableMixin, ModuleStoreTestCase):
# course is outside the context manager that is verifying the number of queries, # course is outside the context manager that is verifying the number of queries,
# and with split mongo, that method ends up querying disabled_xblocks (which is then # and with split mongo, that method ends up querying disabled_xblocks (which is then
# cached and hence not queried as part of call_single_thread). # cached and hence not queried as part of call_single_thread).
(ModuleStoreEnum.Type.mongo, False, 1, 6, 4, 17, 4), (ModuleStoreEnum.Type.mongo, False, 1, 6, 4, 16, 4),
(ModuleStoreEnum.Type.mongo, False, 50, 6, 4, 17, 4), (ModuleStoreEnum.Type.mongo, False, 50, 6, 4, 16, 4),
# split mongo: 3 queries, regardless of thread response size. # split mongo: 3 queries, regardless of thread response size.
(ModuleStoreEnum.Type.split, False, 1, 3, 3, 16, 4), (ModuleStoreEnum.Type.split, False, 1, 3, 3, 16, 4),
(ModuleStoreEnum.Type.split, False, 50, 3, 3, 16, 4), (ModuleStoreEnum.Type.split, False, 50, 3, 3, 16, 4),
# Enabling Enterprise integration should have no effect on the number of mongo queries made. # Enabling Enterprise integration should have no effect on the number of mongo queries made.
(ModuleStoreEnum.Type.mongo, True, 1, 6, 4, 17, 4), (ModuleStoreEnum.Type.mongo, True, 1, 6, 4, 16, 4),
(ModuleStoreEnum.Type.mongo, True, 50, 6, 4, 17, 4), (ModuleStoreEnum.Type.mongo, True, 50, 6, 4, 16, 4),
# split mongo: 3 queries, regardless of thread response size. # split mongo: 3 queries, regardless of thread response size.
(ModuleStoreEnum.Type.split, True, 1, 3, 3, 16, 4), (ModuleStoreEnum.Type.split, True, 1, 3, 3, 16, 4),
(ModuleStoreEnum.Type.split, True, 50, 3, 3, 16, 4), (ModuleStoreEnum.Type.split, True, 50, 3, 3, 16, 4),
......
...@@ -4,12 +4,17 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -4,12 +4,17 @@ from django.utils.translation import ugettext_lazy as _
from . import models from . import models
class ScheduleExperienceAdminInline(admin.StackedInline):
model = models.ScheduleExperience
@admin.register(models.Schedule) @admin.register(models.Schedule)
class ScheduleAdmin(admin.ModelAdmin): class ScheduleAdmin(admin.ModelAdmin):
list_display = ('username', 'course_id', 'active', 'start', 'upgrade_deadline') list_display = ('username', 'course_id', 'active', 'start', 'upgrade_deadline')
raw_id_fields = ('enrollment',) raw_id_fields = ('enrollment',)
readonly_fields = ('modified',) readonly_fields = ('modified',)
search_fields = ('enrollment__user__username', 'enrollment__course_id',) search_fields = ('enrollment__user__username', 'enrollment__course_id',)
inlines = (ScheduleExperienceAdminInline,)
def username(self, obj): def username(self, obj):
return obj.enrollment.user.username return obj.enrollment.user.username
......
from collections import namedtuple, defaultdict
from copy import deepcopy from copy import deepcopy
import datetime import datetime
import ddt import ddt
...@@ -9,6 +10,9 @@ from freezegun import freeze_time ...@@ -9,6 +10,9 @@ from freezegun import freeze_time
from mock import Mock, patch from mock import Mock, patch
import pytz import pytz
from commerce.models import CommerceConfiguration
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from courseware.models import DynamicUpgradeDeadlineConfiguration from courseware.models import DynamicUpgradeDeadlineConfiguration
from edx_ace.channel import ChannelType from edx_ace.channel import ChannelType
from edx_ace.utils.date import serialize from edx_ace.utils.date import serialize
...@@ -19,11 +23,13 @@ from openedx.core.djangoapps.schedules import resolvers, tasks ...@@ -19,11 +23,13 @@ from openedx.core.djangoapps.schedules import resolvers, tasks
from openedx.core.djangoapps.schedules.resolvers import _get_datetime_beginning_of_day from openedx.core.djangoapps.schedules.resolvers import _get_datetime_beginning_of_day
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory, ScheduleFactory from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory, ScheduleFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import FilteredQueryCountMixin, CacheIsolationTestCase
from student.models import CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
SITE_QUERY = 2 # django_site, site_configuration_siteconfiguration SITE_QUERY = 1 # django_site
SITE_CONFIG_QUERY = 1 # site_configuration_siteconfiguration
SCHEDULES_QUERY = 1 # schedules_schedule SCHEDULES_QUERY = 1 # schedules_schedule
COURSE_MODES_QUERY = 1 # course_modes_coursemode COURSE_MODES_QUERY = 1 # course_modes_coursemode
...@@ -33,18 +39,14 @@ ORG_DEADLINE_QUERY = 1 # courseware_orgdynamicupgradedeadlineconfiguration ...@@ -33,18 +39,14 @@ ORG_DEADLINE_QUERY = 1 # courseware_orgdynamicupgradedeadlineconfiguration
COURSE_DEADLINE_QUERY = 1 # courseware_coursedynamicupgradedeadlineconfiguration COURSE_DEADLINE_QUERY = 1 # courseware_coursedynamicupgradedeadlineconfiguration
COMMERCE_CONFIG_QUERY = 1 # commerce_commerceconfiguration COMMERCE_CONFIG_QUERY = 1 # commerce_commerceconfiguration
NUM_QUERIES_NO_MATCHING_SCHEDULES = ( NUM_QUERIES_SITE_SCHEDULES = (
SITE_QUERY + SITE_QUERY +
SITE_CONFIG_QUERY +
SCHEDULES_QUERY SCHEDULES_QUERY
) )
NUM_QUERIES_WITH_MATCHES = (
NUM_QUERIES_NO_MATCHING_SCHEDULES +
COURSE_MODES_QUERY
)
NUM_QUERIES_FIRST_MATCH = ( NUM_QUERIES_FIRST_MATCH = (
NUM_QUERIES_WITH_MATCHES NUM_QUERIES_SITE_SCHEDULES
+ GLOBAL_DEADLINE_QUERY + GLOBAL_DEADLINE_QUERY
+ ORG_DEADLINE_QUERY + ORG_DEADLINE_QUERY
+ COURSE_DEADLINE_QUERY + COURSE_DEADLINE_QUERY
...@@ -54,9 +56,12 @@ NUM_QUERIES_FIRST_MATCH = ( ...@@ -54,9 +56,12 @@ NUM_QUERIES_FIRST_MATCH = (
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
ExperienceTest = namedtuple('ExperienceTest', 'experience offset email_sent')
@ddt.ddt @ddt.ddt
@freeze_time('2017-08-01 00:00:00', tz_offset=0, tick=True) @freeze_time('2017-08-01 00:00:00', tz_offset=0, tick=True)
class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): class ScheduleSendEmailTestBase(FilteredQueryCountMixin, CacheIsolationTestCase):
__test__ = False __test__ = False
...@@ -73,6 +78,9 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -73,6 +78,9 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
ScheduleConfigFactory.create(site=self.site_config.site) ScheduleConfigFactory.create(site=self.site_config.site)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True) DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True)
self._courses_with_verified_modes = set()
def _calculate_bin_for_user(self, user): def _calculate_bin_for_user(self, user):
return user.id % self.task.num_bins return user.id % self.task.num_bins
...@@ -92,6 +100,24 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -92,6 +100,24 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
templates_override[0]['OPTIONS']['string_if_invalid'] = "TEMPLATE WARNING - MISSING VARIABLE [%s]" templates_override[0]['OPTIONS']['string_if_invalid'] = "TEMPLATE WARNING - MISSING VARIABLE [%s]"
return templates_override return templates_override
def _schedule_factory(self, offset=None, **factory_kwargs):
_, _, target_day, upgrade_deadline = self._get_dates(offset=offset)
factory_kwargs.setdefault('start', target_day)
factory_kwargs.setdefault('upgrade_deadline', upgrade_deadline)
factory_kwargs.setdefault('enrollment__course__self_paced', True)
if hasattr(self, 'experience_type'):
factory_kwargs.setdefault('experience__experience_type', self.experience_type)
schedule = ScheduleFactory(**factory_kwargs)
course_id = schedule.enrollment.course_id
if course_id not in self._courses_with_verified_modes:
CourseModeFactory(
course_id=course_id,
mode_slug=CourseMode.VERIFIED,
expiration_datetime=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30),
)
self._courses_with_verified_modes.add(course_id)
return schedule
def test_command_task_binding(self): def test_command_task_binding(self):
self.assertEqual(self.command.async_send_task, self.task) self.assertEqual(self.command.async_send_task, self.task)
...@@ -130,11 +156,7 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -130,11 +156,7 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
with patch.object(self.task, 'async_send_task') as mock_schedule_send: with patch.object(self.task, 'async_send_task') as mock_schedule_send:
current_day, offset, target_day, upgrade_deadline = self._get_dates() current_day, offset, target_day, upgrade_deadline = self._get_dates()
schedules = [ schedules = [
ScheduleFactory.create( self._schedule_factory() for _ in range(schedule_count)
start=target_day,
upgrade_deadline=upgrade_deadline,
enrollment__course__self_paced=True,
) for _ in range(schedule_count)
] ]
bins_in_use = frozenset((self._calculate_bin_for_user(s.enrollment.user)) for s in schedules) bins_in_use = frozenset((self._calculate_bin_for_user(s.enrollment.user)) for s in schedules)
...@@ -142,18 +164,17 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -142,18 +164,17 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
target_day_str = serialize(target_day) target_day_str = serialize(target_day)
for b in range(self.task.num_bins): for b in range(self.task.num_bins):
LOG.debug('Running bin %d', b) LOG.debug('Checking bin %d', b)
expected_queries = NUM_QUERIES_NO_MATCHING_SCHEDULES expected_queries = NUM_QUERIES_SITE_SCHEDULES
if b in bins_in_use: if b in bins_in_use:
if is_first_match: if is_first_match:
expected_queries = ( expected_queries = (
# Since this is the first match, we need to cache all of the config models, so we run a # Since this is the first match, we need to cache all of the config models, so we run a
# query for each of those... # query for each of those...
NUM_QUERIES_FIRST_MATCH NUM_QUERIES_FIRST_MATCH
+ COURSE_MODES_QUERY # to cache the course modes for this course
) )
is_first_match = False is_first_match = False
else:
expected_queries = NUM_QUERIES_WITH_MATCHES
with self.assertNumQueries(expected_queries, table_blacklist=WAFFLE_TABLES): with self.assertNumQueries(expected_queries, table_blacklist=WAFFLE_TABLES):
self.task.apply(kwargs=dict( self.task.apply(kwargs=dict(
...@@ -171,13 +192,12 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -171,13 +192,12 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
def test_no_course_overview(self): def test_no_course_overview(self):
current_day, offset, target_day, upgrade_deadline = self._get_dates() current_day, offset, target_day, upgrade_deadline = self._get_dates()
schedule = ScheduleFactory.create( # Don't use CourseEnrollmentFactory since it creates a course overview
start=target_day, enrollment = CourseEnrollment.objects.create(
upgrade_deadline=upgrade_deadline, course_id=CourseKey.from_string('edX/toy/Not_2012_Fall'),
enrollment__course__self_paced=True, user=UserFactory.create(),
) )
schedule.enrollment.course_id = CourseKey.from_string('edX/toy/Not_2012_Fall') schedule = self._schedule_factory(enrollment=enrollment)
schedule.enrollment.save()
with patch.object(self.task, 'async_send_task') as mock_schedule_send: with patch.object(self.task, 'async_send_task') as mock_schedule_send:
for b in range(self.task.num_bins): for b in range(self.task.num_bins):
...@@ -249,25 +269,16 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -249,25 +269,16 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
user2 = UserFactory.create(id=self.task.num_bins * 2) user2 = UserFactory.create(id=self.task.num_bins * 2)
current_day, offset, target_day, upgrade_deadline = self._get_dates() current_day, offset, target_day, upgrade_deadline = self._get_dates()
ScheduleFactory.create( self._schedule_factory(
upgrade_deadline=upgrade_deadline,
start=target_day,
enrollment__course__org=filtered_org, enrollment__course__org=filtered_org,
enrollment__course__self_paced=True,
enrollment__user=user1, enrollment__user=user1,
) )
ScheduleFactory.create( self._schedule_factory(
upgrade_deadline=upgrade_deadline,
start=target_day,
enrollment__course__org=unfiltered_org, enrollment__course__org=unfiltered_org,
enrollment__course__self_paced=True,
enrollment__user=user1, enrollment__user=user1,
) )
ScheduleFactory.create( self._schedule_factory(
upgrade_deadline=upgrade_deadline,
start=target_day,
enrollment__course__org=unfiltered_org, enrollment__course__org=unfiltered_org,
enrollment__course__self_paced=True,
enrollment__user=user2, enrollment__user=user2,
) )
...@@ -284,18 +295,13 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -284,18 +295,13 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
user1 = UserFactory.create(id=self.task.num_bins) user1 = UserFactory.create(id=self.task.num_bins)
current_day, offset, target_day, upgrade_deadline = self._get_dates() current_day, offset, target_day, upgrade_deadline = self._get_dates()
schedule = ScheduleFactory.create( end_date_offset = -2 if has_course_ended else 2
start=target_day, self._schedule_factory(
upgrade_deadline=upgrade_deadline,
enrollment__course__self_paced=True,
enrollment__user=user1, enrollment__user=user1,
enrollment__course__start=current_day - datetime.timedelta(days=30),
enrollment__course__end=current_day + datetime.timedelta(days=end_date_offset)
) )
schedule.enrollment.course.start = current_day - datetime.timedelta(days=30)
end_date_offset = -2 if has_course_ended else 2
schedule.enrollment.course.end = current_day + datetime.timedelta(days=end_date_offset)
schedule.enrollment.course.save()
with patch.object(self.task, 'async_send_task') as mock_schedule_send: with patch.object(self.task, 'async_send_task') as mock_schedule_send:
self.task.apply(kwargs=dict( self.task.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset, bin_num=0, site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset, bin_num=0,
...@@ -312,15 +318,14 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -312,15 +318,14 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
current_day, offset, target_day, upgrade_deadline = self._get_dates() current_day, offset, target_day, upgrade_deadline = self._get_dates()
num_courses = 3 num_courses = 3
for course_index in range(num_courses): for course_index in range(num_courses):
ScheduleFactory.create( self._schedule_factory(
start=target_day,
upgrade_deadline=upgrade_deadline,
enrollment__course__self_paced=True,
enrollment__user=user, enrollment__user=user,
enrollment__course__id=CourseKey.from_string('edX/toy/course{}'.format(course_index)) enrollment__course__id=CourseKey.from_string('edX/toy/course{}'.format(course_index))
) )
additional_course_queries = num_courses - 1 if self.queries_deadline_for_each_course else 0 # 2 queries per course, one for the course opt out and one for the course modes
# one query for course modes for the first schedule if we aren't checking the deadline for each course
additional_course_queries = (num_courses * 2) - 1 if self.queries_deadline_for_each_course else 1
expected_query_count = NUM_QUERIES_FIRST_MATCH + additional_course_queries expected_query_count = NUM_QUERIES_FIRST_MATCH + additional_course_queries
with self.assertNumQueries(expected_query_count, table_blacklist=WAFFLE_TABLES): with self.assertNumQueries(expected_query_count, table_blacklist=WAFFLE_TABLES):
with patch.object(self.task, 'async_send_task') as mock_schedule_send: with patch.object(self.task, 'async_send_task') as mock_schedule_send:
...@@ -333,7 +338,9 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -333,7 +338,9 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
self.assertEqual(mock_schedule_send.apply_async.call_count, expected_call_count) self.assertEqual(mock_schedule_send.apply_async.call_count, expected_call_count)
self.assertFalse(mock_ace.send.called) self.assertFalse(mock_ace.send.called)
@ddt.data(1, 10, 100) @ddt.data(
1, 10
)
def test_templates(self, message_count): def test_templates(self, message_count):
for offset in self.expected_offsets: for offset in self.expected_offsets:
self._assert_template_for_offset(offset, message_count) self._assert_template_for_offset(offset, message_count)
...@@ -344,10 +351,8 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -344,10 +351,8 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
user = UserFactory.create() user = UserFactory.create()
for course_index in range(message_count): for course_index in range(message_count):
ScheduleFactory.create( self._schedule_factory(
start=target_day, offset=offset,
upgrade_deadline=upgrade_deadline,
enrollment__course__self_paced=True,
enrollment__user=user, enrollment__user=user,
enrollment__course__id=CourseKey.from_string('edX/toy/course{}'.format(course_index)) enrollment__course__id=CourseKey.from_string('edX/toy/course{}'.format(course_index))
) )
...@@ -366,7 +371,10 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -366,7 +371,10 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
num_expected_queries = NUM_QUERIES_FIRST_MATCH num_expected_queries = NUM_QUERIES_FIRST_MATCH
if self.queries_deadline_for_each_course: if self.queries_deadline_for_each_course:
num_expected_queries += (message_count - 1) # one query per course for opt-out and one for course modes
num_expected_queries += (message_count * 2) - 1
else:
num_expected_queries += 1
with self.assertNumQueries(num_expected_queries, table_blacklist=WAFFLE_TABLES): with self.assertNumQueries(num_expected_queries, table_blacklist=WAFFLE_TABLES):
self.task.apply(kwargs=dict( self.task.apply(kwargs=dict(
...@@ -385,3 +393,23 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase): ...@@ -385,3 +393,23 @@ class ScheduleSendEmailTestBase(SharedModuleStoreTestCase):
self.assertNotIn("TEMPLATE WARNING", template) self.assertNotIn("TEMPLATE WARNING", template)
self.assertNotIn("{{", template) self.assertNotIn("{{", template)
self.assertNotIn("}}", template) self.assertNotIn("}}", template)
def _check_if_email_sent_for_experience(self, test_config):
current_day, offset, target_day, _ = self._get_dates(offset=test_config.offset)
kwargs = {
'offset': offset
}
if test_config.experience is None:
kwargs['experience'] = None
else:
kwargs['experience__experience_type'] = test_config.experience
schedule = self._schedule_factory(**kwargs)
with patch.object(tasks, 'ace') as mock_ace:
self.task.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset,
bin_num=self._calculate_bin_for_user(schedule.enrollment.user),
))
self.assertEqual(mock_ace.send.called, test_config.email_sent)
import ddt
from mock import patch from mock import patch
from unittest import skipUnless from unittest import skipUnless
...@@ -5,11 +6,16 @@ from django.conf import settings ...@@ -5,11 +6,16 @@ from django.conf import settings
from openedx.core.djangoapps.schedules import resolvers, tasks from openedx.core.djangoapps.schedules import resolvers, tasks
from openedx.core.djangoapps.schedules.management.commands import send_course_update as nudge from openedx.core.djangoapps.schedules.management.commands import send_course_update as nudge
from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import ScheduleSendEmailTestBase from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import (
ScheduleSendEmailTestBase,
ExperienceTest
)
from openedx.core.djangoapps.schedules.management.commands.tests.upsell_base import ScheduleUpsellTestMixin from openedx.core.djangoapps.schedules.management.commands.tests.upsell_base import ScheduleUpsellTestMixin
from openedx.core.djangoapps.schedules.models import ScheduleExperience
from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.core.djangolib.testing.utils import skip_unless_lms
@ddt.ddt
@skip_unless_lms @skip_unless_lms
@skipUnless( @skipUnless(
'openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS, 'openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS,
...@@ -25,7 +31,8 @@ class TestSendCourseUpdate(ScheduleUpsellTestMixin, ScheduleSendEmailTestBase): ...@@ -25,7 +31,8 @@ class TestSendCourseUpdate(ScheduleUpsellTestMixin, ScheduleSendEmailTestBase):
command = nudge.Command command = nudge.Command
deliver_config = 'deliver_course_update' deliver_config = 'deliver_course_update'
enqueue_config = 'enqueue_course_update' enqueue_config = 'enqueue_course_update'
expected_offsets = xrange(-7, -77, -7) expected_offsets = range(-7, -77, -7)
experience_type = ScheduleExperience.EXPERIENCES.course_updates
queries_deadline_for_each_course = True queries_deadline_for_each_course = True
...@@ -35,3 +42,11 @@ class TestSendCourseUpdate(ScheduleUpsellTestMixin, ScheduleSendEmailTestBase): ...@@ -35,3 +42,11 @@ class TestSendCourseUpdate(ScheduleUpsellTestMixin, ScheduleSendEmailTestBase):
mock_highlights = patcher.start() mock_highlights = patcher.start()
mock_highlights.return_value = ['Highlight {}'.format(num + 1) for num in range(3)] mock_highlights.return_value = ['Highlight {}'.format(num + 1) for num in range(3)]
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
@ddt.data(
ExperienceTest(experience=ScheduleExperience.EXPERIENCES.default, offset=expected_offsets[0], email_sent=False),
ExperienceTest(experience=ScheduleExperience.EXPERIENCES.course_updates, offset=expected_offsets[0], email_sent=True),
ExperienceTest(experience=None, offset=expected_offsets[0], email_sent=False),
)
def test_schedule_in_different_experience(self, test_config):
self._check_if_email_sent_for_experience(test_config)
from unittest import skipUnless from unittest import skipUnless
import ddt
from django.conf import settings from django.conf import settings
from openedx.core.djangoapps.schedules import resolvers, tasks from openedx.core.djangoapps.schedules import resolvers, tasks
from openedx.core.djangoapps.schedules.management.commands import send_recurring_nudge as nudge from openedx.core.djangoapps.schedules.management.commands import send_recurring_nudge as nudge
from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import ScheduleSendEmailTestBase from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import ScheduleSendEmailTestBase, \
ExperienceTest
from openedx.core.djangoapps.schedules.management.commands.tests.upsell_base import ScheduleUpsellTestMixin from openedx.core.djangoapps.schedules.management.commands.tests.upsell_base import ScheduleUpsellTestMixin
from openedx.core.djangoapps.schedules.models import ScheduleExperience
from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.core.djangolib.testing.utils import skip_unless_lms
@ddt.ddt
@skip_unless_lms @skip_unless_lms
@skipUnless( @skipUnless(
'openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS, 'openedx.core.djangoapps.schedules.apps.SchedulesConfig' in settings.INSTALLED_APPS,
...@@ -27,3 +31,14 @@ class TestSendRecurringNudge(ScheduleUpsellTestMixin, ScheduleSendEmailTestBase) ...@@ -27,3 +31,14 @@ class TestSendRecurringNudge(ScheduleUpsellTestMixin, ScheduleSendEmailTestBase)
expected_offsets = (-3, -10) expected_offsets = (-3, -10)
consolidates_emails_for_learner = True consolidates_emails_for_learner = True
@ddt.data(
ExperienceTest(experience=ScheduleExperience.EXPERIENCES.default, offset=-3, email_sent=True),
ExperienceTest(experience=ScheduleExperience.EXPERIENCES.default, offset=-10, email_sent=True),
ExperienceTest(experience=ScheduleExperience.EXPERIENCES.course_updates, offset=-3, email_sent=True),
ExperienceTest(experience=ScheduleExperience.EXPERIENCES.course_updates, offset=-10, email_sent=False),
ExperienceTest(experience=None, offset=-3, email_sent=True),
ExperienceTest(experience=None, offset=-10, email_sent=True),
)
def test_nudge_experience(self, test_config):
self._check_if_email_sent_for_experience(test_config)
...@@ -11,8 +11,9 @@ from opaque_keys.edx.locator import CourseLocator ...@@ -11,8 +11,9 @@ from opaque_keys.edx.locator import CourseLocator
from course_modes.models import CourseMode from course_modes.models import CourseMode
from openedx.core.djangoapps.schedules import resolvers, tasks from openedx.core.djangoapps.schedules import resolvers, tasks
from openedx.core.djangoapps.schedules.management.commands import send_upgrade_reminder as reminder from openedx.core.djangoapps.schedules.management.commands import send_upgrade_reminder as reminder
from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import ScheduleSendEmailTestBase from openedx.core.djangoapps.schedules.management.commands.tests.send_email_base import ScheduleSendEmailTestBase, \
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory ExperienceTest
from openedx.core.djangoapps.schedules.models import ScheduleExperience
from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
...@@ -41,18 +42,14 @@ class TestUpgradeReminder(ScheduleSendEmailTestBase): ...@@ -41,18 +42,14 @@ class TestUpgradeReminder(ScheduleSendEmailTestBase):
@ddt.data(True, False) @ddt.data(True, False)
@patch.object(tasks, 'ace') @patch.object(tasks, 'ace')
def test_verified_learner(self, is_verified, mock_ace): def test_verified_learner(self, is_verified, mock_ace):
user = UserFactory.create(id=self.task.num_bins)
current_day, offset, target_day, upgrade_deadline = self._get_dates() current_day, offset, target_day, upgrade_deadline = self._get_dates()
ScheduleFactory.create( schedule = self._schedule_factory(
upgrade_deadline=upgrade_deadline,
enrollment__course__self_paced=True,
enrollment__user=user,
enrollment__mode=CourseMode.VERIFIED if is_verified else CourseMode.AUDIT, enrollment__mode=CourseMode.VERIFIED if is_verified else CourseMode.AUDIT,
) )
self.task.apply(kwargs=dict( self.task.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset, site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset,
bin_num=self._calculate_bin_for_user(user), bin_num=self._calculate_bin_for_user(schedule.enrollment.user),
)) ))
self.assertEqual(mock_ace.send.called, not is_verified) self.assertEqual(mock_ace.send.called, not is_verified)
...@@ -62,10 +59,8 @@ class TestUpgradeReminder(ScheduleSendEmailTestBase): ...@@ -62,10 +59,8 @@ class TestUpgradeReminder(ScheduleSendEmailTestBase):
user = UserFactory.create() user = UserFactory.create()
schedules = [ schedules = [
ScheduleFactory.create( self._schedule_factory(
upgrade_deadline=upgrade_deadline,
enrollment__user=user, enrollment__user=user,
enrollment__course__self_paced=True,
enrollment__course__id=CourseLocator('edX', 'toy', 'Course{}'.format(i)), enrollment__course__id=CourseLocator('edX', 'toy', 'Course{}'.format(i)),
enrollment__mode=CourseMode.VERIFIED if i in (0, 3) else CourseMode.AUDIT, enrollment__mode=CourseMode.VERIFIED if i in (0, 3) else CourseMode.AUDIT,
) )
...@@ -88,3 +83,23 @@ class TestUpgradeReminder(ScheduleSendEmailTestBase): ...@@ -88,3 +83,23 @@ class TestUpgradeReminder(ScheduleSendEmailTestBase):
message.context['course_ids'], message.context['course_ids'],
[str(schedules[i].enrollment.course.id) for i in (1, 2, 4)] [str(schedules[i].enrollment.course.id) for i in (1, 2, 4)]
) )
@patch.object(tasks, 'ace')
def test_course_without_verified_mode(self, mock_ace):
current_day, offset, target_day, upgrade_deadline = self._get_dates()
schedule = self._schedule_factory()
schedule.enrollment.course.modes.filter(mode_slug=CourseMode.VERIFIED).delete()
self.task.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=serialize(target_day), day_offset=offset,
bin_num=self._calculate_bin_for_user(schedule.enrollment.user),
))
self.assertEqual(mock_ace.send.called, False)
@ddt.data(
ExperienceTest(experience=ScheduleExperience.EXPERIENCES.default, offset=expected_offsets[0], email_sent=True),
ExperienceTest(experience=ScheduleExperience.EXPERIENCES.course_updates, offset=expected_offsets[0], email_sent=False),
ExperienceTest(experience=None, offset=expected_offsets[0], email_sent=True),
)
def test_upgrade_reminder_experience(self, test_config):
self._check_if_email_sent_for_experience(test_config)
...@@ -8,7 +8,6 @@ from edx_ace.utils.date import serialize ...@@ -8,7 +8,6 @@ from edx_ace.utils.date import serialize
from edx_ace.message import Message from edx_ace.message import Message
from courseware.models import DynamicUpgradeDeadlineConfiguration from courseware.models import DynamicUpgradeDeadlineConfiguration
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
@ddt.ddt @ddt.ddt
...@@ -34,10 +33,8 @@ class ScheduleUpsellTestMixin(object): ...@@ -34,10 +33,8 @@ class ScheduleUpsellTestMixin(object):
if testcase.set_deadline: if testcase.set_deadline:
upgrade_deadline = current_day + datetime.timedelta(days=testcase.deadline_offset) upgrade_deadline = current_day + datetime.timedelta(days=testcase.deadline_offset)
schedule = ScheduleFactory.create( schedule = self._schedule_factory(
start=target_day, upgrade_deadline=upgrade_deadline
upgrade_deadline=upgrade_deadline,
enrollment__course__self_paced=True,
) )
sent_messages = [] sent_messages = []
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('schedules', '0005_auto_20171010_1722'),
]
operations = [
migrations.CreateModel(
name='ScheduleExperience',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('experience_type', models.PositiveSmallIntegerField(default=0, choices=[(0, b'Recurring Nudge and Upgrade Reminder'), (1, b'Course Updates')])),
('schedule', models.OneToOneField(related_name='experience', to='schedules.Schedule')),
],
),
]
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from model_utils import Choices
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from config_models.models import ConfigurationModel from config_models.models import ConfigurationModel
...@@ -23,6 +24,12 @@ class Schedule(TimeStampedModel): ...@@ -23,6 +24,12 @@ class Schedule(TimeStampedModel):
help_text=_('Deadline by which the learner must upgrade to a verified seat') help_text=_('Deadline by which the learner must upgrade to a verified seat')
) )
def get_experience_type(self):
try:
return self.experience.experience_type
except ScheduleExperience.DoesNotExist:
return ScheduleExperience.EXPERIENCES.default
class Meta(object): class Meta(object):
verbose_name = _('Schedule') verbose_name = _('Schedule')
verbose_name_plural = _('Schedules') verbose_name_plural = _('Schedules')
...@@ -39,3 +46,13 @@ class ScheduleConfig(ConfigurationModel): ...@@ -39,3 +46,13 @@ class ScheduleConfig(ConfigurationModel):
deliver_upgrade_reminder = models.BooleanField(default=False) deliver_upgrade_reminder = models.BooleanField(default=False)
enqueue_course_update = models.BooleanField(default=False) enqueue_course_update = models.BooleanField(default=False)
deliver_course_update = models.BooleanField(default=False) deliver_course_update = models.BooleanField(default=False)
class ScheduleExperience(models.Model):
EXPERIENCES = Choices(
(0, 'default', 'Recurring Nudge and Upgrade Reminder'),
(1, 'course_updates', 'Course Updates')
)
schedule = models.OneToOneField(Schedule, related_name='experience')
experience_type = models.PositiveSmallIntegerField(choices=EXPERIENCES, default=EXPERIENCES.default)
...@@ -18,7 +18,7 @@ from courseware.date_summary import verified_upgrade_deadline_link, verified_upg ...@@ -18,7 +18,7 @@ from courseware.date_summary import verified_upgrade_deadline_link, verified_upg
from openedx.core.djangoapps.monitoring_utils import function_trace, set_custom_metric from openedx.core.djangoapps.monitoring_utils import function_trace, set_custom_metric
from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG from openedx.core.djangoapps.schedules.config import COURSE_UPDATE_WAFFLE_FLAG
from openedx.core.djangoapps.schedules.exceptions import CourseUpdateDoesNotExist from openedx.core.djangoapps.schedules.exceptions import CourseUpdateDoesNotExist
from openedx.core.djangoapps.schedules.models import Schedule from openedx.core.djangoapps.schedules.models import Schedule, ScheduleExperience
from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin
from openedx.core.djangoapps.schedules.template_context import ( from openedx.core.djangoapps.schedules.template_context import (
absolute_url, absolute_url,
...@@ -64,6 +64,9 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver): ...@@ -64,6 +64,9 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver):
relative to. For example, if this resolver finds schedules that started 7 days ago relative to. For example, if this resolver finds schedules that started 7 days ago
this variable should be set to "start". this variable should be set to "start".
num_bins -- the int number of bins to split the users into num_bins -- the int number of bins to split the users into
experience_filter -- a queryset filter used to select only the users who should be getting this message as part
of their experience. This defaults to users without a specified experience type and those
in the "recurring nudges and upgrade reminder" experience.
""" """
async_send_task = attr.ib() async_send_task = attr.ib()
site = attr.ib() site = attr.ib()
...@@ -74,6 +77,8 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver): ...@@ -74,6 +77,8 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver):
schedule_date_field = None schedule_date_field = None
num_bins = DEFAULT_NUM_BINS num_bins = DEFAULT_NUM_BINS
experience_filter = (Q(experience__experience_type=ScheduleExperience.EXPERIENCES.default)
| Q(experience__isnull=True))
def __attrs_post_init__(self): def __attrs_post_init__(self):
# TODO: in the next refactor of this task, pass in current_datetime instead of reproducing it here # TODO: in the next refactor of this task, pass in current_datetime instead of reproducing it here
...@@ -122,11 +127,10 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver): ...@@ -122,11 +127,10 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver):
schedules = Schedule.objects.select_related( schedules = Schedule.objects.select_related(
'enrollment__user__profile', 'enrollment__user__profile',
'enrollment__course', 'enrollment__course',
).prefetch_related(
'enrollment__course__modes'
).filter( ).filter(
Q(enrollment__course__end__isnull=True) | Q( Q(enrollment__course__end__isnull=True) | Q(
enrollment__course__end__gte=self.current_datetime), enrollment__course__end__gte=self.current_datetime),
self.experience_filter,
enrollment__user__in=users, enrollment__user__in=users,
enrollment__is_active=True, enrollment__is_active=True,
**schedule_day_equals_target_day_filter **schedule_day_equals_target_day_filter
...@@ -143,6 +147,8 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver): ...@@ -143,6 +147,8 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver):
# This will run the query and cache all of the results in memory. # This will run the query and cache all of the results in memory.
num_schedules = len(schedules) num_schedules = len(schedules)
LOG.debug('Number of schedules = %d', num_schedules)
# This should give us a sense of the volume of data being processed by each task. # This should give us a sense of the volume of data being processed by each task.
set_custom_metric('num_schedules', num_schedules) set_custom_metric('num_schedules', num_schedules)
...@@ -232,6 +238,14 @@ class RecurringNudgeResolver(BinnedSchedulesBaseResolver): ...@@ -232,6 +238,14 @@ class RecurringNudgeResolver(BinnedSchedulesBaseResolver):
schedule_date_field = 'start' schedule_date_field = 'start'
num_bins = RECURRING_NUDGE_NUM_BINS num_bins = RECURRING_NUDGE_NUM_BINS
@property
def experience_filter(self):
if self.day_offset == -3:
experiences = [ScheduleExperience.EXPERIENCES.default, ScheduleExperience.EXPERIENCES.course_updates]
return Q(experience__experience_type__in=experiences) | Q(experience__isnull=True)
else:
return Q(experience__experience_type=ScheduleExperience.EXPERIENCES.default) | Q(experience__isnull=True)
def get_template_context(self, user, user_schedules): def get_template_context(self, user, user_schedules):
first_schedule = user_schedules[0] first_schedule = user_schedules[0]
context = { context = {
...@@ -333,6 +347,7 @@ class CourseUpdateResolver(BinnedSchedulesBaseResolver): ...@@ -333,6 +347,7 @@ class CourseUpdateResolver(BinnedSchedulesBaseResolver):
log_prefix = 'Course Update' log_prefix = 'Course Update'
schedule_date_field = 'start' schedule_date_field = 'start'
num_bins = COURSE_UPDATE_NUM_BINS num_bins = COURSE_UPDATE_NUM_BINS
experience_filter = Q(experience__experience_type=ScheduleExperience.EXPERIENCES.course_updates)
def schedules_for_bin(self): def schedules_for_bin(self):
week_num = abs(self.day_offset) / 7 week_num = abs(self.day_offset) / 7
......
...@@ -12,6 +12,9 @@ from courseware.models import ( ...@@ -12,6 +12,9 @@ from courseware.models import (
OrgDynamicUpgradeDeadlineConfiguration OrgDynamicUpgradeDeadlineConfiguration
) )
from edx_ace.utils import date from edx_ace.utils import date
from openedx.core.djangoapps.schedules.exceptions import CourseUpdateDoesNotExist
from openedx.core.djangoapps.schedules.models import ScheduleExperience
from openedx.core.djangoapps.schedules.resolvers import get_week_highlights
from openedx.core.djangoapps.signals.signals import COURSE_START_DATE_CHANGED from openedx.core.djangoapps.signals.signals import COURSE_START_DATE_CHANGED
from openedx.core.djangoapps.theming.helpers import get_current_site from openedx.core.djangoapps.theming.helpers import get_current_site
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -53,14 +56,22 @@ def create_schedule(sender, **kwargs): ...@@ -53,14 +56,22 @@ def create_schedule(sender, **kwargs):
upgrade_deadline = _calculate_upgrade_deadline(enrollment.course_id, content_availability_date) upgrade_deadline = _calculate_upgrade_deadline(enrollment.course_id, content_availability_date)
Schedule.objects.create( schedule = Schedule.objects.create(
enrollment=enrollment, enrollment=enrollment,
start=content_availability_date, start=content_availability_date,
upgrade_deadline=upgrade_deadline upgrade_deadline=upgrade_deadline
) )
log.debug('Schedules: created a new schedule starting at %s with an upgrade deadline of %s', try:
content_availability_date, upgrade_deadline) get_week_highlights(enrollment.course_id, 1)
experience_type = ScheduleExperience.EXPERIENCES.course_updates
except CourseUpdateDoesNotExist:
experience_type = ScheduleExperience.EXPERIENCES.default
ScheduleExperience(schedule=schedule, experience_type=experience_type).save()
log.debug('Schedules: created a new schedule starting at %s with an upgrade deadline of %s and experience type: %s',
content_availability_date, upgrade_deadline, ScheduleExperience.EXPERIENCES[experience_type])
@receiver(COURSE_START_DATE_CHANGED, dispatch_uid="update_schedules_on_course_start_changed") @receiver(COURSE_START_DATE_CHANGED, dispatch_uid="update_schedules_on_course_start_changed")
......
...@@ -6,6 +6,13 @@ from student.tests.factories import CourseEnrollmentFactory ...@@ -6,6 +6,13 @@ from student.tests.factories import CourseEnrollmentFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
class ScheduleExperienceFactory(factory.DjangoModelFactory):
class Meta(object):
model = models.ScheduleExperience
experience_type = models.ScheduleExperience.EXPERIENCES.default
class ScheduleFactory(factory.DjangoModelFactory): class ScheduleFactory(factory.DjangoModelFactory):
class Meta(object): class Meta(object):
model = models.Schedule model = models.Schedule
...@@ -13,6 +20,7 @@ class ScheduleFactory(factory.DjangoModelFactory): ...@@ -13,6 +20,7 @@ class ScheduleFactory(factory.DjangoModelFactory):
start = factory.Faker('future_datetime', tzinfo=pytz.UTC) start = factory.Faker('future_datetime', tzinfo=pytz.UTC)
upgrade_deadline = factory.Faker('future_datetime', tzinfo=pytz.UTC) upgrade_deadline = factory.Faker('future_datetime', tzinfo=pytz.UTC)
enrollment = factory.SubFactory(CourseEnrollmentFactory) enrollment = factory.SubFactory(CourseEnrollmentFactory)
experience = factory.RelatedFactory(ScheduleExperienceFactory, 'schedule')
class ScheduleConfigFactory(factory.DjangoModelFactory): class ScheduleConfigFactory(factory.DjangoModelFactory):
......
...@@ -6,6 +6,8 @@ from pytz import utc ...@@ -6,6 +6,8 @@ from pytz import utc
from course_modes.models import CourseMode from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory from course_modes.tests.factories import CourseModeFactory
from courseware.models import DynamicUpgradeDeadlineConfiguration from courseware.models import DynamicUpgradeDeadlineConfiguration
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.schedules.models import ScheduleExperience
from openedx.core.djangoapps.schedules.signals import CREATE_SCHEDULE_WAFFLE_FLAG from openedx.core.djangoapps.schedules.signals import CREATE_SCHEDULE_WAFFLE_FLAG
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
...@@ -23,15 +25,22 @@ from ..tests.factories import ScheduleConfigFactory ...@@ -23,15 +25,22 @@ from ..tests.factories import ScheduleConfigFactory
@skip_unless_lms @skip_unless_lms
class CreateScheduleTests(SharedModuleStoreTestCase): class CreateScheduleTests(SharedModuleStoreTestCase):
def assert_schedule_created(self): def assert_schedule_created(self, experience_type=ScheduleExperience.EXPERIENCES.default):
course = _create_course_run(self_paced=True) course = _create_course_run(self_paced=True)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT) enrollment = CourseEnrollmentFactory(
course_id=course.id,
mode=CourseMode.AUDIT,
)
self.assertIsNotNone(enrollment.schedule) self.assertIsNotNone(enrollment.schedule)
self.assertIsNone(enrollment.schedule.upgrade_deadline) self.assertIsNone(enrollment.schedule.upgrade_deadline)
self.assertEquals(enrollment.schedule.experience.experience_type, experience_type)
def assert_schedule_not_created(self): def assert_schedule_not_created(self):
course = _create_course_run(self_paced=True) course = _create_course_run(self_paced=True)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT) enrollment = CourseEnrollmentFactory(
course_id=course.id,
mode=CourseMode.AUDIT,
)
with self.assertRaises(Schedule.DoesNotExist): with self.assertRaises(Schedule.DoesNotExist):
enrollment.schedule enrollment.schedule
...@@ -78,6 +87,14 @@ class CreateScheduleTests(SharedModuleStoreTestCase): ...@@ -78,6 +87,14 @@ class CreateScheduleTests(SharedModuleStoreTestCase):
with self.assertRaises(Schedule.DoesNotExist): with self.assertRaises(Schedule.DoesNotExist):
enrollment.schedule enrollment.schedule
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
@patch('openedx.core.djangoapps.schedules.signals.get_week_highlights')
def test_create_schedule_course_updates_experience(self, mock_get_week_highlights, mock_get_current_site):
site = SiteFactory.create()
mock_get_week_highlights.return_value = True
mock_get_current_site.return_value = site
self.assert_schedule_created(experience_type=ScheduleExperience.EXPERIENCES.course_updates)
@ddt.ddt @ddt.ddt
@skip_unless_lms @skip_unless_lms
...@@ -104,7 +121,7 @@ class UpdateScheduleTests(SharedModuleStoreTestCase): ...@@ -104,7 +121,7 @@ class UpdateScheduleTests(SharedModuleStoreTestCase):
course = _create_course_run(self_paced=True, start_day_offset=5) # course starts in future course = _create_course_run(self_paced=True, start_day_offset=5) # course starts in future
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT) enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
self.assert_schedule_dates(enrollment.schedule, enrollment.course_overview.start) self.assert_schedule_dates(enrollment.schedule, enrollment.course.start)
course.start = course.start + datetime.timedelta(days=3) # new course start changes to another future date course.start = course.start + datetime.timedelta(days=3) # new course start changes to another future date
self.store.update_item(course, ModuleStoreEnum.UserID.test) self.store.update_item(course, ModuleStoreEnum.UserID.test)
...@@ -128,7 +145,7 @@ class UpdateScheduleTests(SharedModuleStoreTestCase): ...@@ -128,7 +145,7 @@ class UpdateScheduleTests(SharedModuleStoreTestCase):
course = _create_course_run(self_paced=True, start_day_offset=5) # course starts in future course = _create_course_run(self_paced=True, start_day_offset=5) # course starts in future
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT) enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
previous_start = enrollment.course_overview.start previous_start = enrollment.course.start
self.assert_schedule_dates(enrollment.schedule, previous_start) self.assert_schedule_dates(enrollment.schedule, previous_start)
course.start = course.start + datetime.timedelta(days=-10) # new course start changes to a past date course.start = course.start + datetime.timedelta(days=-10) # new course start changes to a past date
......
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