From 17864353a532d5dfc57966256cb1beac39584008 Mon Sep 17 00:00:00 2001 From: Don Mitchell Date: Fri, 15 Nov 2013 12:54:06 -0500 Subject: [PATCH] Restful course settings STUD-946 STUD-947 --- .../contentstore/tests/test_contentstore.py | 22 +- .../contentstore/tests/test_course_index.py | 4 +- .../tests/test_course_settings.py | 295 +++++++++++------- cms/djangoapps/contentstore/tests/utils.py | 20 +- .../contentstore/views/checklist.py | 22 +- .../contentstore/views/component.py | 114 +++---- cms/djangoapps/contentstore/views/course.py | 202 ++++++------ cms/djangoapps/contentstore/views/item.py | 46 ++- .../models/settings/course_details.py | 47 +-- .../models/settings/course_grading.py | 163 ++++------ .../coffee/spec/views/overview_spec.coffee | 2 +- cms/static/js/collections/course_grader.js | 4 - cms/static/js/models/assignment_grade.js | 28 +- .../js/models/settings/course_details.js | 9 +- .../models/settings/course_grading_policy.js | 9 - .../js/views/overview_assignment_grader.js | 2 +- cms/static/js/views/settings/main.js | 6 +- cms/templates/edit_subsection.html | 3 +- cms/templates/overview.html | 3 +- cms/templates/settings.html | 33 +- cms/templates/settings_advanced.html | 4 +- cms/templates/settings_graders.html | 9 +- cms/templates/widgets/header.html | 6 +- cms/urls.py | 13 +- common/lib/xmodule/xmodule/course_module.py | 3 + 25 files changed, 516 insertions(+), 553 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 831800e27b..237a60809e 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1572,8 +1572,7 @@ class ContentStoreTest(ModuleStoreTestCase): status_code=200, html=True ) - # TODO: uncomment when course index no longer has locations being returned. - # _test_no_locations(self, resp) + _test_no_locations(self, resp) def test_course_overview_view_with_course(self): """Test viewing the course overview page with an existing course""" @@ -1656,23 +1655,8 @@ class ContentStoreTest(ModuleStoreTestCase): test_get_html('checklists') test_get_html('assets') test_get_html('tabs') - - # settings_details - resp = self.client.get_html(reverse('settings_details', - kwargs={'org': loc.org, - 'course': loc.course, - 'name': loc.name})) - self.assertEqual(resp.status_code, 200) - _test_no_locations(self, resp) - - # settings_details - resp = self.client.get_html(reverse('settings_grading', - kwargs={'org': loc.org, - 'course': loc.course, - 'name': loc.name})) - self.assertEqual(resp.status_code, 200) - # TODO: uncomment when grading is not using old locations. - # _test_no_locations(self, resp) + test_get_html('settings/details') + test_get_html('settings/grading') # advanced settings resp = self.client.get_html(reverse('course_advanced_settings', diff --git a/cms/djangoapps/contentstore/tests/test_course_index.py b/cms/djangoapps/contentstore/tests/test_course_index.py index 1fcdb6f040..4c4c736fb7 100644 --- a/cms/djangoapps/contentstore/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/tests/test_course_index.py @@ -3,7 +3,6 @@ Unit tests for getting the list of courses and the course outline. """ import json import lxml -from django.core.urlresolvers import reverse from contentstore.tests.utils import CourseTestCase from xmodule.modulestore.django import loc_mapper @@ -60,8 +59,7 @@ class TestCourseIndex(CourseTestCase): """ Test the error conditions for the access """ - locator = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True) - outline_url = locator.url_reverse('course/', '') + outline_url = self.course_locator.url_reverse('course/', '') # register a non-staff member and try to delete the course branch non_staff_client, _ = self.createNonStaffAuthedUserClient() response = non_staff_client.delete(outline_url, {}, HTTP_ACCEPT='application/json') diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index d1cbcde4d0..bbd386bd58 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -6,7 +6,6 @@ import json import copy import mock -from django.core.urlresolvers import reverse from django.utils.timezone import UTC from django.test.utils import override_settings @@ -21,6 +20,7 @@ from models.settings.course_metadata import CourseMetadata from xmodule.fields import Date from .utils import CourseTestCase +from xmodule.modulestore.django import loc_mapper class CourseDetailsTestCase(CourseTestCase): @@ -28,8 +28,10 @@ class CourseDetailsTestCase(CourseTestCase): Tests the first course settings page (course dates, overview, etc.). """ def test_virgin_fetch(self): - details = CourseDetails.fetch(self.course.location) - self.assertEqual(details.course_location, self.course.location, "Location not copied into") + details = CourseDetails.fetch(self.course_locator) + self.assertEqual(details.org, self.course.location.org, "Org not copied into") + self.assertEqual(details.course_id, self.course.location.course, "Course_id not copied into") + self.assertEqual(details.run, self.course.location.name, "Course name not copied into") self.assertEqual(details.course_image_name, self.course.course_image) self.assertIsNotNone(details.start_date.tzinfo) self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date)) @@ -40,10 +42,9 @@ class CourseDetailsTestCase(CourseTestCase): self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) def test_encoder(self): - details = CourseDetails.fetch(self.course.location) + details = CourseDetails.fetch(self.course_locator) jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.loads(jsondetails) - self.assertTupleEqual(Location(jsondetails['course_location']), self.course.location, "Location !=") self.assertEqual(jsondetails['course_image_name'], self.course.course_image) self.assertIsNone(jsondetails['end_date'], "end date somehow initialized ") self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ") @@ -57,7 +58,6 @@ class CourseDetailsTestCase(CourseTestCase): Test the encoder out of its original constrained purpose to see if it functions for general use """ details = { - 'location': Location(['tag', 'org', 'course', 'category', 'name']), 'number': 1, 'string': 'string', 'datetime': datetime.datetime.now(UTC()) @@ -65,59 +65,49 @@ class CourseDetailsTestCase(CourseTestCase): jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.loads(jsondetails) - self.assertIn('location', jsondetails) - self.assertIn('org', jsondetails['location']) - self.assertEquals('org', jsondetails['location'][1]) self.assertEquals(1, jsondetails['number']) self.assertEqual(jsondetails['string'], 'string') def test_update_and_fetch(self): - jsondetails = CourseDetails.fetch(self.course.location) + jsondetails = CourseDetails.fetch(self.course_locator) jsondetails.syllabus = "bar" # encode - decode to convert date fields and other data which changes form self.assertEqual( - CourseDetails.update_from_json(jsondetails.__dict__).syllabus, + CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__).syllabus, jsondetails.syllabus, "After set syllabus" ) jsondetails.overview = "Overview" self.assertEqual( - CourseDetails.update_from_json(jsondetails.__dict__).overview, + CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__).overview, jsondetails.overview, "After set overview" ) jsondetails.intro_video = "intro_video" self.assertEqual( - CourseDetails.update_from_json(jsondetails.__dict__).intro_video, + CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__).intro_video, jsondetails.intro_video, "After set intro_video" ) jsondetails.effort = "effort" self.assertEqual( - CourseDetails.update_from_json(jsondetails.__dict__).effort, + CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__).effort, jsondetails.effort, "After set effort" ) jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC()) self.assertEqual( - CourseDetails.update_from_json(jsondetails.__dict__).start_date, + CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__).start_date, jsondetails.start_date ) jsondetails.course_image_name = "an_image.jpg" self.assertEqual( - CourseDetails.update_from_json(jsondetails.__dict__).course_image_name, + CourseDetails.update_from_json(self.course_locator, jsondetails.__dict__).course_image_name, jsondetails.course_image_name ) @override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) def test_marketing_site_fetch(self): - settings_details_url = reverse( - 'settings_details', - kwargs={ - 'org': self.course.location.org, - 'name': self.course.location.name, - 'course': self.course.location.course - } - ) + settings_details_url = self.course_locator.url_reverse('settings/details/') with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}): - response = self.client.get(settings_details_url) + response = self.client.get_html(settings_details_url) self.assertNotContains(response, "Course Summary Page") self.assertNotContains(response, "Send a note to students via email") self.assertContains(response, "course summary page will not be viewable") @@ -135,17 +125,10 @@ class CourseDetailsTestCase(CourseTestCase): self.assertNotContains(response, "Requirements") def test_regular_site_fetch(self): - settings_details_url = reverse( - 'settings_details', - kwargs={ - 'org': self.course.location.org, - 'name': self.course.location.name, - 'course': self.course.location.course - } - ) + settings_details_url = self.course_locator.url_reverse('settings/details/') with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): - response = self.client.get(settings_details_url) + response = self.client.get_html(settings_details_url) self.assertContains(response, "Course Summary Page") self.assertContains(response, "Send a note to students via email") self.assertNotContains(response, "course summary page will not be viewable") @@ -168,10 +151,12 @@ class CourseDetailsViewTest(CourseTestCase): Tests for modifying content on the first course settings page (course dates, overview, etc.). """ def alter_field(self, url, details, field, val): + """ + Change the one field to the given value and then invoke the update post to see if it worked. + """ setattr(details, field, val) # Need to partially serialize payload b/c the mock doesn't handle it correctly payload = copy.copy(details.__dict__) - payload['course_location'] = details.course_location.url() payload['start_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.start_date) payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date) payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start) @@ -181,16 +166,17 @@ class CourseDetailsViewTest(CourseTestCase): @staticmethod def convert_datetime_to_iso(datetime_obj): + """ + Use the xblock serializer to convert the datetime + """ return Date().to_json(datetime_obj) def test_update_and_fetch(self): - loc = self.course.location - details = CourseDetails.fetch(loc) + details = CourseDetails.fetch(self.course_locator) # resp s/b json from here on - url = reverse('course_settings', kwargs={'org': loc.org, 'course': loc.course, - 'name': loc.name, 'section': 'details'}) - resp = self.client.get(url) + url = self.course_locator.url_reverse('settings/details/') + resp = self.client.get_json(url) self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get") utc = UTC() @@ -206,6 +192,9 @@ class CourseDetailsViewTest(CourseTestCase): self.alter_field(url, details, 'course_image_name', "course_image_name") def compare_details_with_encoding(self, encoded, details, context): + """ + compare all of the fields of the before and after dicts + """ self.compare_date_fields(details, encoded, context, 'start_date') self.compare_date_fields(details, encoded, context, 'end_date') self.compare_date_fields(details, encoded, context, 'enrollment_start') @@ -216,6 +205,9 @@ class CourseDetailsViewTest(CourseTestCase): self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==") def compare_date_fields(self, details, encoded, context, field): + """ + Compare the given date fields between the before and after doing json deserialization + """ if details[field] is not None: date = Date() if field in encoded and encoded[field] is not None: @@ -234,142 +226,191 @@ class CourseGradingTest(CourseTestCase): Tests for the course settings grading page. """ 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") + test_grader = CourseGradingModel(self.course) + self.assertIsNotNone(test_grader.graders) + self.assertIsNotNone(test_grader.grade_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") + test_grader = CourseGradingModel.fetch(self.course_locator) 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) + subgrader = CourseGradingModel.fetch_grader(self.course_locator, 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__) + test_grader = CourseGradingModel.fetch(self.course_locator) + altered_grader = CourseGradingModel.update_from_json(self.course_locator, 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__) + altered_grader = CourseGradingModel.update_from_json(self.course_locator, 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__) + altered_grader = CourseGradingModel.update_from_json(self.course_locator, test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D") test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0} - altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) + altered_grader = CourseGradingModel.update_from_json(self.course_locator, test_grader.__dict__) self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period") def test_update_grader_from_json(self): - test_grader = CourseGradingModel.fetch(self.course.location) - altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) + test_grader = CourseGradingModel.fetch(self.course_locator) + altered_grader = CourseGradingModel.update_grader_from_json(self.course_locator, test_grader.graders[1]) self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update") test_grader.graders[1]['min_count'] = test_grader.graders[1].get('min_count') + 2 - altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) + altered_grader = CourseGradingModel.update_grader_from_json(self.course_locator, test_grader.graders[1]) self.assertDictEqual(test_grader.graders[1], altered_grader, "min_count[1] + 2") test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1 - altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1]) + altered_grader = CourseGradingModel.update_grader_from_json(self.course_locator, test_grader.graders[1]) self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2") def test_update_cutoffs_from_json(self): - test_grader = CourseGradingModel.fetch(self.course.location) - CourseGradingModel.update_cutoffs_from_json(test_grader.course_location, test_grader.grade_cutoffs) + test_grader = CourseGradingModel.fetch(self.course_locator) + CourseGradingModel.update_cutoffs_from_json(self.course_locator, test_grader.grade_cutoffs) # Unlike other tests, need to actually perform a db fetch for this test since update_cutoffs_from_json # simply returns the cutoffs you send into it, rather than returning the db contents. - altered_grader = CourseGradingModel.fetch(self.course.location) + altered_grader = CourseGradingModel.fetch(self.course_locator) self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "Noop update") test_grader.grade_cutoffs['D'] = 0.3 - CourseGradingModel.update_cutoffs_from_json(test_grader.course_location, test_grader.grade_cutoffs) - altered_grader = CourseGradingModel.fetch(self.course.location) + CourseGradingModel.update_cutoffs_from_json(self.course_locator, test_grader.grade_cutoffs) + altered_grader = CourseGradingModel.fetch(self.course_locator) self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff add D") test_grader.grade_cutoffs['Pass'] = 0.75 - CourseGradingModel.update_cutoffs_from_json(test_grader.course_location, test_grader.grade_cutoffs) - altered_grader = CourseGradingModel.fetch(self.course.location) + CourseGradingModel.update_cutoffs_from_json(self.course_locator, test_grader.grade_cutoffs) + altered_grader = CourseGradingModel.fetch(self.course_locator) self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff change 'Pass'") def test_delete_grace_period(self): - test_grader = CourseGradingModel.fetch(self.course.location) - CourseGradingModel.update_grace_period_from_json(test_grader.course_location, test_grader.grace_period) + test_grader = CourseGradingModel.fetch(self.course_locator) + CourseGradingModel.update_grace_period_from_json(self.course_locator, test_grader.grace_period) # update_grace_period_from_json doesn't return anything, so query the db for its contents. - altered_grader = CourseGradingModel.fetch(self.course.location) + altered_grader = CourseGradingModel.fetch(self.course_locator) self.assertEqual(test_grader.grace_period, altered_grader.grace_period, "Noop update") test_grader.grace_period = {'hours': 15, 'minutes': 5, 'seconds': 30} - CourseGradingModel.update_grace_period_from_json(test_grader.course_location, test_grader.grace_period) - altered_grader = CourseGradingModel.fetch(self.course.location) + CourseGradingModel.update_grace_period_from_json(self.course_locator, test_grader.grace_period) + altered_grader = CourseGradingModel.fetch(self.course_locator) self.assertDictEqual(test_grader.grace_period, altered_grader.grace_period, "Adding in a grace period") test_grader.grace_period = {'hours': 1, 'minutes': 10, 'seconds': 0} # Now delete the grace period - CourseGradingModel.delete_grace_period(test_grader.course_location) + CourseGradingModel.delete_grace_period(self.course_locator) # update_grace_period_from_json doesn't return anything, so query the db for its contents. - altered_grader = CourseGradingModel.fetch(self.course.location) + altered_grader = CourseGradingModel.fetch(self.course_locator) # Once deleted, the grace period should simply be None self.assertEqual(None, altered_grader.grace_period, "Delete grace period") def test_update_section_grader_type(self): # Get the descriptor and the section_grader_type and assert they are the default values descriptor = get_modulestore(self.course.location).get_item(self.course.location) - section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) + section_grader_type = CourseGradingModel.get_section_grader_type(self.course_locator) self.assertEqual('Not Graded', section_grader_type['graderType']) self.assertEqual(None, descriptor.format) self.assertEqual(False, descriptor.graded) # Change the default grader type to Homework, which should also mark the section as graded - CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Homework'}) + CourseGradingModel.update_section_grader_type(self.course, 'Homework') descriptor = get_modulestore(self.course.location).get_item(self.course.location) - section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) + section_grader_type = CourseGradingModel.get_section_grader_type(self.course_locator) self.assertEqual('Homework', section_grader_type['graderType']) self.assertEqual('Homework', descriptor.format) self.assertEqual(True, descriptor.graded) # Change the grader type back to Not Graded, which should also unmark the section as graded - CourseGradingModel.update_section_grader_type(self.course.location, {'graderType': 'Not Graded'}) + CourseGradingModel.update_section_grader_type(self.course, 'Not Graded') descriptor = get_modulestore(self.course.location).get_item(self.course.location) - section_grader_type = CourseGradingModel.get_section_grader_type(self.course.location) + section_grader_type = CourseGradingModel.get_section_grader_type(self.course_locator) self.assertEqual('Not Graded', section_grader_type['graderType']) self.assertEqual(None, descriptor.format) self.assertEqual(False, descriptor.graded) + def test_get_set_grader_types_ajax(self): + """ + Test configuring the graders via ajax calls + """ + grader_type_url_base = self.course_locator.url_reverse('settings/grading') + # test get whole + response = self.client.get_json(grader_type_url_base) + whole_model = json.loads(response.content) + self.assertIn('graders', whole_model) + self.assertIn('grade_cutoffs', whole_model) + self.assertIn('grace_period', whole_model) + # test post/update whole + whole_model['grace_period'] = {'hours': 1, 'minutes': 30, 'seconds': 0} + response = self.client.ajax_post(grader_type_url_base, whole_model) + self.assertEqual(200, response.status_code) + response = self.client.get_json(grader_type_url_base) + whole_model = json.loads(response.content) + self.assertEqual(whole_model['grace_period'], {'hours': 1, 'minutes': 30, 'seconds': 0}) + # test get one grader + self.assertGreater(len(whole_model['graders']), 1) # ensure test will make sense + response = self.client.get_json(grader_type_url_base + '/1') + grader_sample = json.loads(response.content) + self.assertEqual(grader_sample, whole_model['graders'][1]) + # test add grader + new_grader = { + "type": "Extra Credit", + "min_count": 1, + "drop_count": 2, + "short_label": None, + "weight": 15, + } + response = self.client.ajax_post( + '{}/{}'.format(grader_type_url_base, len(whole_model['graders'])), + new_grader + ) + self.assertEqual(200, response.status_code) + grader_sample = json.loads(response.content) + new_grader['id'] = len(whole_model['graders']) + self.assertEqual(new_grader, grader_sample) + # test delete grader + response = self.client.delete(grader_type_url_base + '/1', HTTP_ACCEPT="application/json") + self.assertEqual(204, response.status_code) + response = self.client.get_json(grader_type_url_base) + updated_model = json.loads(response.content) + new_grader['id'] -= 1 # one fewer and the id mutates + self.assertIn(new_grader, updated_model['graders']) + self.assertNotIn(whole_model['graders'][1], updated_model['graders']) + + def setup_test_set_get_section_grader_ajax(self): + """ + Populate the course, grab a section, get the url for the assignment type access + """ + self.populateCourse() + sections = get_modulestore(self.course_location).get_items( + self.course_location.replace(category="sequential", name=None) + ) + # see if test makes sense + self.assertGreater(len(sections), 0, "No sections found") + section = sections[0] # just take the first one + section_locator = loc_mapper().translate_location(self.course_location.course_id, section.location, False, True) + return section_locator.url_reverse('xblock') + + def test_set_get_section_grader_ajax(self): + """ + Test setting and getting section grades via the grade as url + """ + grade_type_url = self.setup_test_set_get_section_grader_ajax() + response = self.client.ajax_post(grade_type_url, {'graderType': u'Homework'}) + self.assertEqual(200, response.status_code) + response = self.client.get_json(grade_type_url + '?fields=graderType') + self.assertEqual(json.loads(response.content).get('graderType'), u'Homework') + # and unset + response = self.client.ajax_post(grade_type_url, {'graderType': u'Not Graded'}) + self.assertEqual(200, response.status_code) + response = self.client.get_json(grade_type_url + '?fields=graderType') + self.assertEqual(json.loads(response.content).get('graderType'), u'Not Graded') + class CourseMetadataEditingTest(CourseTestCase): """ @@ -436,25 +477,52 @@ class CourseMetadataEditingTest(CourseTestCase): class CourseGraderUpdatesTest(CourseTestCase): + """ + Test getting, deleting, adding, & updating graders + """ def setUp(self): + """Compute the url to use in tests""" super(CourseGraderUpdatesTest, self).setUp() - self.url = reverse("course_settings", kwargs={ - 'org': self.course.location.org, - 'course': self.course.location.course, - 'name': self.course.location.name, - 'grader_index': 0, - }) + self.url = self.course_locator.url_reverse('settings/grading') + self.starting_graders = CourseGradingModel(self.course).graders def test_get(self): - resp = self.client.get(self.url) + """Test getting a specific grading type record.""" + resp = self.client.get_json(self.url + '/0') self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) + self.assertEqual(self.starting_graders[0], obj) def test_delete(self): - resp = self.client.delete(self.url) + """Test deleting a specific grading type record.""" + resp = self.client.delete(self.url + '/0', HTTP_ACCEPT="application/json") self.assertEqual(resp.status_code, 204) + current_graders = CourseGradingModel.fetch(self.course_locator).graders + self.assertNotIn(self.starting_graders[0], current_graders) + self.assertEqual(len(self.starting_graders) - 1, len(current_graders)) - def test_post(self): + def test_update(self): + """Test updating a specific grading type record.""" + grader = { + "id": 0, + "type": "manual", + "min_count": 5, + "drop_count": 10, + "short_label": "yo momma", + "weight": 17.3, + } + resp = self.client.ajax_post(self.url + '/0', grader) + self.assertEqual(resp.status_code, 200) + obj = json.loads(resp.content) + self.assertEqual(obj, grader) + current_graders = CourseGradingModel.fetch(self.course_locator).graders + self.assertEqual(len(self.starting_graders), len(current_graders)) + + def test_add(self): + """Test adding a grading type record.""" + # the same url works for changing the whole grading model (graceperiod, cutoffs, and grading types) when + # the grading_index is None; thus, using None to imply adding a grading_type doesn't work; so, it uses an + # index out of bounds to imply create item. grader = { "type": "manual", "min_count": 5, @@ -462,6 +530,11 @@ class CourseGraderUpdatesTest(CourseTestCase): "short_label": "yo momma", "weight": 17.3, } - resp = self.client.ajax_post(self.url, grader) + resp = self.client.ajax_post('{}/{}'.format(self.url, len(self.starting_graders) + 1), grader) self.assertEqual(resp.status_code, 200) obj = json.loads(resp.content) + self.assertEqual(obj['id'], len(self.starting_graders)) + del obj['id'] + self.assertEqual(obj, grader) + current_graders = CourseGradingModel.fetch(self.course_locator).graders + self.assertEqual(len(self.starting_graders) + 1, len(current_graders)) diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index b59f214054..0e716cc878 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -10,8 +10,9 @@ from django.test.client import Client from django.test.utils import override_settings from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from contentstore.tests.modulestore_config import TEST_MODULESTORE +from xmodule.modulestore.django import loc_mapper def parse_json(response): @@ -41,6 +42,7 @@ class AjaxEnabledTestClient(Client): if not isinstance(data, basestring): data = json.dumps(data or {}) kwargs.setdefault("HTTP_X_REQUESTED_WITH", "XMLHttpRequest") + kwargs.setdefault("HTTP_ACCEPT", "application/json") return self.post(path=path, data=data, content_type=content_type, **kwargs) def get_html(self, path, data=None, follow=False, **extra): @@ -88,6 +90,9 @@ class CourseTestCase(ModuleStoreTestCase): display_name='Robot Super Course', ) self.course_location = self.course.location + self.course_locator = loc_mapper().translate_location( + self.course.location.course_id, self.course.location, False, True + ) def createNonStaffAuthedUserClient(self): """ @@ -106,3 +111,16 @@ class CourseTestCase(ModuleStoreTestCase): client = Client() client.login(username=uname, password=password) return client, nonstaff + + def populateCourse(self): + """ + Add 2 chapters, 4 sections, 8 verticals, 16 problems to self.course (branching 2) + """ + def descend(parent, stack): + xblock_type = stack.pop(0) + for _ in range(2): + child = ItemFactory.create(category=xblock_type, parent_location=parent.location) + if stack: + descend(child, stack) + + descend(self.course, ['chapter', 'sequential', 'vertical', 'problem']) diff --git a/cms/djangoapps/contentstore/views/checklist.py b/cms/djangoapps/contentstore/views/checklist.py index 5643e5c044..61c6c672a7 100644 --- a/cms/djangoapps/contentstore/views/checklist.py +++ b/cms/djangoapps/contentstore/views/checklist.py @@ -5,7 +5,6 @@ from util.json_request import JsonResponse from django.http import HttpResponseBadRequest from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_http_methods -from django.core.urlresolvers import reverse from django_future.csrf import ensure_csrf_cookie from mitxmako.shortcuts import render_to_response from django.http import HttpResponseNotFound @@ -22,6 +21,8 @@ from xmodule.modulestore.locator import BlockUsageLocator __all__ = ['checklists_handler'] + +# pylint: disable=unused-argument @require_http_methods(("GET", "POST", "PUT")) @login_required @ensure_csrf_cookie @@ -85,8 +86,8 @@ def checklists_handler(request, tag=None, course_id=None, branch=None, version_g return JsonResponse(expanded_checklist) else: return HttpResponseBadRequest( - ( "Could not save checklist state because the checklist index " - "was out of range or unspecified."), + ("Could not save checklist state because the checklist index " + "was out of range or unspecified."), content_type="text/plain" ) else: @@ -113,14 +114,12 @@ def expand_checklist_action_url(course_module, checklist): The method does a copy of the input checklist and does not modify the input argument. """ expanded_checklist = copy.deepcopy(checklist) - oldurlconf_map = { - "SettingsDetails": "settings_details", - "SettingsGrading": "settings_grading" - } urlconf_map = { "ManageUsers": "course_team", - "CourseOutline": "course" + "CourseOutline": "course", + "SettingsDetails": "settings/details", + "SettingsGrading": "settings/grading", } for item in expanded_checklist.get('items'): @@ -130,12 +129,5 @@ def expand_checklist_action_url(course_module, checklist): ctx_loc = course_module.location location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True) item['action_url'] = location.url_reverse(url_prefix, '') - elif action_url in oldurlconf_map: - urlconf_name = oldurlconf_map[action_url] - item['action_url'] = reverse(urlconf_name, kwargs={ - 'org': course_module.location.org, - 'course': course_module.location.course, - 'name': course_module.location.name, - }) return expanded_checklist diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 70d7bef7ce..1d49ff0dd3 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -2,15 +2,11 @@ import json import logging from collections import defaultdict -from django.http import (HttpResponse, HttpResponseBadRequest, - HttpResponseForbidden) +from django.http import HttpResponse, HttpResponseBadRequest from django.contrib.auth.decorators import login_required -from django.views.decorators.http import require_http_methods from django.core.exceptions import PermissionDenied -from django_future.csrf import ensure_csrf_cookie from django.conf import settings -from xmodule.modulestore.exceptions import (ItemNotFoundError, - InvalidLocationError) +from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError from mitxmako.shortcuts import render_to_response from xmodule.modulestore import Location @@ -19,7 +15,7 @@ from xmodule.util.date_utils import get_default_time_display from xmodule.modulestore.django import loc_mapper from xblock.fields import Scope -from util.json_request import expect_json, JsonResponse +from util.json_request import expect_json from contentstore.utils import get_lms_link_for_item, compute_unit_state, UnitState, get_course_for_item @@ -35,7 +31,6 @@ __all__ = ['OPEN_ENDED_COMPONENT_TYPES', 'ADVANCED_COMPONENT_POLICY_KEY', 'edit_subsection', 'edit_unit', - 'assignment_type_update', 'create_draft', 'publish_draft', 'unpublish_unit', @@ -75,12 +70,8 @@ def edit_subsection(request, location): except ItemNotFoundError: return HttpResponseBadRequest() - lms_link = get_lms_link_for_item( - location, course_id=course.location.course_id - ) - preview_link = get_lms_link_for_item( - location, course_id=course.location.course_id, preview=True - ) + lms_link = get_lms_link_for_item(location, course_id=course.location.course_id) + preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True) # make sure that location references a 'sequential', otherwise return # BadRequest @@ -92,8 +83,8 @@ def edit_subsection(request, location): # we're for now assuming a single parent if len(parent_locs) != 1: logging.error( - 'Multiple (or none) parents have been found for %s', - location + 'Multiple (or none) parents have been found for %s', + location ) # this should blow up if we don't find any parents, which would be erroneous @@ -109,7 +100,7 @@ def edit_subsection(request, location): for field in fields.values() if field.name not in ['display_name', 'start', 'due', 'format'] - and field.scope == Scope.settings + and field.scope == Scope.settings ) can_view_live = False @@ -120,6 +111,9 @@ def edit_subsection(request, location): can_view_live = True break + course_locator = loc_mapper().translate_location( + course.location.course_id, course.location, False, True + ) locator = loc_mapper().translate_location( course.location.course_id, item.location, False, True ) @@ -127,19 +121,17 @@ def edit_subsection(request, location): return render_to_response( 'edit_subsection.html', { - 'subsection': item, - 'context_course': course, - 'new_unit_category': 'vertical', - 'lms_link': lms_link, - 'preview_link': preview_link, - 'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders), - # For grader, which is not yet converted - 'parent_location': course.location, - 'parent_item': parent, - 'locator': locator, - 'policy_metadata': policy_metadata, - 'subsection_units': subsection_units, - 'can_view_live': can_view_live + 'subsection': item, + 'context_course': course, + 'new_unit_category': 'vertical', + 'lms_link': lms_link, + 'preview_link': preview_link, + 'course_graders': json.dumps(CourseGradingModel.fetch(course_locator).graders), + 'parent_item': parent, + 'locator': locator, + 'policy_metadata': policy_metadata, + 'subsection_units': subsection_units, + 'can_view_live': can_view_live } ) @@ -175,8 +167,8 @@ def edit_unit(request, location): except ItemNotFoundError: return HttpResponseBadRequest() lms_link = get_lms_link_for_item( - item.location, - course_id=course.location.course_id + item.location, + course_id=course.location.course_id ) # Note that the unit_state (draft, public, private) does not match up with the published value @@ -234,7 +226,7 @@ def edit_unit(request, location): category, False, None # don't override default data - )) + )) except PluginMissingError: # dhm: I got this once but it can happen any time the # course author configures an advanced component which does @@ -260,12 +252,10 @@ def edit_unit(request, location): # this will need to change to check permissions correctly so as # to pick the correct parent subsection - containing_subsection_locs = modulestore().get_parent_locations( - location, None - ) + containing_subsection_locs = modulestore().get_parent_locations(location, None) containing_subsection = modulestore().get_item(containing_subsection_locs[0]) containing_section_locs = modulestore().get_parent_locations( - containing_subsection.location, None + containing_subsection.location, None ) containing_section = modulestore().get_item(containing_section_locs[0]) @@ -283,18 +273,18 @@ def edit_unit(request, location): preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE') preview_lms_link = ( - '//{preview_lms_base}/courses/{org}/{course}/' - '{course_name}/courseware/{section}/{subsection}/{index}' - ).format( - preview_lms_base=preview_lms_base, - lms_base=settings.LMS_BASE, - org=course.location.org, - course=course.location.course, - course_name=course.location.name, - section=containing_section.location.name, - subsection=containing_subsection.location.name, - index=index - ) + '//{preview_lms_base}/courses/{org}/{course}/' + '{course_name}/courseware/{section}/{subsection}/{index}' + ).format( + preview_lms_base=preview_lms_base, + lms_base=settings.LMS_BASE, + org=course.location.org, + course=course.location.course, + course_name=course.location.name, + section=containing_section.location.name, + subsection=containing_subsection.location.name, + index=index + ) return render_to_response('unit.html', { 'context_course': course, @@ -321,28 +311,6 @@ def edit_unit(request, location): }) -@expect_json -@login_required -@require_http_methods(("GET", "POST", "PUT")) -@ensure_csrf_cookie -def assignment_type_update(request, org, course, category, name): - """ - CRUD operations on assignment types for sections and subsections and - anything else gradable. - """ - location = Location(['i4x', org, course, category, name]) - if not has_access(request.user, location): - return HttpResponseForbidden() - - if request.method == 'GET': - rsp = CourseGradingModel.get_section_grader_type(location) - elif request.method in ('POST', 'PUT'): # post or put, doesn't matter. - rsp = CourseGradingModel.update_section_grader_type( - location, request.json - ) - return JsonResponse(rsp) - - @login_required @expect_json def create_draft(request): @@ -374,8 +342,8 @@ def publish_draft(request): item = modulestore().get_item(location) _xmodule_recurse( - item, - lambda i: modulestore().publish(i.location, request.user.id) + item, + lambda i: modulestore().publish(i.location, request.user.id) ) return HttpResponse() diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 187ee9343b..f96c476c44 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -32,8 +32,7 @@ from contentstore.course_info_model import ( from contentstore.utils import ( get_lms_link_for_item, add_extra_panel_tab, remove_extra_panel_tab, get_modulestore) -from models.settings.course_details import ( - CourseDetails, CourseSettingsEncoder) +from models.settings.course_details import CourseDetails, CourseSettingsEncoder from models.settings.course_grading import CourseGradingModel from models.settings.course_metadata import CourseMetadata @@ -53,13 +52,12 @@ from student.models import CourseEnrollment from xmodule.html_module import AboutDescriptor from xmodule.modulestore.locator import BlockUsageLocator from course_creators.views import get_course_creator_status, add_user_with_status_unrequested +from contentstore import utils __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler', - 'get_course_settings', - 'course_config_graders_page', + 'settings_handler', + 'grading_handler', 'course_config_advanced_page', - 'course_settings_updates', - 'course_grader_updates', 'course_advanced_updates', 'textbook_index', 'textbook_by_id', 'create_textbook'] @@ -190,10 +188,8 @@ def course_index(request, course_id, branch, version_guid, block): 'lms_link': lms_link, 'sections': sections, 'course_graders': json.dumps( - CourseGradingModel.fetch(course.location).graders + CourseGradingModel.fetch(location).graders ), - # This is used by course grader, which has not yet been updated. - 'parent_location': course.location, 'parent_locator': location, 'new_section_category': 'chapter', 'new_subsection_category': 'sequential', @@ -394,54 +390,106 @@ def course_info_update_handler( @login_required @ensure_csrf_cookie -def get_course_settings(request, org, course, name): +@require_http_methods(("GET", "PUT", "POST")) +@expect_json +def settings_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None): """ - Send models and views as well as html for editing the course settings to - the client. - - org, course, name: Attributes of the Location for the item to edit + Course settings for dates and about pages + GET + html: get the page + json: get the CourseDetails model + PUT + json: update the Course and About xblocks through the CourseDetails model """ - location = get_location_and_verify_access(request, org, course, name) + locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) + if not has_access(request.user, locator): + raise PermissionDenied() - course_module = modulestore().get_item(location) + if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': + course_old_location = loc_mapper().translate_locator_to_location(locator) + course_module = modulestore().get_item(course_old_location) - new_loc = loc_mapper().translate_location(location.course_id, location, False, True) - upload_asset_url = new_loc.url_reverse('assets/', '') + upload_asset_url = locator.url_reverse('assets/') - return render_to_response('settings.html', { - 'context_course': course_module, - 'course_location': location, - 'details_url': reverse(course_settings_updates, - kwargs={"org": org, - "course": course, - "name": name, - "section": "details"}), - 'about_page_editable': not settings.MITX_FEATURES.get( - 'ENABLE_MKTG_SITE', False - ), - 'upload_asset_url': upload_asset_url - }) + return render_to_response('settings.html', { + 'context_course': course_module, + 'course_locator': locator, + 'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_old_location), + 'course_image_url': utils.course_image_url(course_module), + 'details_url': locator.url_reverse('/settings/details/'), + 'about_page_editable': not settings.MITX_FEATURES.get( + 'ENABLE_MKTG_SITE', False + ), + 'upload_asset_url': upload_asset_url + }) + elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): + if request.method == 'GET': + return JsonResponse( + CourseDetails.fetch(locator), + # encoder serializes dates, old locations, and instances + encoder=CourseSettingsEncoder + ) + else: # post or put, doesn't matter. + return JsonResponse( + CourseDetails.update_from_json(locator, request.json), + encoder=CourseSettingsEncoder + ) @login_required @ensure_csrf_cookie -def course_config_graders_page(request, org, course, name): +@require_http_methods(("GET", "POST", "PUT", "DELETE")) +@expect_json +def grading_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None, grader_index=None): """ - Send models and views as well as html for editing the course settings to - the client. - - org, course, name: Attributes of the Location for the item to edit + Course Grading policy configuration + GET + html: get the page + json no grader_index: get the CourseGrading model (graceperiod, cutoffs, and graders) + json w/ grader_index: get the specific grader + PUT + json no grader_index: update the Course through the CourseGrading model + json w/ grader_index: create or update the specific grader (create if index out of range) """ - location = get_location_and_verify_access(request, org, course, name) + locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) + if not has_access(request.user, locator): + raise PermissionDenied() - course_module = modulestore().get_item(location) - course_details = CourseGradingModel.fetch(location) + if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': + course_old_location = loc_mapper().translate_locator_to_location(locator) + course_module = modulestore().get_item(course_old_location) + course_details = CourseGradingModel.fetch(locator) - return render_to_response('settings_graders.html', { - 'context_course': course_module, - 'course_location': location, - 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder) - }) + return render_to_response('settings_graders.html', { + 'context_course': course_module, + 'course_locator': locator, + 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder), + 'grading_url': locator.url_reverse('/settings/grading/'), + }) + elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): + if request.method == 'GET': + if grader_index is None: + return JsonResponse( + CourseGradingModel.fetch(locator), + # encoder serializes dates, old locations, and instances + encoder=CourseSettingsEncoder + ) + else: + return JsonResponse(CourseGradingModel.fetch_grader(locator, grader_index)) + elif request.method in ('POST', 'PUT'): # post or put, doesn't matter. + # None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader + if grader_index is None: + return JsonResponse( + CourseGradingModel.update_from_json(locator, request.json), + encoder=CourseSettingsEncoder + ) + else: + return JsonResponse( + CourseGradingModel.update_grader_from_json(locator, request.json) + ) + elif request.method == "DELETE" and grader_index is not None: + CourseGradingModel.delete_grader(locator, grader_index) + return JsonResponse() @login_required @@ -460,75 +508,11 @@ def course_config_advanced_page(request, org, course, name): return render_to_response('settings_advanced.html', { 'context_course': course_module, 'course_location': location, + 'course_locator': loc_mapper().translate_location(location.course_id, location, False, True), 'advanced_dict': json.dumps(CourseMetadata.fetch(location)), }) -@expect_json -@login_required -@ensure_csrf_cookie -def course_settings_updates(request, org, course, name, section): - """ - Restful CRUD operations on course settings. This differs from - get_course_settings by communicating purely through json (not rendering any - html) and handles section level operations rather than whole page. - - org, course: Attributes of the Location for the item to edit - section: one of details, faculty, grading, problems, discussions - """ - get_location_and_verify_access(request, org, course, name) - - if section == 'details': - manager = CourseDetails - elif section == 'grading': - manager = CourseGradingModel - else: - return - - if request.method == 'GET': - # Cannot just do a get w/o knowing the course name :-( - return JsonResponse( - manager.fetch(Location(['i4x', org, course, 'course', name])), - encoder=CourseSettingsEncoder - ) - elif request.method in ('POST', 'PUT'): # post or put, doesn't matter. - return JsonResponse( - manager.update_from_json(request.json), - encoder=CourseSettingsEncoder - ) - - -@expect_json -@require_http_methods(("GET", "POST", "PUT", "DELETE")) -@login_required -@ensure_csrf_cookie -def course_grader_updates(request, org, course, name, grader_index=None): - """ - Restful CRUD operations on course_info updates. This differs from - get_course_settings by communicating purely through json (not rendering any - html) and handles section level operations rather than whole page. - - org, course: Attributes of the Location for the item to edit - """ - - location = get_location_and_verify_access(request, org, course, name) - - if request.method == 'GET': - # Cannot just do a get w/o knowing the course name :-( - return JsonResponse(CourseGradingModel.fetch_grader( - Location(location), grader_index - )) - elif request.method == "DELETE": - # ??? Should this return anything? Perhaps success fail? - CourseGradingModel.delete_grader(Location(location), grader_index) - return JsonResponse() - else: # post or put, doesn't matter. - return JsonResponse(CourseGradingModel.update_grader_from_json( - Location(location), - request.json - )) - - @require_http_methods(("GET", "POST", "PUT", "DELETE")) @login_required @ensure_csrf_cookie diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index d33d00377b..5d6f560dcc 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -31,6 +31,8 @@ from django.http import HttpResponseBadRequest from xblock.fields import Scope from preview import handler_prefix, get_preview_html from mitxmako.shortcuts import render_to_response, render_to_string +from django.views.decorators.csrf import ensure_csrf_cookie +from models.settings.course_grading import CourseGradingModel __all__ = ['orphan_handler', 'xblock_handler'] @@ -55,18 +57,20 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= all children and "all_versions" to delete from all (mongo) versions. GET json: returns representation of the xblock (locator id, data, and metadata). + if ?fields=graderType, it returns the graderType for the unit instead of the above. html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view) PUT or POST - json: if xblock location is specified, update the xblock instance. The json payload can contain + json: if xblock locator is specified, update the xblock instance. The json payload can contain these fields, all optional: :data: the new value for the data. :children: the locator ids of children for this xblock. :metadata: new values for the metadata fields. Any whose values are None will be deleted not set to None! Absent ones will be left alone. :nullout: which metadata fields to set to None + :graderType: change how this unit is graded The JSON representation on the updated xblock (minus children) is returned. - if xblock location is not specified, create a new xblock instance. The json playload can contain + if xblock locator is not specified, create a new xblock instance. The json playload can contain these fields: :parent_locator: parent for new xblock, required :category: type of xblock, required @@ -75,14 +79,19 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= The locator (and old-style id) for the created xblock (minus children) is returned. """ if course_id is not None: - location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) - if not has_access(request.user, location): + locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) + if not has_access(request.user, locator): raise PermissionDenied() - old_location = loc_mapper().translate_locator_to_location(location) + old_location = loc_mapper().translate_locator_to_location(locator) if request.method == 'GET': if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): - rsp = _get_module_info(location) + fields = request.REQUEST.get('fields', '').split(',') + if 'graderType' in fields: + # right now can't combine output of this w/ output of _get_module_info, but worthy goal + return JsonResponse(CourseGradingModel.get_section_grader_type(locator)) + # TODO: pass fields to _get_module_info and only return those + rsp = _get_module_info(locator) return JsonResponse(rsp) else: component = modulestore().get_item(old_location) @@ -109,12 +118,13 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= return _delete_item_at_location(old_location, delete_children, delete_all_versions) else: # Since we have a course_id, we are updating an existing xblock. return _save_item( - location, + locator, old_location, data=request.json.get('data'), children=request.json.get('children'), metadata=request.json.get('metadata'), - nullout=request.json.get('nullout') + nullout=request.json.get('nullout'), + grader_type=request.json.get('graderType') ) elif request.method in ('PUT', 'POST'): return _create_item(request) @@ -125,11 +135,15 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= ) -def _save_item(usage_loc, item_location, data=None, children=None, metadata=None, nullout=None): +def _save_item(usage_loc, item_location, data=None, children=None, metadata=None, nullout=None, + grader_type=None + ): """ - Saves certain properties (data, children, metadata, nullout) for a given xblock item. + Saves xblock w/ its fields. Has special processing for grader_type and nullout and Nones in metadata. + nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert + to default). - The item_location is still the old-style location. + The item_location is still the old-style location whereas usage_loc is a BlockUsageLocator """ store = get_modulestore(item_location) @@ -194,12 +208,16 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None if existing_item.category == 'video': manage_video_subtitles_save(existing_item, existing_item) - # Note that children aren't being returned until we have a use case. - return JsonResponse({ + result = { 'id': unicode(usage_loc), 'data': data, 'metadata': own_metadata(existing_item) - }) + } + if grader_type is not None: + result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type)) + + # Note that children aren't being returned until we have a use case. + return JsonResponse(result) @login_required diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py index 99ce00b891..acce1f8079 100644 --- a/cms/djangoapps/models/settings/course_details.py +++ b/cms/djangoapps/models/settings/course_details.py @@ -1,20 +1,25 @@ +import re +import logging +import datetime +import json +from json.encoder import JSONEncoder + from xmodule.modulestore import Location from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.inheritance import own_metadata -import json -from json.encoder import JSONEncoder from contentstore.utils import get_modulestore, course_image_url from models.settings import course_grading from contentstore.utils import update_item from xmodule.fields import Date -import re -import logging -import datetime +from xmodule.modulestore.django import loc_mapper class CourseDetails(object): - def __init__(self, location): - self.course_location = location # a Location obj + def __init__(self, org, course_id, run): + # still need these for now b/c the client's screen shows these 3 fields + self.org = org + self.course_id = course_id + self.run = run self.start_date = None # 'start' self.end_date = None # 'end' self.enrollment_start = None @@ -31,12 +36,9 @@ class CourseDetails(object): """ Fetch the course details for the given course from persistence and return a CourseDetails model. """ - if not isinstance(course_location, Location): - course_location = Location(course_location) - - course = cls(course_location) - - descriptor = get_modulestore(course_location).get_item(course_location) + course_old_location = loc_mapper().translate_locator_to_location(course_location) + descriptor = get_modulestore(course_old_location).get_item(course_old_location) + course = cls(course_old_location.org, course_old_location.course, course_old_location.name) course.start_date = descriptor.start course.end_date = descriptor.end @@ -45,7 +47,7 @@ class CourseDetails(object): course.course_image_name = descriptor.course_image course.course_image_asset_path = course_image_url(descriptor) - temploc = course_location.replace(category='about', name='syllabus') + temploc = course_old_location.replace(category='about', name='syllabus') try: course.syllabus = get_modulestore(temploc).get_item(temploc).data except ItemNotFoundError: @@ -73,14 +75,12 @@ class CourseDetails(object): return course @classmethod - def update_from_json(cls, jsondict): + def update_from_json(cls, course_location, jsondict): """ Decode the json into CourseDetails and save any changed attrs to the db """ - # TODO make it an error for this to be undefined & for it to not be retrievable from modulestore - course_location = Location(jsondict['course_location']) - # Will probably want to cache the inflight courses because every blur generates an update - descriptor = get_modulestore(course_location).get_item(course_location) + course_old_location = loc_mapper().translate_locator_to_location(course_location) + descriptor = get_modulestore(course_old_location).get_item(course_old_location) dirty = False @@ -134,11 +134,11 @@ class CourseDetails(object): # MongoKeyValueStore before we update the mongo datastore. descriptor.save() - get_modulestore(course_location).update_metadata(course_location, own_metadata(descriptor)) + get_modulestore(course_old_location).update_metadata(course_old_location, own_metadata(descriptor)) # NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # to make faster, could compare against db or could have client send over a list of which fields changed. - temploc = Location(course_location).replace(category='about', name='syllabus') + temploc = Location(course_old_location).replace(category='about', name='syllabus') update_item(temploc, jsondict['syllabus']) temploc = temploc.replace(name='overview') @@ -151,7 +151,7 @@ class CourseDetails(object): recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) update_item(temploc, recomposed_video_tag) - # Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm + # Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm # it persisted correctly return CourseDetails.fetch(course_location) @@ -188,6 +188,9 @@ class CourseDetails(object): # TODO move to a more general util? class CourseSettingsEncoder(json.JSONEncoder): + """ + Serialize CourseDetails, CourseGradingModel, datetime, and old Locations + """ def default(self, obj): if isinstance(obj, (CourseDetails, course_grading.CourseGradingModel)): return obj.__dict__ diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index 578961fad6..8af4c44eef 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -1,6 +1,7 @@ -from xmodule.modulestore import Location -from contentstore.utils import get_modulestore from datetime import timedelta +from contentstore.utils import get_modulestore +from xmodule.modulestore.django import loc_mapper +from xblock.fields import Scope class CourseGradingModel(object): @@ -9,22 +10,20 @@ class CourseGradingModel(object): """ # Within this class, allow access to protected members of client classes. # This comes up when accessing kvs data and caches during kvs saves and modulestore writes. - # pylint: disable=W0212 def __init__(self, course_descriptor): - self.course_location = course_descriptor.location - self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100] + self.graders = [ + CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader) + ] # weights transformed to ints [0..100] self.grade_cutoffs = course_descriptor.grade_cutoffs self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor) @classmethod def fetch(cls, course_location): """ - Fetch the course details for the given course from persistence and return a CourseDetails model. + Fetch the course grading policy for the given course from persistence and return a CourseGradingModel. """ - if not isinstance(course_location, Location): - course_location = Location(course_location) - - descriptor = get_modulestore(course_location).get_item(course_location) + course_old_location = loc_mapper().translate_locator_to_location(course_location) + descriptor = get_modulestore(course_old_location).get_item(course_old_location) model = cls(descriptor) return model @@ -35,12 +34,8 @@ class CourseGradingModel(object): Fetch the course's nth grader Returns an empty dict if there's no such grader. """ - if not isinstance(course_location, Location): - course_location = Location(course_location) - - descriptor = get_modulestore(course_location).get_item(course_location) - # # ??? it would be good if these had the course_location in them so that they stand alone sufficiently - # # but that would require not using CourseDescriptor's field directly. Opinions? + course_old_location = loc_mapper().translate_locator_to_location(course_location) + descriptor = get_modulestore(course_old_location).get_item(course_old_location) index = int(index) if len(descriptor.raw_grader) > index: @@ -57,44 +52,22 @@ class CourseGradingModel(object): } @staticmethod - def fetch_cutoffs(course_location): - """ - Fetch the course's grade cutoffs. - """ - if not isinstance(course_location, Location): - course_location = Location(course_location) - - descriptor = get_modulestore(course_location).get_item(course_location) - return descriptor.grade_cutoffs - - @staticmethod - def fetch_grace_period(course_location): - """ - Fetch the course's default grace period. - """ - if not isinstance(course_location, Location): - course_location = Location(course_location) - - descriptor = get_modulestore(course_location).get_item(course_location) - return {'grace_period': CourseGradingModel.convert_set_grace_period(descriptor)} - - @staticmethod - def update_from_json(jsondict): + def update_from_json(course_location, jsondict): """ Decode the json into CourseGradingModel and save any changes. Returns the modified model. Probably not the usual path for updates as it's too coarse grained. """ - course_location = Location(jsondict['course_location']) - descriptor = get_modulestore(course_location).get_item(course_location) + course_old_location = loc_mapper().translate_locator_to_location(course_location) + descriptor = get_modulestore(course_old_location).get_item(course_old_location) + graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']] descriptor.raw_grader = graders_parsed descriptor.grade_cutoffs = jsondict['grade_cutoffs'] - # Save the data that we've just changed to the underlying - # MongoKeyValueStore before we update the mongo datastore. - descriptor.save() - get_modulestore(course_location).update_item(course_location, descriptor.xblock_kvs._data) + get_modulestore(course_old_location).update_item( + course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.content) + ) CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period']) @@ -106,12 +79,8 @@ class CourseGradingModel(object): Create or update the grader of the given type (string key) for the given course. Returns the modified grader which is a full model on the client but not on the server (just a dict) """ - if not isinstance(course_location, Location): - course_location = Location(course_location) - - descriptor = get_modulestore(course_location).get_item(course_location) - # # ??? it would be good if these had the course_location in them so that they stand alone sufficiently - # # but that would require not using CourseDescriptor's field directly. Opinions? + course_old_location = loc_mapper().translate_locator_to_location(course_location) + descriptor = get_modulestore(course_old_location).get_item(course_old_location) # parse removes the id; so, grab it before parse index = int(grader.get('id', len(descriptor.raw_grader))) @@ -122,10 +91,9 @@ class CourseGradingModel(object): else: descriptor.raw_grader.append(grader) - # Save the data that we've just changed to the underlying - # MongoKeyValueStore before we update the mongo datastore. - descriptor.save() - get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data) + get_modulestore(course_old_location).update_item( + course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.content) + ) return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index]) @@ -135,16 +103,13 @@ class CourseGradingModel(object): Create or update the grade cutoffs for the given course. Returns sent in cutoffs (ie., no extra db fetch). """ - if not isinstance(course_location, Location): - course_location = Location(course_location) - - descriptor = get_modulestore(course_location).get_item(course_location) + course_old_location = loc_mapper().translate_locator_to_location(course_location) + descriptor = get_modulestore(course_old_location).get_item(course_old_location) descriptor.grade_cutoffs = cutoffs - # Save the data that we've just changed to the underlying - # MongoKeyValueStore before we update the mongo datastore. - descriptor.save() - get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data) + get_modulestore(course_old_location).update_item( + course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.content) + ) return cutoffs @@ -155,8 +120,8 @@ class CourseGradingModel(object): grace_period entry in an enclosing dict. It is also safe to call this method with a value of None for graceperiodjson. """ - if not isinstance(course_location, Location): - course_location = Location(course_location) + course_old_location = loc_mapper().translate_locator_to_location(course_location) + descriptor = get_modulestore(course_old_location).get_item(course_old_location) # Before a graceperiod has ever been created, it will be None (once it has been # created, it cannot be set back to None). @@ -164,81 +129,67 @@ class CourseGradingModel(object): if 'grace_period' in graceperiodjson: graceperiodjson = graceperiodjson['grace_period'] - # lms requires these to be in a fixed order grace_timedelta = timedelta(**graceperiodjson) - - descriptor = get_modulestore(course_location).get_item(course_location) descriptor.graceperiod = grace_timedelta - # Save the data that we've just changed to the underlying - # MongoKeyValueStore before we update the mongo datastore. - descriptor.save() - get_modulestore(course_location).update_metadata(course_location, descriptor._field_data._kvs._metadata) + get_modulestore(course_old_location).update_metadata( + course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.settings) + ) @staticmethod def delete_grader(course_location, index): """ Delete the grader of the given type from the given course. """ - if not isinstance(course_location, Location): - course_location = Location(course_location) + course_old_location = loc_mapper().translate_locator_to_location(course_location) + descriptor = get_modulestore(course_old_location).get_item(course_old_location) - descriptor = get_modulestore(course_location).get_item(course_location) index = int(index) if index < len(descriptor.raw_grader): del descriptor.raw_grader[index] # force propagation to definition descriptor.raw_grader = descriptor.raw_grader - # Save the data that we've just changed to the underlying - # MongoKeyValueStore before we update the mongo datastore. - descriptor.save() - get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data) + get_modulestore(course_old_location).update_item( + course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.content) + ) @staticmethod def delete_grace_period(course_location): """ - Delete the course's default grace period. + Delete the course's grace period. """ - if not isinstance(course_location, Location): - course_location = Location(course_location) + course_old_location = loc_mapper().translate_locator_to_location(course_location) + descriptor = get_modulestore(course_old_location).get_item(course_old_location) - descriptor = get_modulestore(course_location).get_item(course_location) del descriptor.graceperiod - # Save the data that we've just changed to the underlying - # MongoKeyValueStore before we update the mongo datastore. - descriptor.save() - get_modulestore(course_location).update_metadata(course_location, descriptor._field_data._kvs._metadata) + get_modulestore(course_old_location).update_metadata( + course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.settings) + ) @staticmethod def get_section_grader_type(location): - if not isinstance(location, Location): - location = Location(location) - - descriptor = get_modulestore(location).get_item(location) - return {"graderType": descriptor.format if descriptor.format is not None else 'Not Graded', - "location": location, - "id": 99 # just an arbitrary value to - } + old_location = loc_mapper().translate_locator_to_location(location) + descriptor = get_modulestore(old_location).get_item(old_location) + return { + "graderType": descriptor.format if descriptor.format is not None else 'Not Graded', + "location": unicode(location), + } @staticmethod - def update_section_grader_type(location, jsondict): - if not isinstance(location, Location): - location = Location(location) - - descriptor = get_modulestore(location).get_item(location) - if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded": - descriptor.format = jsondict.get('graderType') + def update_section_grader_type(descriptor, grader_type): + if grader_type is not None and grader_type != u"Not Graded": + descriptor.format = grader_type descriptor.graded = True else: del descriptor.format del descriptor.graded - # Save the data that we've just changed to the underlying - # MongoKeyValueStore before we update the mongo datastore. - descriptor.save() - get_modulestore(location).update_metadata(location, descriptor._field_data._kvs._metadata) + get_modulestore(descriptor.location).update_metadata( + descriptor.location, descriptor.get_explicitly_set_fields_by_scope(Scope.settings) + ) + return {'graderType': grader_type} @staticmethod def convert_set_grace_period(descriptor): diff --git a/cms/static/coffee/spec/views/overview_spec.coffee b/cms/static/coffee/spec/views/overview_spec.coffee index 9ece1b0059..cbc0821213 100644 --- a/cms/static/coffee/spec/views/overview_spec.coffee +++ b/cms/static/coffee/spec/views/overview_spec.coffee @@ -36,7 +36,7 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base appendSetFixtures """
-
diff --git a/cms/static/js/collections/course_grader.js b/cms/static/js/collections/course_grader.js index c4adf64e1f..7dde698cb2 100644 --- a/cms/static/js/collections/course_grader.js +++ b/cms/static/js/collections/course_grader.js @@ -2,10 +2,6 @@ define(["backbone", "js/models/settings/course_grader"], function(Backbone, Cour var CourseGraderCollection = Backbone.Collection.extend({ model : CourseGrader, - course_location : null, // must be set to a Location object - url : function() { - return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/settings-grading/' + this.course_location.get('name') + '/'; - }, sumWeights : function() { return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0); } diff --git a/cms/static/js/models/assignment_grade.js b/cms/static/js/models/assignment_grade.js index 4c3d54b976..83f00a7d10 100644 --- a/cms/static/js/models/assignment_grade.js +++ b/cms/static/js/models/assignment_grade.js @@ -1,26 +1,14 @@ -define(["backbone", "underscore", "js/models/location"], function(Backbone, _, Location) { +define(["backbone", "underscore"], function(Backbone, _) { var AssignmentGrade = Backbone.Model.extend({ defaults : { - graderType : null, // the type label (string). May be "Not Graded" which implies None. I'd like to use id but that's ephemeral - location : null // A location object + graderType : null, // the type label (string). May be "Not Graded" which implies None. + locator : null // locator for the block }, - initialize : function(attrs) { - if (attrs['assignmentUrl']) { - this.set('location', new Location(attrs['assignmentUrl'], {parse: true})); - } - }, - parse : function(attrs) { - if (attrs && attrs['location']) { - attrs.location = new Location(attrs['location'], {parse: true}); - } - }, - urlRoot : function() { - if (this.has('location')) { - var location = this.get('location'); - return '/' + location.get('org') + "/" + location.get('course') + '/' + location.get('category') + '/' - + location.get('name') + '/gradeas/'; - } - else return ""; + idAttribute: 'locator', + urlRoot : '/xblock/', + url: function() { + // add ?fields=graderType to the request url (only needed for fetch, but innocuous for others) + return Backbone.Model.prototype.url.apply(this) + '?' + $.param({fields: 'graderType'}); } }); return AssignmentGrade; diff --git a/cms/static/js/models/settings/course_details.js b/cms/static/js/models/settings/course_details.js index 13cc4ce692..058cacadd7 100644 --- a/cms/static/js/models/settings/course_details.js +++ b/cms/static/js/models/settings/course_details.js @@ -1,8 +1,10 @@ -define(["backbone", "underscore", "gettext", "js/models/location"], function(Backbone, _, gettext, Location) { +define(["backbone", "underscore", "gettext"], function(Backbone, _, gettext) { var CourseDetails = Backbone.Model.extend({ defaults: { - location : null, // the course's Location model, required + org : '', + course_id: '', + run: '', start_date: null, // maps to 'start' end_date: null, // maps to 'end' enrollment_start: null, @@ -17,9 +19,6 @@ var CourseDetails = Backbone.Model.extend({ // When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset) parse: function(attributes) { - if (attributes['course_location']) { - attributes.location = new Location(attributes.course_location, {parse:true}); - } if (attributes['start_date']) { attributes.start_date = new Date(attributes.start_date); } diff --git a/cms/static/js/models/settings/course_grading_policy.js b/cms/static/js/models/settings/course_grading_policy.js index 1e23a4ecf4..d034aa2cef 100644 --- a/cms/static/js/models/settings/course_grading_policy.js +++ b/cms/static/js/models/settings/course_grading_policy.js @@ -3,15 +3,11 @@ define(["backbone", "js/models/location", "js/collections/course_grader"], var CourseGradingPolicy = Backbone.Model.extend({ defaults : { - course_location : null, graders : null, // CourseGraderCollection grade_cutoffs : null, // CourseGradeCutoff model grace_period : null // either null or { hours: n, minutes: m, ...} }, parse: function(attributes) { - if (attributes['course_location']) { - attributes.course_location = new Location(attributes.course_location, {parse:true}); - } if (attributes['graders']) { var graderCollection; // interesting race condition: if {parse:true} when newing, then parse called before .attributes created @@ -21,7 +17,6 @@ var CourseGradingPolicy = Backbone.Model.extend({ } else { graderCollection = new CourseGraderCollection(attributes.graders, {parse:true}); - graderCollection.course_location = attributes['course_location'] || this.get('course_location'); } attributes.graders = graderCollection; } @@ -35,10 +30,6 @@ var CourseGradingPolicy = Backbone.Model.extend({ } return attributes; }, - url : function() { - var location = this.get('course_location'); - return '/' + location.get('org') + "/" + location.get('course') + '/settings-details/' + location.get('name') + '/section/grading'; - }, gracePeriodToDate : function() { var newDate = new Date(); if (this.has('grace_period') && this.get('grace_period')['hours']) diff --git a/cms/static/js/views/overview_assignment_grader.js b/cms/static/js/views/overview_assignment_grader.js index 40e9349693..b7b501f572 100644 --- a/cms/static/js/views/overview_assignment_grader.js +++ b/cms/static/js/views/overview_assignment_grader.js @@ -21,7 +21,7 @@ define(["backbone", "underscore", "gettext", "js/models/assignment_grade", "js/v '
  • Not Graded
  • ' + ''); this.assignmentGrade = new AssignmentGrade({ - assignmentUrl : this.$el.closest('.id-holder').data('id'), + locator : this.$el.closest('.id-holder').data('locator'), graderType : this.$el.data('initial-status')}); // TODO throw exception if graders is null this.graders = this.options['graders']; diff --git a/cms/static/js/views/settings/main.js b/cms/static/js/views/settings/main.js index ded2781f66..63776829c3 100644 --- a/cms/static/js/views/settings/main.js +++ b/cms/static/js/views/settings/main.js @@ -21,9 +21,9 @@ var DetailsView = ValidatingView.extend({ initialize : function() { this.fileAnchorTemplate = _.template(' <%= filename %>'); // fill in fields - this.$el.find("#course-name").val(this.model.get('location').get('name')); - this.$el.find("#course-organization").val(this.model.get('location').get('org')); - this.$el.find("#course-number").val(this.model.get('location').get('course')); + this.$el.find("#course-organization").val(this.model.get('org')); + this.$el.find("#course-number").val(this.model.get('course_id')); + this.$el.find("#course-name").val(this.model.get('run')); this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' }); // Avoid showing broken image on mistyped/nonexistent image diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index d4676fb9fb..7947d9dd43 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -31,7 +31,7 @@