Commit 806114a3 by Tyler Hallada

Check org before course deadline config w/ tests

parent 4c8b257b
...@@ -50,7 +50,11 @@ import request_cache ...@@ -50,7 +50,11 @@ import request_cache
from student.signals import UNENROLL_DONE, ENROLL_STATUS_CHANGE, REFUND_ORDER, ENROLLMENT_TRACK_UPDATED from student.signals import UNENROLL_DONE, ENROLL_STATUS_CHANGE, REFUND_ORDER, ENROLLMENT_TRACK_UPDATED
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 DynamicUpgradeDeadlineConfiguration, CourseDynamicUpgradeDeadlineConfiguration from courseware.models import (
CourseDynamicUpgradeDeadlineConfiguration,
DynamicUpgradeDeadlineConfiguration,
OrgDynamicUpgradeDeadlineConfiguration
)
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
...@@ -1740,7 +1744,12 @@ class CourseEnrollment(models.Model): ...@@ -1740,7 +1744,12 @@ class CourseEnrollment(models.Model):
return None return None
course_config = CourseDynamicUpgradeDeadlineConfiguration.current(self.course_id) 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 return None
try: try:
......
...@@ -379,47 +379,45 @@ class DynamicUpgradeDeadlineConfiguration(ConfigurationModel): ...@@ -379,47 +379,45 @@ class DynamicUpgradeDeadlineConfiguration(ConfigurationModel):
) )
class CourseDynamicUpgradeDeadlineConfiguration(ConfigurationModel): class OptOutDynamicUpgradeDeadlineConfiguration(DynamicUpgradeDeadlineConfiguration):
""" Dynamic upgrade deadline configuration with opt-out switch.
This is an abstract model that both CourseDynamicUpgradeDeadlineConfiguration and
OrgDynamicUpgradeDeadlineConfiguration inherit.
"""
opt_out = models.BooleanField(
default=False,
help_text=_('Disable the dynamic upgrade deadline for this course run.')
)
def opted_in(self):
"""Convienence 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):
"""Convienence 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(OptOutDynamicUpgradeDeadlineConfiguration):
""" """
Per-course run configuration for dynamic upgrade deadlines. Per-course run configuration for dynamic upgrade deadlines.
This model controls dynamic upgrade deadlines on a per-course run level, allowing course runs to 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. have different deadlines or opt out of the functionality altogether.
""" """
class Meta(object):
app_label = 'courseware'
KEY_FIELDS = ('course_id',) KEY_FIELDS = ('course_id',)
course_id = CourseKeyField(max_length=255, db_index=True) 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(ConfigurationModel): class OrgDynamicUpgradeDeadlineConfiguration(OptOutDynamicUpgradeDeadlineConfiguration):
""" """
Per-org configuration for dynamic upgrade deadlines. Per-org configuration for dynamic upgrade deadlines.
This model controls dynamic upgrade deadlines on a per-org level, allowing organizations to This model controls dynamic upgrade deadlines on a per-org level, allowing organizations to
have different deadlines or opt out of the functionality altogether. have different deadlines or opt out of the functionality altogether.
""" """
class Meta(object):
app_label = 'courseware'
KEY_FIELDS = ('org_id',) KEY_FIELDS = ('org_id',)
org_id = models.CharField(max_length=255, db_index=True) 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 ( ...@@ -24,10 +24,15 @@ from courseware.date_summary import (
VerifiedUpgradeDeadlineDate, VerifiedUpgradeDeadlineDate,
CertificateAvailableDate 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.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
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.schedules.signals import CREATE_SCHEDULE_WAFFLE_FLAG
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
...@@ -562,6 +567,7 @@ class TestDateAlerts(SharedModuleStoreTestCase): ...@@ -562,6 +567,7 @@ class TestDateAlerts(SharedModuleStoreTestCase):
self.assertEqual(len(messages), 0) self.assertEqual(len(messages), 0)
@ddt.ddt
@attr(shard=1) @attr(shard=1)
class TestScheduleOverrides(SharedModuleStoreTestCase): class TestScheduleOverrides(SharedModuleStoreTestCase):
...@@ -599,15 +605,28 @@ class TestScheduleOverrides(SharedModuleStoreTestCase): ...@@ -599,15 +605,28 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
@override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True) @override_waffle_flag(CREATE_SCHEDULE_WAFFLE_FLAG, True)
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.
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) 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) 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=global_config.deadline_days)
self.assertEqual(block.date, expected) 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( course_config = CourseDynamicUpgradeDeadlineConfiguration.objects.create(
enabled=True, course_id=course.id, deadline_days=3 enabled=True, course_id=course.id, deadline_days=3
) )
...@@ -650,6 +669,68 @@ class TestScheduleOverrides(SharedModuleStoreTestCase): ...@@ -650,6 +669,68 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
block = VerifiedUpgradeDeadlineDate(course, enrollment.user) block = VerifiedUpgradeDeadlineDate(course, enrollment.user)
self.assertEqual(block.date, expected) 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): def create_user(verification_status=None):
""" Create a new User instance. """ Create a new User instance.
...@@ -705,7 +786,7 @@ def create_course_run( ...@@ -705,7 +786,7 @@ def create_course_run(
return course 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. """ Create a new course run and course modes.
All date-related arguments are relative to the current date-time (now) unless otherwise specified. 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): ...@@ -714,9 +795,11 @@ def create_self_paced_course_run(days_till_start=1):
Arguments: Arguments:
days_till_start (int): Number of days until the course starts. 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) 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( CourseModeFactory(
course_id=course.id, course_id=course.id,
......
...@@ -139,6 +139,7 @@ class TestUpgradeReminder(SharedModuleStoreTestCase): ...@@ -139,6 +139,7 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
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)
is_first_match = True is_first_match = True
course_switch_queries = 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 = upgrade_deadline test_datetime = upgrade_deadline
test_datetime_str = serialize(test_datetime) test_datetime_str = serialize(test_datetime)
...@@ -151,7 +152,7 @@ class TestUpgradeReminder(SharedModuleStoreTestCase): ...@@ -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 # Since this is the first match, we need to cache all of the config models, so we run a query
# for each of those... # for each of those...
NUM_QUERIES_FIRST_MATCH NUM_QUERIES_FIRST_MATCH
+ course_switch_queries + course_switch_queries + org_switch_queries
) )
is_first_match = False is_first_match = False
else: else:
...@@ -257,7 +258,8 @@ class TestUpgradeReminder(SharedModuleStoreTestCase): ...@@ -257,7 +258,8 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
test_datetime_str = serialize(test_datetime) test_datetime_str = serialize(test_datetime)
course_switch_queries = 1 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: if not this_org_list:
expected_queries += NUM_QUERIES_NO_ORG_LIST expected_queries += NUM_QUERIES_NO_ORG_LIST
...@@ -283,11 +285,14 @@ class TestUpgradeReminder(SharedModuleStoreTestCase): ...@@ -283,11 +285,14 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
for course_num in (1, 2, 3) 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 = datetime.datetime(2017, 8, 3, 19, 44, 30, tzinfo=pytz.UTC)
test_datetime_str = serialize(test_datetime) 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): with self.assertNumQueries(expected_query_count, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleUpgradeReminder.apply(kwargs=dict( tasks.ScheduleUpgradeReminder.apply(kwargs=dict(
site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=2, site_id=self.site_config.site.id, target_day_str=test_datetime_str, day_offset=2,
...@@ -320,7 +325,8 @@ class TestUpgradeReminder(SharedModuleStoreTestCase): ...@@ -320,7 +325,8 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
expiration_datetime=future_datetime 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 = future_datetime
test_datetime_str = serialize(test_datetime) test_datetime_str = serialize(test_datetime)
...@@ -339,7 +345,9 @@ class TestUpgradeReminder(SharedModuleStoreTestCase): ...@@ -339,7 +345,9 @@ class TestUpgradeReminder(SharedModuleStoreTestCase):
mock_schedule_send.apply_async = lambda args, *_a, **_kw: sent_messages.append(args) 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 # 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): with self.assertNumQueries(num_expected_queries, table_blacklist=WAFFLE_TABLES):
tasks.ScheduleUpgradeReminder.apply(kwargs=dict( tasks.ScheduleUpgradeReminder.apply(kwargs=dict(
......
...@@ -6,7 +6,11 @@ from django.dispatch import receiver ...@@ -6,7 +6,11 @@ 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 (
CourseDynamicUpgradeDeadlineConfiguration,
DynamicUpgradeDeadlineConfiguration,
OrgDynamicUpgradeDeadlineConfiguration
)
from edx_ace.utils import date from edx_ace.utils import date
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
...@@ -110,9 +114,18 @@ def _get_upgrade_deadline_delta_setting(course_id): ...@@ -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 # Use the default from this model whether or not the feature is enabled
delta = global_config.deadline_days 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 # Check if the course has a deadline
course_config = CourseDynamicUpgradeDeadlineConfiguration.current(course_id) 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 delta = course_config.deadline_days
elif course_config.opted_out():
delta = None
return delta 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