From 4805946a83170a3eb3fab8c8df91fb43b6f60f4a Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Fri, 9 Oct 2015 14:29:51 -0400 Subject: [PATCH] Override due dates in the LMS for self-paced courses. --- lms/djangoapps/ccx/tests/test_overrides.py | 9 +--- lms/djangoapps/courseware/field_overrides.py | 13 ++--- .../courseware/self_paced_overrides.py | 23 +++++++++ .../courseware/tests/test_field_overrides.py | 13 +++++ .../tests/test_self_paced_overrides.py | 49 +++++++++++++++++++ lms/djangoapps/instructor/tests/test_tools.py | 9 +--- lms/envs/aws.py | 6 +++ 7 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 lms/djangoapps/courseware/self_paced_overrides.py create mode 100644 lms/djangoapps/courseware/tests/test_self_paced_overrides.py diff --git a/lms/djangoapps/ccx/tests/test_overrides.py b/lms/djangoapps/ccx/tests/test_overrides.py index dafd442872..d88a8d4c35 100644 --- a/lms/djangoapps/ccx/tests/test_overrides.py +++ b/lms/djangoapps/ccx/tests/test_overrides.py @@ -9,6 +9,7 @@ from nose.plugins.attrib import attr from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error from django.test.utils import override_settings +from lms.djangoapps.courseware.tests.test_field_overrides import inject_field_overrides from request_cache.middleware import RequestCache from student.tests.factories import AdminFactory # pylint: disable=import-error from xmodule.modulestore.tests.django_utils import ( @@ -69,13 +70,7 @@ class TestFieldOverrides(ModuleStoreTestCase): self.addCleanup(RequestCache.clear_request_cache) - # Apparently the test harness doesn't use LmsFieldStorage, and I'm not - # sure if there's a way to poke the test harness to do so. So, we'll - # just inject the override field storage in this brute force manner. - OverrideFieldData.provider_classes = None - for block in iter_blocks(ccx.course): - block._field_data = OverrideFieldData.wrap( # pylint: disable=protected-access - AdminFactory.create(), course, block._field_data) # pylint: disable=protected-access + inject_field_overrides(iter_blocks(ccx.course), course, AdminFactory.create()) def cleanup_provider_classes(): """ diff --git a/lms/djangoapps/courseware/field_overrides.py b/lms/djangoapps/courseware/field_overrides.py index e5e50fd7de..44207e1e71 100644 --- a/lms/djangoapps/courseware/field_overrides.py +++ b/lms/djangoapps/courseware/field_overrides.py @@ -24,7 +24,7 @@ from xblock.field_data import FieldData from xmodule.modulestore.inheritance import InheritanceMixin NOTSET = object() -ENABLED_OVERRIDE_PROVIDERS_KEY = "courseware.field_overrides.enabled_providers" +ENABLED_OVERRIDE_PROVIDERS_KEY = "courseware.field_overrides.enabled_providers.{course_id}" def resolve_dotted(name): @@ -77,7 +77,6 @@ class OverrideFieldData(FieldData): settings.FIELD_OVERRIDE_PROVIDERS)) enabled_providers = cls._providers_for_course(course) - if enabled_providers: # TODO: we might not actually want to return here. Might be better # to check for instance.providers after the instance is built. This @@ -98,14 +97,16 @@ class OverrideFieldData(FieldData): course: The course XBlock """ request_cache = RequestCache.get_request_cache() - enabled_providers = request_cache.data.get( - ENABLED_OVERRIDE_PROVIDERS_KEY, NOTSET - ) + if course is None: + cache_key = ENABLED_OVERRIDE_PROVIDERS_KEY.format(course_id='None') + else: + cache_key = ENABLED_OVERRIDE_PROVIDERS_KEY.format(course_id=unicode(course.id)) + enabled_providers = request_cache.data.get(cache_key, NOTSET) if enabled_providers == NOTSET: enabled_providers = tuple( (provider_class for provider_class in cls.provider_classes if provider_class.enabled_for(course)) ) - request_cache.data[ENABLED_OVERRIDE_PROVIDERS_KEY] = enabled_providers + request_cache.data[cache_key] = enabled_providers return enabled_providers diff --git a/lms/djangoapps/courseware/self_paced_overrides.py b/lms/djangoapps/courseware/self_paced_overrides.py new file mode 100644 index 0000000000..abd7b389c2 --- /dev/null +++ b/lms/djangoapps/courseware/self_paced_overrides.py @@ -0,0 +1,23 @@ +""" +Field overrides for self-paced courses. This allows overriding due +dates for each block in the course. +""" + +from .field_overrides import FieldOverrideProvider + + +class SelfPacedDateOverrideProvider(FieldOverrideProvider): + """ + A concrete implementation of + :class:`~courseware.field_overrides.FieldOverrideProvider` which allows for + due dates to be overridden for self-paced courses. + """ + def get(self, block, name, default): + if name == 'due': + return None + return default + + @classmethod + def enabled_for(cls, course): + """This provider is enabled for self-paced courses only.""" + return course.self_paced diff --git a/lms/djangoapps/courseware/tests/test_field_overrides.py b/lms/djangoapps/courseware/tests/test_field_overrides.py index 89e98fe192..24f6d61859 100644 --- a/lms/djangoapps/courseware/tests/test_field_overrides.py +++ b/lms/djangoapps/courseware/tests/test_field_overrides.py @@ -132,3 +132,16 @@ class TestOverrideProvider(FieldOverrideProvider): @classmethod def enabled_for(cls, course): return True + + +def inject_field_overrides(blocks, course, user): + """ + Apparently the test harness doesn't use LmsFieldStorage, and I'm + not sure if there's a way to poke the test harness to do so. So, + we'll just inject the override field storage in this brute force + manner. + """ + OverrideFieldData.provider_classes = None + for block in blocks: + block._field_data = OverrideFieldData.wrap( # pylint: disable=protected-access + user, course, block._field_data) # pylint: disable=protected-access diff --git a/lms/djangoapps/courseware/tests/test_self_paced_overrides.py b/lms/djangoapps/courseware/tests/test_self_paced_overrides.py new file mode 100644 index 0000000000..e0df82b6e6 --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_self_paced_overrides.py @@ -0,0 +1,49 @@ +""" +Tests for self-paced course due date overrides. +""" + +from datetime import datetime +from dateutil.tz import tzutc +from django.test.utils import override_settings + +from student.tests.factories import UserFactory +from lms.djangoapps.ccx.tests.test_overrides import inject_field_overrides +from lms.djangoapps.courseware.field_overrides import OverrideFieldData +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + + +@override_settings( + FIELD_OVERRIDE_PROVIDERS=('courseware.self_paced_overrides.SelfPacedDateOverrideProvider',) +) +class SelfPacedDateOverrideTest(ModuleStoreTestCase): + """ + Tests for self-paced due date overrides. + """ + + def setUp(self): + super(SelfPacedDateOverrideTest, self).setUp() + self.due_date = datetime(2015, 5, 26, 8, 30, 00).replace(tzinfo=tzutc()) + self.instructor_led_course, self.il_section = self.setup_course("Instructor Led Course", False) + self.self_paced_course, self.sp_section = self.setup_course("Self-Paced Course", True) + + def tearDown(self): + super(SelfPacedDateOverrideTest, self).tearDown() + OverrideFieldData.provider_classes = None + + def setup_course(self, display_name, self_paced): + """Set up a course with `display_name` and `self_paced` attributes. + + Creates a child block with a due date, and ensures that field + overrides are correctly applied for both blocks. + """ + course = CourseFactory.create(display_name=display_name, self_paced=self_paced) + section = ItemFactory.create(parent=course, due=self.due_date) + inject_field_overrides((course, section), course, UserFactory.create()) + return (course, section) + + def test_instructor_led(self): + self.assertEqual(self.due_date, self.il_section.due) + + def test_self_paced(self): + self.assertIsNone(self.sp_section.due) diff --git a/lms/djangoapps/instructor/tests/test_tools.py b/lms/djangoapps/instructor/tests/test_tools.py index 54068d4ce2..9ea90f6773 100644 --- a/lms/djangoapps/instructor/tests/test_tools.py +++ b/lms/djangoapps/instructor/tests/test_tools.py @@ -12,6 +12,7 @@ from django.test.utils import override_settings from nose.plugins.attrib import attr from courseware.field_overrides import OverrideFieldData # pylint: disable=import-error +from lms.djangoapps.ccx.tests.test_overrides import inject_field_overrides from student.tests.factories import UserFactory # pylint: disable=import-error from xmodule.fields import Date from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase @@ -196,7 +197,6 @@ class TestSetDueDateExtension(ModuleStoreTestCase): Fixtures. """ super(TestSetDueDateExtension, self).setUp() - OverrideFieldData.provider_classes = None self.due = due = datetime.datetime(2010, 5, 12, 2, 42, tzinfo=utc) course = CourseFactory.create() @@ -216,12 +216,7 @@ class TestSetDueDateExtension(ModuleStoreTestCase): self.week3 = week3 self.user = user - # Apparently the test harness doesn't use LmsFieldStorage, and I'm not - # sure if there's a way to poke the test harness to do so. So, we'll - # just inject the override field storage in this brute force manner. - for block in (course, week1, week2, week3, homework, assignment): - block._field_data = OverrideFieldData.wrap( # pylint: disable=protected-access - user, course, block._field_data) # pylint: disable=protected-access + inject_field_overrides((course, week1, week2, week3, homework, assignment), course, user) def tearDown(self): super(TestSetDueDateExtension, self).tearDown() diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 30cac67656..3d3008c223 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -676,6 +676,12 @@ if FEATURES.get('INDIVIDUAL_DUE_DATES'): 'courseware.student_field_overrides.IndividualStudentOverrideProvider', ) +##### Self-Paced Course Due Dates ##### +if FEATURES.get('ENABLE_SELF_PACED_COURSES'): + FIELD_OVERRIDE_PROVIDERS += ( + 'courseware.self_paced_overrides.SelfPacedDateOverrideProvider', + ) + # PROFILE IMAGE CONFIG PROFILE_IMAGE_BACKEND = ENV_TOKENS.get('PROFILE_IMAGE_BACKEND', PROFILE_IMAGE_BACKEND) PROFILE_IMAGE_SECRET_KEY = AUTH_TOKENS.get('PROFILE_IMAGE_SECRET_KEY', PROFILE_IMAGE_SECRET_KEY)