change the config model used to configure schedules

parent 50e04d5f
...@@ -50,6 +50,7 @@ import lms.lib.comment_client as cc ...@@ -50,6 +50,7 @@ import lms.lib.comment_client as cc
import request_cache import request_cache
from certificates.models import GeneratedCertificate from certificates.models import GeneratedCertificate
from course_modes.models import CourseMode from course_modes.models import CourseMode
from courseware.models import CourseScheduleConfiguration
from enrollment.api import _default_course_mode from enrollment.api import _default_course_mode
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
...@@ -1707,8 +1708,9 @@ class CourseEnrollment(models.Model): ...@@ -1707,8 +1708,9 @@ class CourseEnrollment(models.Model):
) )
return None return None
schedule_config = CourseScheduleConfiguration.current(self.course_id)
try: try:
if self.schedule: if schedule_config.enabled and schedule_config.verified_upgrade_deadline_enabled and self.schedule:
log.debug( log.debug(
'Schedules: Pulling upgrade deadline for CourseEnrollment %d from Schedule %d.', 'Schedules: Pulling upgrade deadline for CourseEnrollment %d from Schedule %d.',
self.id, self.schedule.id self.id, self.schedule.id
......
...@@ -8,4 +8,5 @@ admin.site.register(models.DynamicUpgradeDeadlineConfiguration, ConfigurationMod ...@@ -8,4 +8,5 @@ admin.site.register(models.DynamicUpgradeDeadlineConfiguration, ConfigurationMod
admin.site.register(models.OfflineComputedGrade) admin.site.register(models.OfflineComputedGrade)
admin.site.register(models.OfflineComputedGradeLog) admin.site.register(models.OfflineComputedGradeLog)
admin.site.register(models.CourseDynamicUpgradeDeadlineConfiguration, KeyedConfigurationModelAdmin) admin.site.register(models.CourseDynamicUpgradeDeadlineConfiguration, KeyedConfigurationModelAdmin)
admin.site.register(models.CourseScheduleConfiguration, KeyedConfigurationModelAdmin)
admin.site.register(models.StudentModule) admin.site.register(models.StudentModule)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import openedx.core.djangoapps.xmodule_django.models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('courseware', '0002_coursedynamicupgradedeadlineconfiguration_dynamicupgradedeadlineconfiguration'),
]
operations = [
migrations.CreateModel(
name='CourseScheduleConfiguration',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
('course_id', openedx.core.djangoapps.xmodule_django.models.CourseKeyField(max_length=255, db_index=True)),
('verified_upgrade_deadline_days', models.PositiveSmallIntegerField(default=21, help_text='Number of days a learner has to upgrade after content is made available')),
('verified_upgrade_deadline_enabled', models.BooleanField(default=False, help_text='Should this course display an upgrade deadline to users. Only applies to courses with schedules.')),
('verified_upgrade_reminder_message_enabled', models.BooleanField(default=False, help_text='Should we send verified upgrade reminder messages to users in this course.')),
('recurring_reminder_message_enabled', models.BooleanField(default=False, help_text='Should we send recurring nudge messages to users in this course.')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
),
]
...@@ -400,3 +400,34 @@ class CourseDynamicUpgradeDeadlineConfiguration(ConfigurationModel): ...@@ -400,3 +400,34 @@ class CourseDynamicUpgradeDeadlineConfiguration(ConfigurationModel):
default=False, default=False,
help_text=_('Disable the dynamic upgrade deadline for this course run.') help_text=_('Disable the dynamic upgrade deadline for this course run.')
) )
class CourseScheduleConfiguration(ConfigurationModel):
"""
Per-course run configuration for dynamic upgrade deadlines.
This model controls dynamic upgrade deadlines on a per-course run level, allowing course runs to
have different deadlines or opt out of the functionality altogether.
"""
class Meta(object):
app_label = 'courseware'
KEY_FIELDS = ('course_id',)
course_id = CourseKeyField(max_length=255, db_index=True)
verified_upgrade_deadline_days = models.PositiveSmallIntegerField(
default=21,
help_text=_('Number of days a learner has to upgrade after content is made available')
)
verified_upgrade_deadline_enabled = models.BooleanField(
default=False,
help_text=_('Should this course display an upgrade deadline to users. Only applies to courses with schedules.'),
)
verified_upgrade_reminder_message_enabled = models.BooleanField(
default=False,
help_text=_('Should we send verified upgrade reminder messages to users in this course.'),
)
recurring_reminder_message_enabled = models.BooleanField(
default=False,
help_text=_('Should we send recurring nudge messages to users in this course.'),
)
...@@ -21,7 +21,7 @@ from courseware.date_summary import ( ...@@ -21,7 +21,7 @@ from courseware.date_summary import (
VerifiedUpgradeDeadlineDate, VerifiedUpgradeDeadlineDate,
CertificateAvailableDate CertificateAvailableDate
) )
from courseware.models import DynamicUpgradeDeadlineConfiguration, CourseDynamicUpgradeDeadlineConfiguration from courseware.models import CourseScheduleConfiguration
from lms.djangoapps.verify_student.models import VerificationDeadline from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
...@@ -36,7 +36,6 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -36,7 +36,6 @@ from xmodule.modulestore.tests.factories import CourseFactory
@attr(shard=1) @attr(shard=1)
@ddt.ddt @ddt.ddt
@waffle.testutils.override_switch('schedules.enable-create-schedule-receiver', True)
class CourseDateSummaryTest(SharedModuleStoreTestCase): class CourseDateSummaryTest(SharedModuleStoreTestCase):
"""Tests for course date summary blocks.""" """Tests for course date summary blocks."""
...@@ -480,46 +479,37 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): ...@@ -480,46 +479,37 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def test_date_with_self_paced_with_enrollment_before_course_start(self): def test_date_with_self_paced_with_enrollment_before_course_start(self):
""" Enrolling before a course begins should result in the upgrade deadline being set relative to the """ Enrolling before a course begins should result in the upgrade deadline being set relative to the
course start date. """ course start date. """
global_config = DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
course = self.create_self_paced_course_run(days_till_start=3) course = self.create_self_paced_course_run(days_till_start=3)
schedule_config = CourseScheduleConfiguration.objects.create(course_id=course.id, enabled=True)
overview = CourseOverview.get_from_id(course.id) overview = CourseOverview.get_from_id(course.id)
expected = overview.start + timedelta(days=global_config.deadline_days) expected = overview.start + timedelta(days=schedule_config.verified_upgrade_deadline_days)
self.assert_upgrade_deadline(course, expected) self.assert_upgrade_deadline(course, expected)
def test_date_with_self_paced_with_enrollment_after_course_start(self): def test_date_with_self_paced_with_enrollment_after_course_start(self):
""" Enrolling after a course begins should result in the upgrade deadline being set relative to the """ Enrolling after a course begins should result in the upgrade deadline being set relative to the
enrollment date. """ enrollment date. """
global_config = DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
course = self.create_self_paced_course_run(days_till_start=-1) course = self.create_self_paced_course_run(days_till_start=-1)
schedule_config = CourseScheduleConfiguration.objects.create(course_id=course.id, enabled=True)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT) enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
block = VerifiedUpgradeDeadlineDate(course, enrollment.user) block = VerifiedUpgradeDeadlineDate(course, enrollment.user)
expected = enrollment.created + timedelta(days=global_config.deadline_days) expected = enrollment.created + timedelta(days=schedule_config.verified_upgrade_deadline_days)
self.assertEqual(block.date, expected) self.assertEqual(block.date, expected)
def test_date_with_self_paced_with_enrollment_after_course_start_days_configured(self):
# Courses should be able to override the deadline # Courses should be able to override the deadline
course_config = CourseDynamicUpgradeDeadlineConfiguration.objects.create( course = self.create_self_paced_course_run(days_till_start=-1)
enabled=True, course_id=course.id, opt_out=False, deadline_days=3 schedule_config = CourseScheduleConfiguration.objects.create(
course_id=course.id, enabled=True, verified_upgrade_deadline_days=3
) )
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT) enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
block = VerifiedUpgradeDeadlineDate(course, enrollment.user) block = VerifiedUpgradeDeadlineDate(course, enrollment.user)
expected = enrollment.created + timedelta(days=course_config.deadline_days) expected = enrollment.created + timedelta(days=schedule_config.verified_upgrade_deadline_days)
self.assertEqual(block.date, expected) self.assertEqual(block.date, expected)
def test_date_with_self_paced_without_dynamic_upgrade_deadline(self): def test_date_with_self_paced_without_dynamic_upgrade_deadline(self):
""" Disabling the dynamic upgrade deadline functionality should result in the verified mode's """ Disabling the dynamic upgrade deadline functionality should result in the verified mode's
expiration date being returned. """ expiration date being returned. """
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=False)
course = self.create_self_paced_course_run() course = self.create_self_paced_course_run()
CourseScheduleConfiguration.objects.create(course_id=course.id, enabled=False)
expected = CourseMode.objects.get(course_id=course.id, mode_slug=CourseMode.VERIFIED).expiration_datetime expected = CourseMode.objects.get(course_id=course.id, mode_slug=CourseMode.VERIFIED).expiration_datetime
self.assert_upgrade_deadline(course, expected) self.assert_upgrade_deadline(course, expected)
def test_date_with_self_paced_with_course_opt_out(self):
""" If the course run has opted out of the dynamic deadline, the course mode's deadline should be used. """
course = self.create_self_paced_course_run(days_till_start=-1)
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
CourseDynamicUpgradeDeadlineConfiguration.objects.create(enabled=True, course_id=course.id, opt_out=True)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
block = VerifiedUpgradeDeadlineDate(course, enrollment.user)
expected = CourseMode.objects.get(course_id=course.id, mode_slug=CourseMode.VERIFIED).expiration_datetime
self.assertEqual(block.date, expected)
...@@ -6,25 +6,23 @@ from django.dispatch import receiver ...@@ -6,25 +6,23 @@ from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from course_modes.models import CourseMode from course_modes.models import CourseMode
from courseware.models import DynamicUpgradeDeadlineConfiguration, CourseDynamicUpgradeDeadlineConfiguration from courseware.models import CourseScheduleConfiguration
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
from student.models import CourseEnrollment from student.models import CourseEnrollment
from .models import Schedule from .models import Schedule
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def _get_upgrade_deadline(enrollment): def _get_upgrade_deadline(schedule_config, enrollment):
""" Returns the upgrade deadline for the given enrollment. """ Returns the upgrade deadline for the given enrollment.
The deadline is determined based on the following data (in priority order): The deadline is determined based on the following data (in priority order):
1. Course run-specific deadline configuration (CourseDynamicUpgradeDeadlineConfiguration) 1. Course run-specific schedule configuration (CourseScheduleConfiguration)
2. Global deadline configuration (DynamicUpgradeDeadlineConfiguration) 2. Verified course mode expiration
3. Verified course mode expiration
""" """
course_key = enrollment.course_id course_key = enrollment.course_id
upgrade_deadline = None upgrade_deadline = datetime.date.max
try: try:
verified_mode = CourseMode.verified_mode_for_course(course_key) verified_mode = CourseMode.verified_mode_for_course(course_key)
...@@ -33,17 +31,7 @@ def _get_upgrade_deadline(enrollment): ...@@ -33,17 +31,7 @@ def _get_upgrade_deadline(enrollment):
except CourseMode.DoesNotExist: except CourseMode.DoesNotExist:
pass pass
global_config = DynamicUpgradeDeadlineConfiguration.current() delta = schedule_config.verified_upgrade_deadline_days
if global_config.enabled:
delta = global_config.deadline_days
# Check if the given course has opted out of the feature
course_config = CourseDynamicUpgradeDeadlineConfiguration.current(course_key)
if course_config.enabled:
if course_config.opt_out:
return upgrade_deadline
delta = course_config.deadline_days
course_overview = CourseOverview.get_from_id(course_key) course_overview = CourseOverview.get_from_id(course_key)
...@@ -52,19 +40,15 @@ def _get_upgrade_deadline(enrollment): ...@@ -52,19 +40,15 @@ def _get_upgrade_deadline(enrollment):
content_availability_date = max(enrollment.created, course_overview.start) content_availability_date = max(enrollment.created, course_overview.start)
cav_based_deadline = content_availability_date + datetime.timedelta(days=delta) cav_based_deadline = content_availability_date + datetime.timedelta(days=delta)
# If the deadline from above is None, make sure we have a value for comparison
upgrade_deadline = upgrade_deadline or datetime.date.max
# The content availability-based deadline should never occur after the verified mode's # The content availability-based deadline should never occur after the verified mode's
# expiration date, if one is set. # expiration date, if one is set.
upgrade_deadline = min(upgrade_deadline, cav_based_deadline) return min(upgrade_deadline, cav_based_deadline)
return upgrade_deadline
@receiver(post_save, sender=CourseEnrollment, dispatch_uid='create_schedule_for_enrollment') @receiver(post_save, sender=CourseEnrollment, dispatch_uid='create_schedule_for_enrollment')
def create_schedule(sender, **kwargs): def create_schedule(sender, **kwargs):
if WaffleSwitchNamespace('schedules').is_enabled('enable-create-schedule-receiver') and kwargs['created']:
enrollment = kwargs['instance'] enrollment = kwargs['instance']
upgrade_deadline = _get_upgrade_deadline(enrollment) schedule_config = CourseScheduleConfiguration.current(enrollment.course_id)
if schedule_config.enabled and kwargs['created']:
upgrade_deadline = _get_upgrade_deadline(schedule_config, enrollment)
Schedule.objects.create(enrollment=enrollment, start=timezone.now(), upgrade_deadline=upgrade_deadline) Schedule.objects.create(enrollment=enrollment, start=timezone.now(), upgrade_deadline=upgrade_deadline)
from django.test import TestCase from datetime import datetime, timedelta
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace from pytz import utc
from xmodule.modulestore.tests.factories import CourseFactory
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from courseware.models import CourseScheduleConfiguration
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 CourseEnrollmentFactory from student.tests.factories import CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from ..models import Schedule from ..models import Schedule
@skip_unless_lms @skip_unless_lms
class CreateScheduleTests(TestCase): class CreateScheduleTests(ModuleStoreTestCase):
def test_create_schedule(self):
""" A schedule should be created for every new enrollment if the switch is active. """
SWITCH_NAME = 'enable-create-schedule-receiver' def create_course_run(self):
switch_namesapce = WaffleSwitchNamespace('schedules') now = datetime.now(utc)
course = CourseFactory.create(start=now + timedelta(days=-1))
with switch_namesapce.override(SWITCH_NAME, True): CourseModeFactory(course_id=course.id, mode_slug=CourseMode.AUDIT)
enrollment = CourseEnrollmentFactory() CourseModeFactory(
self.assertIsNotNone(enrollment.schedule) course_id=course.id,
mode_slug=CourseMode.VERIFIED,
expiration_datetime=now + timedelta(days=30)
)
return course
def test_not_create_schedule(self):
course = self.create_course_run()
CourseScheduleConfiguration.objects.create(course_id=course.id, enabled=False)
with switch_namesapce.override(SWITCH_NAME, False): enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
enrollment = CourseEnrollmentFactory()
with self.assertRaises(Schedule.DoesNotExist): with self.assertRaises(Schedule.DoesNotExist):
enrollment.schedule enrollment.schedule
def test_create_schedule(self):
course = self.create_course_run()
CourseScheduleConfiguration.objects.create(course_id=course.id, enabled=True)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
self.assertIsNotNone(enrollment.schedule)
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