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 327e75c7f4..fd3344f76b 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
@@ -263,12 +255,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])
@@ -286,18 +276,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,
@@ -324,28 +314,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):
@@ -377,8 +345,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 """