diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index e71f05de30..68df268b3e 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -11,15 +11,21 @@ from cms.djangoapps.models.settings.course_details import CourseDetails,\ import json from util import converters import calendar +from util.converters import jsdate_to_time +from django.utils.timezone import UTC +from cms.djangoapps.models.settings.course_grading import CourseGradingModel +from cms.djangoapps.contentstore.utils import get_modulestore +import copy # YYYY-MM-DDThh:mm:ss.s+/-HH:MM class ConvertersTestCase(TestCase): - def struct_to_datetime(self, struct_time): + @staticmethod + def struct_to_datetime(struct_time): return datetime.datetime(struct_time.tm_year, struct_time.tm_mon, struct_time.tm_mday, struct_time.tm_hour, struct_time.tm_min, struct_time.tm_sec) def compare_dates(self, date1, date2, expected_delta): - dt1 = self.struct_to_datetime(date1) - dt2 = self.struct_to_datetime(date2) + dt1 = ConvertersTestCase.struct_to_datetime(date1) + dt2 = ConvertersTestCase.struct_to_datetime(date2) self.assertEqual(dt1 - dt2, expected_delta, str(date1) + "-" + str(date2) + "!=" + str(expected_delta)) def test_iso_to_struct(self): @@ -28,7 +34,8 @@ class ConvertersTestCase(TestCase): self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00"), converters.jsdate_to_time("2012-12-31T23:59"), datetime.timedelta(minutes=1)) self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"), converters.jsdate_to_time("2012-12-31T23:59:59"), datetime.timedelta(seconds=1)) -class CourseDetailsTestCase(TestCase): + +class CourseTestCase(TestCase): def setUp(self): uname = 'testuser' email = 'test+courses@edx.org' @@ -75,6 +82,7 @@ class CourseDetailsTestCase(TestCase): """Create new course""" self.client.post(reverse('create_new_course'), self.course_data) +class CourseDetailsTestCase(CourseTestCase): def test_virgin_fetch(self): details = CourseDetails.fetch(self.course_location) self.assertEqual(details.course_location, self.course_location, "Location not copied into") @@ -104,6 +112,7 @@ class CourseDetailsTestCase(TestCase): ## NOTE: I couldn't figure out how to validly test time setting w/ all the conversions jsondetails = CourseDetails.fetch(self.course_location) jsondetails.syllabus = "bar" + # encode - decode to convert date fields and other data which changes form self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).syllabus, jsondetails.syllabus, "After set syllabus") jsondetails.overview = "Overview" @@ -116,58 +125,12 @@ class CourseDetailsTestCase(TestCase): self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort, jsondetails.effort, "After set effort") -class CourseDetailsViewTest(TestCase): - def setUp(self): - uname = 'testuser' - email = 'test+courses@edx.org' - password = 'foo' - - # Create the use so we can log them in. - self.user = User.objects.create_user(uname, email, password) - - # Note that we do not actually need to do anything - # for registration if we directly mark them active. - self.user.is_active = True - # Staff has access to view all courses - self.user.is_staff = True - self.user.save() - - # Flush and initialize the module store - # It needs the templates because it creates new records - # by cloning from the template. - # Note that if your test module gets in some weird state - # (though it shouldn't), do this manually - # from the bash shell to drop it: - # $ mongo test_xmodule --eval "db.dropDatabase()" - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - xmodule.templates.update_templates() - - self.client = Client() - self.client.login(username=uname, password=password) - - self.course_data = { - 'template': 'i4x://edx/templates/course/Empty', - 'org': 'MITx', - 'number': '999', - 'display_name': 'Robot Super Course', - } - self.course_location = Location('i4x', 'MITx', '999', 'course', 'Robot_Super_Course') - self.create_course() - - def tearDown(self): - xmodule.modulestore.django._MODULESTORES = {} - xmodule.modulestore.django.modulestore().collection.drop() - - def create_course(self): - """Create new course""" - self.client.post(reverse('create_new_course'), self.course_data) - +class CourseDetailsViewTest(CourseTestCase): def alter_field(self, url, details, field, val): setattr(details, field, val) -# jsondetails = json.dumps(details, cls=CourseSettingsEncoder) - resp = self.client.post(url, details) - self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + val) + # FIXME post is not invoking views.course_settings_updates + resp = self.client.post(url, details.__dict__, "application/json") + self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val)) def test_update_and_fetch(self): details = CourseDetails.fetch(self.course_location) @@ -182,13 +145,13 @@ class CourseDetailsViewTest(TestCase): resp = self.client.get(url) self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get") -# self.alter_field(url, details, 'start_date', time.time() * 1000) -# self.alter_field(url, details, 'start_date', time.time() * 1000 + 60 * 60 * 24) -# self.alter_field(url, details, 'end_date', time.time() * 1000 + 60 * 60 * 24 * 100) -# self.alter_field(url, details, 'enrollment_start', time.time() * 1000) + utc = UTC() +# self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,12,1,30, tzinfo=utc)) +# self.alter_field(url, details, 'start_date', datetime.datetime(2012,11,1,13,30, tzinfo=utc)) +# self.alter_field(url, details, 'end_date', datetime.datetime(2013,2,12,1,30, tzinfo=utc)) +# self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012,10,12,1,30, tzinfo=utc)) # -# self.alter_field(url, details, 'enrollment_end', time.time() * 1000 + 60 * 60 * 24 * 8) -# self.alter_field(url, details, 'syllabus', "bar") +# self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012,11,15,1,30, tzinfo=utc)) # self.alter_field(url, details, 'overview', "Overview") # self.alter_field(url, details, 'intro_video', "intro_video") # self.alter_field(url, details, 'effort', "effort") @@ -205,9 +168,75 @@ class CourseDetailsViewTest(TestCase): def compare_date_fields(self, details, encoded, context, field): if details[field] is not None: if field in encoded and encoded[field] is not None: - self.assertEqual(encoded[field] / 1000, calendar.timegm(details[field]), "dates not == at " + context) + encoded_encoded = jsdate_to_time(encoded[field]) + details_encoded = jsdate_to_time(details[field]) + dt1 = ConvertersTestCase.struct_to_datetime(encoded_encoded) + dt2 = ConvertersTestCase.struct_to_datetime(details_encoded) + expected_delta = datetime.timedelta(0) + self.assertEqual(dt1 - dt2, expected_delta, str(encoded_encoded) + "!=" + str(details_encoded) + " at " + context) else: self.fail(field + " missing from encoded but in details at " + context) elif field in encoded and encoded[field] is not None: self.fail(field + " included in encoding but missing from details at " + context) + +class CourseGradingTest(CourseTestCase): + def test_initial_grader(self): + descriptor = get_modulestore(self.course_location).get_item(self.course_location) + test_grader = CourseGradingModel(descriptor) + # ??? How much should this test bake in expectations about defaults and thus fail if defaults change? + self.assertEqual(self.course_location, test_grader.course_location, "Course locations") + self.assertIsNotNone(test_grader.graders, "No graders") + self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") + + def test_fetch_grader(self): + test_grader = CourseGradingModel.fetch(self.course_location.url()) + self.assertEqual(self.course_location, test_grader.course_location, "Course locations") + self.assertIsNotNone(test_grader.graders, "No graders") + self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") + + test_grader = CourseGradingModel.fetch(self.course_location) + self.assertEqual(self.course_location, test_grader.course_location, "Course locations") + self.assertIsNotNone(test_grader.graders, "No graders") + self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") + + for i, grader in enumerate(test_grader.graders): + subgrader = CourseGradingModel.fetch_grader(self.course_location, i) + self.assertDictEqual(grader, subgrader, str(i) + "th graders not equal") + + subgrader = CourseGradingModel.fetch_grader(self.course_location.list(), 0) + self.assertDictEqual(test_grader.graders[0], subgrader, "failed with location as list") + + def test_fetch_cutoffs(self): + test_grader = CourseGradingModel.fetch_cutoffs(self.course_location) + # ??? should this check that it's at least a dict? (expected is { "pass" : 0.5 } I think) + self.assertIsNotNone(test_grader, "No cutoffs via fetch") + + test_grader = CourseGradingModel.fetch_cutoffs(self.course_location.url()) + self.assertIsNotNone(test_grader, "No cutoffs via fetch with url") + + def test_fetch_grace(self): + test_grader = CourseGradingModel.fetch_grace_period(self.course_location) + # almost a worthless test + self.assertIn('grace_period', test_grader, "No grace via fetch") + + test_grader = CourseGradingModel.fetch_grace_period(self.course_location.url()) + self.assertIn('grace_period', test_grader, "No cutoffs via fetch with url") + + def test_update_from_json(self): + test_grader = CourseGradingModel.fetch(self.course_location) + altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) + self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update") + + test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2 + altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) + self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2") + + test_grader.grade_cutoffs['D'] = 0.3 + altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) + self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D") + + test_grader.grace_period = {'hours' : '4'} + altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) + self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period") + \ No newline at end of file diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 0f9867c7d2..29144ce9fb 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -322,7 +322,7 @@ def edit_unit(request, location): 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, 'subsection': containing_subsection, - 'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else 'Unset', + 'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else None, 'section': containing_section, 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'unit_state': unit_state, diff --git a/cms/djangoapps/models/settings/course_faculty.py b/cms/djangoapps/models/settings/course_faculty.py deleted file mode 100644 index c1812614ec..0000000000 --- a/cms/djangoapps/models/settings/course_faculty.py +++ /dev/null @@ -1,22 +0,0 @@ -from xmodule.modulestore import Location -class CourseFaculty: - def __init__(self, location): - if not isinstance(location, Location): - location = Location(location) - # course_location is used so that updates know where to get the relevant data - self.course_location = location - self.first_name = "" - self.last_name = "" - self.photo = None - self.bio = "" - - - @classmethod - def fetch(cls, course_location): - """ - Fetch a list of faculty for the course - """ - if not isinstance(course_location, Location): - course_location = Location(course_location) - - # Must always have at least one faculty member (possibly empty) \ No newline at end of file diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index 2f6ff30110..e0bab1f225 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -237,7 +237,7 @@ class CourseGradingModel: # 5 hours 59 minutes 59 seconds => converted to iso format rawgrace = descriptor.metadata.get('graceperiod', None) if rawgrace: - parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d*)\s*(\w*)', rawgrace)} + parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)} return parsedgrace else: return None diff --git a/cms/templates/overview.html b/cms/templates/overview.html index 1f99848505..2a46908c55 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -111,7 +111,7 @@
On the date set above, this section – – will be released to students along with the 5 subsections within it. Any units marked private will only be visible to admins.
+On the date set above, this section – – will be released to students. Any units marked private will only be visible to admins.
This unit is scheduled to be released to students on ${release_date} with the subsection "${subsection.display_name}"
+This unit is scheduled to be released to students + % if release_date is not None: + on ${release_date} + % endif + with the subsection "${subsection.display_name}"