Commit 52897d58 by Tyler Hallada Committed by GitHub

Merge pull request #16346 from edx/thallada/ret-org-opt-out

Org-level schedule upgrade deadline opt-out
parents 6b37218e 05dd63e8
......@@ -50,7 +50,11 @@ import request_cache
from student.signals import UNENROLL_DONE, ENROLL_STATUS_CHANGE, REFUND_ORDER, ENROLLMENT_TRACK_UPDATED
from certificates.models import GeneratedCertificate
from course_modes.models import CourseMode
from courseware.models import DynamicUpgradeDeadlineConfiguration, CourseDynamicUpgradeDeadlineConfiguration
from courseware.models import (
CourseDynamicUpgradeDeadlineConfiguration,
DynamicUpgradeDeadlineConfiguration,
OrgDynamicUpgradeDeadlineConfiguration
)
from enrollment.api import _default_course_mode
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
......@@ -1740,7 +1744,12 @@ class CourseEnrollment(models.Model):
return None
course_config = CourseDynamicUpgradeDeadlineConfiguration.current(self.course_id)
if course_config.enabled and course_config.opt_out:
if course_config.opted_out():
# Course-level config should be checked first since it overrides the org-level config
return None
org_config = OrgDynamicUpgradeDeadlineConfiguration.current(self.course_id.org)
if org_config.opted_out() and not course_config.opted_in():
return None
try:
......
......@@ -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.OrgDynamicUpgradeDeadlineConfiguration, 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
from django.conf import settings
import courseware.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('courseware', '0004_auto_20171010_1639'),
]
operations = [
migrations.CreateModel(
name='OrgDynamicUpgradeDeadlineConfiguration',
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')),
('org_id', models.CharField(max_length=255, db_index=True)),
('deadline_days', models.PositiveSmallIntegerField(default=21, help_text='Number of days a learner has to upgrade after content is made available')),
('opt_out', models.BooleanField(default=False, help_text='Disable the dynamic upgrade deadline for this organization.')),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
],
options={
'ordering': ('-change_date',),
'abstract': False,
},
bases=(courseware.models.OptOutDynamicUpgradeDeadlineMixin, models.Model),
),
migrations.AlterModelOptions(
name='coursedynamicupgradedeadlineconfiguration',
options={'ordering': ('-change_date',)},
),
]
......@@ -379,24 +379,59 @@ class DynamicUpgradeDeadlineConfiguration(ConfigurationModel):
)
class CourseDynamicUpgradeDeadlineConfiguration(ConfigurationModel):
class OptOutDynamicUpgradeDeadlineMixin(object):
"""
Provides convenience methods for interpreting the enabled and opt out status.
"""
def opted_in(self):
"""Convenience function that returns True if this config model is both enabled and opt_out is False"""
return self.enabled and not self.opt_out
def opted_out(self):
"""Convenience function that returns True if this config model is both enabled and opt_out is True"""
return self.enabled and self.opt_out
class CourseDynamicUpgradeDeadlineConfiguration(OptOutDynamicUpgradeDeadlineMixin, 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)
deadline_days = models.PositiveSmallIntegerField(
default=21,
help_text=_('Number of days a learner has to upgrade after content is made available')
)
opt_out = models.BooleanField(
default=False,
help_text=_('Disable the dynamic upgrade deadline for this course run.')
)
class OrgDynamicUpgradeDeadlineConfiguration(OptOutDynamicUpgradeDeadlineMixin, ConfigurationModel):
"""
Per-org configuration for dynamic upgrade deadlines.
This model controls dynamic upgrade deadlines on a per-org level, allowing organizations to
have different deadlines or opt out of the functionality altogether.
"""
KEY_FIELDS = ('org_id',)
org_id = models.CharField(max_length=255, db_index=True)
deadline_days = models.PositiveSmallIntegerField(
default=21,
help_text=_('Number of days a learner has to upgrade after content is made available')
)
opt_out = models.BooleanField(
default=False,
help_text=_('Disable the dynamic upgrade deadline for this organization.')
)
......@@ -24,10 +24,15 @@ from courseware.date_summary import (
VerifiedUpgradeDeadlineDate,
CertificateAvailableDate
)
from courseware.models import DynamicUpgradeDeadlineConfiguration, CourseDynamicUpgradeDeadlineConfiguration
from courseware.models import (
CourseDynamicUpgradeDeadlineConfiguration,
DynamicUpgradeDeadlineConfiguration,
OrgDynamicUpgradeDeadlineConfiguration
)
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
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.schedules.signals import CREATE_SCHEDULE_WAFFLE_FLAG
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
......@@ -562,6 +567,7 @@ class TestDateAlerts(SharedModuleStoreTestCase):
self.assertEqual(len(messages), 0)
@ddt.ddt
@attr(shard=1)
class TestScheduleOverrides(SharedModuleStoreTestCase):
......@@ -599,15 +605,28 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
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. """
enrollment date.
Additionally, OrgDynamicUpgradeDeadlineConfiguration should override the number of days until the deadline,
and CourseDynamicUpgradeDeadlineConfiguration should override the org-level override.
"""
global_config = DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
course = create_self_paced_course_run(days_till_start=-1)
course = create_self_paced_course_run(days_till_start=-1, org_id='TestOrg')
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
block = VerifiedUpgradeDeadlineDate(course, enrollment.user)
expected = enrollment.created + timedelta(days=global_config.deadline_days)
self.assertEqual(block.date, expected)
# Courses should be able to override the deadline
# Orgs should be able to override the deadline
org_config = OrgDynamicUpgradeDeadlineConfiguration.objects.create(
enabled=True, org_id=course.org, deadline_days=4
)
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT)
block = VerifiedUpgradeDeadlineDate(course, enrollment.user)
expected = enrollment.created + timedelta(days=org_config.deadline_days)
self.assertEqual(block.date, expected)
# Courses should be able to override the deadline (and the org-level override)
course_config = CourseDynamicUpgradeDeadlineConfiguration.objects.create(
enabled=True, course_id=course.id, deadline_days=3
)
......@@ -650,6 +669,68 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
block = VerifiedUpgradeDeadlineDate(course, enrollment.user)
self.assertEqual(block.date, expected)
@ddt.data(
# (enroll before configs, org enabled, org opt-out, course enabled, course opt-out, expected dynamic deadline)
(False, False, False, False, False, True),
(False, False, False, False, True, True),
(False, False, False, True, False, True),
(False, False, False, True, True, False),
(False, False, True, False, False, True),
(False, False, True, False, True, True),
(False, False, True, True, False, True),
(False, False, True, True, True, False),
(False, True, False, False, False, True),
(False, True, False, False, True, True),
(False, True, False, True, False, True),
(False, True, False, True, True, False), # course-level overrides org-level
(False, True, True, False, False, False),
(False, True, True, False, True, False),
(False, True, True, True, False, True), # course-level overrides org-level
(False, True, True, True, True, False),
(True, False, False, False, False, True),
(True, False, False, False, True, True),
(True, False, False, True, False, True),
(True, False, False, True, True, False),
(True, False, True, False, False, True),
(True, False, True, False, True, True),
(True, False, True, True, False, True),
(True, False, True, True, True, False),
(True, True, False, False, False, True),
(True, True, False, False, True, True),
(True, True, False, True, False, True),
(True, True, False, True, True, False), # course-level overrides org-level
(True, True, True, False, False, False),
(True, True, True, False, True, False),
(True, True, True, True, False, True), # course-level overrides org-level
(True, True, True, True, True, False),
)
@ddt.unpack
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
def test_date_with_org_and_course_config_overrides(self, enroll_first, org_config_enabled, org_config_opt_out,
course_config_enabled, course_config_opt_out,
expected_dynamic_deadline):
""" Runs through every combination of org-level plus course-level DynamicUpgradeDeadlineConfiguration enabled
and opt-out states to verify that course-level overrides the org-level config. """
course = create_self_paced_course_run(days_till_start=-1, org_id='TestOrg')
DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True)
if enroll_first:
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT, course__self_paced=True)
OrgDynamicUpgradeDeadlineConfiguration.objects.create(
enabled=org_config_enabled, opt_out=org_config_opt_out, org_id=course.id.org
)
CourseDynamicUpgradeDeadlineConfiguration.objects.create(
enabled=course_config_enabled, opt_out=course_config_opt_out, course_id=course.id
)
if not enroll_first:
enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT, course__self_paced=True)
# The enrollment has a schedule, and the upgrade_deadline is set when expected_dynamic_deadline is True
if not enroll_first:
self.assertEqual(enrollment.schedule.upgrade_deadline is not None, expected_dynamic_deadline)
# The CourseEnrollment.upgrade_deadline property method is checking the configs
self.assertEqual(enrollment.dynamic_upgrade_deadline is not None, expected_dynamic_deadline)
def create_user(verification_status=None):
""" Create a new User instance.
......@@ -705,7 +786,7 @@ def create_course_run(
return course
def create_self_paced_course_run(days_till_start=1):
def create_self_paced_course_run(days_till_start=1, org_id=None):
""" Create a new course run and course modes.
All date-related arguments are relative to the current date-time (now) unless otherwise specified.
......@@ -714,9 +795,11 @@ def create_self_paced_course_run(days_till_start=1):
Arguments:
days_till_start (int): Number of days until the course starts.
org_id (string): String org id to assign the course to (default: None; use CourseFactory default)
"""
now = datetime.now(utc)
course = CourseFactory.create(start=now + timedelta(days=days_till_start), self_paced=True)
course = CourseFactory.create(start=now + timedelta(days=days_till_start), self_paced=True,
org=org_id if org_id else 'TestedX')
CourseModeFactory(
course_id=course.id,
......
......@@ -139,6 +139,7 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
bins_in_use = frozenset((self._calculate_bin_for_user(s.enrollment.user)) for s in schedules)
is_first_match = True
course_switch_queries = len(set(s.enrollment.course.id for s in schedules))
org_switch_queries = len(set(s.enrollment.course.id.org for s in schedules))
test_datetime = upgrade_deadline
test_datetime_str = serialize(test_datetime)
......@@ -151,7 +152,7 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
# Since this is the first match, we need to cache all of the config models, so we run a query
# for each of those...
NUM_QUERIES_FIRST_MATCH
+ course_switch_queries
+ course_switch_queries + org_switch_queries
)
is_first_match = False
else:
......@@ -257,7 +258,8 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
test_datetime_str = serialize(test_datetime)
course_switch_queries = 1
expected_queries = NUM_QUERIES_FIRST_MATCH + course_switch_queries
org_switch_queries = 1
expected_queries = NUM_QUERIES_FIRST_MATCH + course_switch_queries + org_switch_queries
if not this_org_list:
expected_queries += NUM_QUERIES_NO_ORG_LIST
......@@ -283,11 +285,14 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
for course_num in (1, 2, 3)
]
num_courses = len(set(s.enrollment.course.id for s in schedules))
course_switch_queries = len(set(s.enrollment.course.id for s in schedules))
org_switch_queries = len(set(s.enrollment.course.id.org for s in schedules))
test_datetime = datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC)
test_datetime_str = serialize(test_datetime)
expected_query_count = NUM_QUERIES_FIRST_MATCH + num_courses + NUM_QUERIES_NO_ORG_LIST
expected_query_count = (
NUM_QUERIES_FIRST_MATCH + course_switch_queries + org_switch_queries + NUM_QUERIES_NO_ORG_LIST
)
with self.assertNumQueries(expected_query_count, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleUpgradeReminder.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=2,
......@@ -320,7 +325,8 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
expiration_datetime=future_datetime
)
num_courses = len(set(s.enrollment.course.id for s in schedules))
course_switch_queries = len(set(s.enrollment.course.id for s in schedules))
org_switch_queries = len(set(s.enrollment.course.id.org for s in schedules))
test_datetime = future_datetime
test_datetime_str = serialize(test_datetime)
......@@ -339,7 +345,9 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args)
# we execute one query per course to see if it's opted out of dynamic upgrade deadlines
num_expected_queries = NUM_QUERIES_FIRST_MATCH + NUM_QUERIES_NO_ORG_LIST + num_courses
num_expected_queries = (
NUM_QUERIES_FIRST_MATCH + NUM_QUERIES_NO_ORG_LIST + course_switch_queries + org_switch_queries
)
with self.assertNumQueries(num_expected_queries, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleUpgradeReminder.apply(kwargs=dict(
......
......@@ -6,7 +6,11 @@ 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 (
CourseDynamicUpgradeDeadlineConfiguration,
DynamicUpgradeDeadlineConfiguration,
OrgDynamicUpgradeDeadlineConfiguration
)
from edx_ace.utils import date
from openedx.core.djangoapps.signals.signals import COURSE_START_DATE_CHANGED
from openedx.core.djangoapps.theming.helpers import get_current_site
......@@ -110,9 +114,18 @@ def _get_upgrade_deadline_delta_setting(course_id):
# Use the default from this model whether or not the feature is enabled
delta = global_config.deadline_days
# Check if the org has a deadline
org_config = OrgDynamicUpgradeDeadlineConfiguration.current(course_id.org)
if org_config.opted_in():
delta = org_config.deadline_days
elif org_config.opted_out():
delta = None
# Check if the course has a deadline
course_config = CourseDynamicUpgradeDeadlineConfiguration.current(course_id)
if course_config.enabled and not course_config.opt_out:
if course_config.opted_in():
delta = course_config.deadline_days
elif course_config.opted_out():
delta = None
return delta
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