change the config model used to configure schedules

parent 50e04d5f
......@@ -50,6 +50,7 @@ import lms.lib.comment_client as cc
import request_cache
from certificates.models import GeneratedCertificate
from course_modes.models import CourseMode
from courseware.models import CourseScheduleConfiguration
from enrollment.api import _default_course_mode
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
......@@ -1707,8 +1708,9 @@ class CourseEnrollment(models.Model):
)
return None
schedule_config = CourseScheduleConfiguration.current(self.course_id)
try:
if self.schedule:
if schedule_config.enabled and schedule_config.verified_upgrade_deadline_enabled and self.schedule:
log.debug(
'Schedules: Pulling upgrade deadline for CourseEnrollment %d from Schedule %d.',
self.id, self.schedule.id
......
......@@ -8,4 +8,5 @@ admin.site.register(models.DynamicUpgradeDeadlineConfiguration, ConfigurationMod
admin.site.register(models.OfflineComputedGrade)
admin.site.register(models.OfflineComputedGradeLog)
admin.site.register(models.CourseDynamicUpgradeDeadlineConfiguration, KeyedConfigurationModelAdmin)
admin.site.register(models.CourseScheduleConfiguration, KeyedConfigurationModelAdmin)
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):
default=False,
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 (
VerifiedUpgradeDeadlineDate,
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.tests.factories import SoftwareSecurePhotoVerificationFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
......@@ -36,7 +36,6 @@ from xmodule.modulestore.tests.factories import CourseFactory
@attr(shard=1)
@ddt.ddt
@waffle.testutils.override_switch('schedules.enable-create-schedule-receiver', True)
class CourseDateSummaryTest(SharedModuleStoreTestCase):
"""Tests for course date summary blocks."""
......@@ -480,46 +479,37 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
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
course start date. """
global_config = DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
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)
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)
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
enrollment date. """
global_config = DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
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)
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)
def test_date_with_self_paced_with_enrollment_after_course_start_days_configured(self):
# Courses should be able to override the deadline
course_config = CourseDynamicUpgradeDeadlineConfiguration.objects.create(
enabled=True, course_id=course.id, opt_out=False, deadline_days=3
course = self.create_self_paced_course_run(days_till_start=-1)
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)
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)
def test_date_with_self_paced_without_dynamic_upgrade_deadline(self):
""" Disabling the dynamic upgrade deadline functionality should result in the verified mode's
expiration date being returned. """
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=False)
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
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
from django.utils import timezone
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.waffle_utils import WaffleSwitchNamespace
from student.models import CourseEnrollment
from .models import Schedule
log = logging.getLogger(__name__)
def _get_upgrade_deadline(enrollment):
def _get_upgrade_deadline(schedule_config, enrollment):
""" Returns the upgrade deadline for the given enrollment.
The deadline is determined based on the following data (in priority order):
1. Course run-specific deadline configuration (CourseDynamicUpgradeDeadlineConfiguration)
2. Global deadline configuration (DynamicUpgradeDeadlineConfiguration)
3. Verified course mode expiration
1. Course run-specific schedule configuration (CourseScheduleConfiguration)
2. Verified course mode expiration
"""
course_key = enrollment.course_id
upgrade_deadline = None
upgrade_deadline = datetime.date.max
try:
verified_mode = CourseMode.verified_mode_for_course(course_key)
......@@ -33,38 +31,24 @@ def _get_upgrade_deadline(enrollment):
except CourseMode.DoesNotExist:
pass
global_config = DynamicUpgradeDeadlineConfiguration.current()
if global_config.enabled:
delta = global_config.deadline_days
delta = schedule_config.verified_upgrade_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
course_overview = CourseOverview.get_from_id(course_key)
delta = course_config.deadline_days
# This represents the first date at which the learner can access the content. This will be the latter of
# either the enrollment date or the course's start date.
content_availability_date = max(enrollment.created, course_overview.start)
cav_based_deadline = content_availability_date + datetime.timedelta(days=delta)
course_overview = CourseOverview.get_from_id(course_key)
# This represents the first date at which the learner can access the content. This will be the latter of
# either the enrollment date or the course's start date.
content_availability_date = max(enrollment.created, course_overview.start)
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
# expiration date, if one is set.
upgrade_deadline = min(upgrade_deadline, cav_based_deadline)
return upgrade_deadline
# The content availability-based deadline should never occur after the verified mode's
# expiration date, if one is set.
return min(upgrade_deadline, cav_based_deadline)
@receiver(post_save, sender=CourseEnrollment, dispatch_uid='create_schedule_for_enrollment')
def create_schedule(sender, **kwargs):
if WaffleSwitchNamespace('schedules').is_enabled('enable-create-schedule-receiver') and kwargs['created']:
enrollment = kwargs['instance']
upgrade_deadline = _get_upgrade_deadline(enrollment)
enrollment = kwargs['instance']
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)
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 student.tests.factories import CourseEnrollmentFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from ..models import Schedule
@skip_unless_lms
class CreateScheduleTests(TestCase):
def test_create_schedule(self):
""" A schedule should be created for every new enrollment if the switch is active. """
class CreateScheduleTests(ModuleStoreTestCase):
def create_course_run(self):
now = datetime.now(utc)
course = CourseFactory.create(start=now + timedelta(days=-1))
SWITCH_NAME = 'enable-create-schedule-receiver'
switch_namesapce = WaffleSwitchNamespace('schedules')
CourseModeFactory(course_id=course.id, mode_slug=CourseMode.AUDIT)
CourseModeFactory(
course_id=course.id,
mode_slug=CourseMode.VERIFIED,
expiration_datetime=now + timedelta(days=30)
)
return course
with switch_namesapce.override(SWITCH_NAME, True):
enrollment = CourseEnrollmentFactory()
self.assertIsNotNone(enrollment.schedule)
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()
with self.assertRaises(Schedule.DoesNotExist):
enrollment.schedule
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
with self.assertRaises(Schedule.DoesNotExist):
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