diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 94ec075468..ee7a6644cd 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -30,6 +30,7 @@ from contentstore.views.component import ADVANCED_COMPONENT_POLICY_KEY import ddt from xmodule.modulestore import ModuleStoreEnum +from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from util.milestones_helpers import seed_milestone_relationship_types @@ -56,6 +57,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) self.assertIsNone(details.language, "language somehow initialized" + str(details.language)) self.assertIsNone(details.has_cert_config) + self.assertFalse(details.self_paced) def test_encoder(self): details = CourseDetails.fetch(self.course.id) @@ -86,6 +88,7 @@ class CourseDetailsTestCase(CourseTestCase): self.assertEqual(jsondetails['string'], 'string') def test_update_and_fetch(self): + SelfPacedConfiguration(enabled=True).save() jsondetails = CourseDetails.fetch(self.course.id) jsondetails.syllabus = "bar" # encode - decode to convert date fields and other data which changes form @@ -113,11 +116,21 @@ class CourseDetailsTestCase(CourseTestCase): CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).effort, jsondetails.effort, "After set effort" ) + jsondetails.self_paced = True + self.assertEqual( + CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).self_paced, + jsondetails.self_paced + ) jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC()) self.assertEqual( CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).start_date, jsondetails.start_date ) + jsondetails.end_date = datetime.datetime(2011, 10, 1, 0, tzinfo=UTC()) + self.assertEqual( + CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).end_date, + jsondetails.end_date + ) jsondetails.course_image_name = "an_image.jpg" self.assertEqual( CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).course_image_name, @@ -283,6 +296,19 @@ class CourseDetailsTestCase(CourseTestCase): self.assertContains(response, "Course Introduction Video") self.assertContains(response, "Requirements") + def test_toggle_pacing_during_course_run(self): + SelfPacedConfiguration(enabled=True).save() + self.course.start = datetime.datetime.now() + modulestore().update_item(self.course, self.user.id) + + details = CourseDetails.fetch(self.course.id) + updated_details = CourseDetails.update_from_json( + self.course.id, + dict(details.__dict__, self_paced=True), + self.user + ) + self.assertFalse(updated_details.self_paced) + @ddt.ddt class CourseDetailsViewTest(CourseTestCase): @@ -314,6 +340,7 @@ class CourseDetailsViewTest(CourseTestCase): return Date().to_json(datetime_obj) def test_update_and_fetch(self): + SelfPacedConfiguration(enabled=True).save() details = CourseDetails.fetch(self.course.id) # resp s/b json from here on @@ -334,6 +361,7 @@ class CourseDetailsViewTest(CourseTestCase): self.alter_field(url, details, 'effort', "effort") self.alter_field(url, details, 'course_image_name', "course_image_name") self.alter_field(url, details, 'language', "en") + self.alter_field(url, details, 'self_paced', "true") def compare_details_with_encoding(self, encoded, details, context): """ diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 1f81fb29e2..4ff5151e16 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -28,6 +28,7 @@ from openedx.core.lib.course_tabs import CourseTabPluginManager from openedx.core.djangoapps.credit.api import is_credit_course, get_credit_requirements from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors +from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from xmodule.modulestore import EdxJSONEncoder from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError from opaque_keys import InvalidKeyError @@ -913,6 +914,9 @@ def settings_handler(request, course_key_string): about_page_editable = not marketing_site_enabled enrollment_end_editable = GlobalStaff().has_user(request.user) or not marketing_site_enabled short_description_editable = settings.FEATURES.get('EDITABLE_SHORT_DESCRIPTION', True) + + self_paced_enabled = SelfPacedConfiguration.current().enabled + settings_context = { 'context_course': course_module, 'course_locator': course_key, @@ -929,7 +933,8 @@ def settings_handler(request, course_key_string): 'show_min_grade_warning': False, 'enrollment_end_editable': enrollment_end_editable, 'is_prerequisite_courses_enabled': is_prerequisite_courses_enabled(), - 'is_entrance_exams_enabled': is_entrance_exams_enabled() + 'is_entrance_exams_enabled': is_entrance_exams_enabled(), + 'self_paced_enabled': self_paced_enabled, } if is_prerequisite_courses_enabled(): courses, in_process_course_actions = get_courses_accessible_to_user(request) diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index 4483b1145f..f09ec7b1a3 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -10,6 +10,7 @@ from opaque_keys.edx.locations import Location from xmodule.modulestore.exceptions import ItemNotFoundError from contentstore.utils import course_image_url, has_active_web_certificate from models.settings import course_grading +from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from xmodule.fields import Date from xmodule.modulestore.django import modulestore @@ -54,6 +55,7 @@ class CourseDetails(object): '50' ) # minimum passing score for entrance exam content module/tree, self.has_cert_config = None # course has active certificate configuration + self.self_paced = None @classmethod def _fetch_about_attribute(cls, course_key, attribute): @@ -86,6 +88,7 @@ class CourseDetails(object): # Default course license is "All Rights Reserved" course_details.license = getattr(descriptor, "license", "all-rights-reserved") course_details.has_cert_config = has_active_web_certificate(descriptor) + course_details.self_paced = descriptor.self_paced for attribute in ABOUT_ATTRIBUTES: value = cls._fetch_about_attribute(course_key, attribute) @@ -188,6 +191,13 @@ class CourseDetails(object): descriptor.language = jsondict['language'] dirty = True + if (SelfPacedConfiguration.current().enabled + and descriptor.can_toggle_course_pacing + and 'self_paced' in jsondict + and jsondict['self_paced'] != descriptor.self_paced): + descriptor.self_paced = jsondict['self_paced'] + dirty = True + if dirty: module_store.update_item(descriptor, user.id) diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index f8ca9c534b..ef8129ecea 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -50,6 +50,7 @@ class CourseMetadata(object): 'is_proctored_enabled', 'is_time_limited', 'is_practice_exam', + 'self_paced' ] @classmethod diff --git a/cms/envs/common.py b/cms/envs/common.py index 67c04bab7b..1c0cb10634 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -793,6 +793,9 @@ INSTALLED_APPS = ( # programs support 'openedx.core.djangoapps.programs', + + # Self-paced course configuration + 'openedx.core.djangoapps.self_paced', ) diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index efb3e24dcd..9703977314 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -70,6 +70,7 @@ var CourseDetails = Backbone.Model.extend({ }, _videokey_illegal_chars : /[^a-zA-Z0-9_-]/g, + set_videosource: function(newsource) { // newsource either is