Commit 7ab31f54 by Don Mitchell

Merge pull request #1710 from edx/dhm/restful_settings

Restful course settings
parents 7e508248 17864353
...@@ -1572,8 +1572,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1572,8 +1572,7 @@ class ContentStoreTest(ModuleStoreTestCase):
status_code=200, status_code=200,
html=True 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): def test_course_overview_view_with_course(self):
"""Test viewing the course overview page with an existing course""" """Test viewing the course overview page with an existing course"""
...@@ -1656,23 +1655,8 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1656,23 +1655,8 @@ class ContentStoreTest(ModuleStoreTestCase):
test_get_html('checklists') test_get_html('checklists')
test_get_html('assets') test_get_html('assets')
test_get_html('tabs') test_get_html('tabs')
test_get_html('settings/details')
# settings_details test_get_html('settings/grading')
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)
# advanced settings # advanced settings
resp = self.client.get_html(reverse('course_advanced_settings', resp = self.client.get_html(reverse('course_advanced_settings',
......
...@@ -3,7 +3,6 @@ Unit tests for getting the list of courses and the course outline. ...@@ -3,7 +3,6 @@ Unit tests for getting the list of courses and the course outline.
""" """
import json import json
import lxml import lxml
from django.core.urlresolvers import reverse
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.django import loc_mapper
...@@ -60,8 +59,7 @@ class TestCourseIndex(CourseTestCase): ...@@ -60,8 +59,7 @@ class TestCourseIndex(CourseTestCase):
""" """
Test the error conditions for the access Test the error conditions for the access
""" """
locator = loc_mapper().translate_location(self.course.location.course_id, self.course.location, False, True) outline_url = self.course_locator.url_reverse('course/', '')
outline_url = locator.url_reverse('course/', '')
# register a non-staff member and try to delete the course branch # register a non-staff member and try to delete the course branch
non_staff_client, _ = self.createNonStaffAuthedUserClient() non_staff_client, _ = self.createNonStaffAuthedUserClient()
response = non_staff_client.delete(outline_url, {}, HTTP_ACCEPT='application/json') response = non_staff_client.delete(outline_url, {}, HTTP_ACCEPT='application/json')
......
...@@ -6,7 +6,6 @@ import json ...@@ -6,7 +6,6 @@ import json
import copy import copy
import mock import mock
from django.core.urlresolvers import reverse
from django.utils.timezone import UTC from django.utils.timezone import UTC
from django.test.utils import override_settings from django.test.utils import override_settings
...@@ -21,6 +20,7 @@ from models.settings.course_metadata import CourseMetadata ...@@ -21,6 +20,7 @@ from models.settings.course_metadata import CourseMetadata
from xmodule.fields import Date from xmodule.fields import Date
from .utils import CourseTestCase from .utils import CourseTestCase
from xmodule.modulestore.django import loc_mapper
class CourseDetailsTestCase(CourseTestCase): class CourseDetailsTestCase(CourseTestCase):
...@@ -28,8 +28,10 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -28,8 +28,10 @@ class CourseDetailsTestCase(CourseTestCase):
Tests the first course settings page (course dates, overview, etc.). Tests the first course settings page (course dates, overview, etc.).
""" """
def test_virgin_fetch(self): def test_virgin_fetch(self):
details = CourseDetails.fetch(self.course.location) details = CourseDetails.fetch(self.course_locator)
self.assertEqual(details.course_location, self.course.location, "Location not copied into") 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.assertEqual(details.course_image_name, self.course.course_image)
self.assertIsNotNone(details.start_date.tzinfo) self.assertIsNotNone(details.start_date.tzinfo)
self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date)) self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date))
...@@ -40,10 +42,9 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -40,10 +42,9 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort)) self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
def test_encoder(self): def test_encoder(self):
details = CourseDetails.fetch(self.course.location) details = CourseDetails.fetch(self.course_locator)
jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
jsondetails = json.loads(jsondetails) 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.assertEqual(jsondetails['course_image_name'], self.course.course_image)
self.assertIsNone(jsondetails['end_date'], "end date somehow initialized ") self.assertIsNone(jsondetails['end_date'], "end date somehow initialized ")
self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ") self.assertIsNone(jsondetails['enrollment_start'], "enrollment_start date somehow initialized ")
...@@ -57,7 +58,6 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -57,7 +58,6 @@ class CourseDetailsTestCase(CourseTestCase):
Test the encoder out of its original constrained purpose to see if it functions for general use Test the encoder out of its original constrained purpose to see if it functions for general use
""" """
details = { details = {
'location': Location(['tag', 'org', 'course', 'category', 'name']),
'number': 1, 'number': 1,
'string': 'string', 'string': 'string',
'datetime': datetime.datetime.now(UTC()) 'datetime': datetime.datetime.now(UTC())
...@@ -65,59 +65,49 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -65,59 +65,49 @@ class CourseDetailsTestCase(CourseTestCase):
jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
jsondetails = json.loads(jsondetails) 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.assertEquals(1, jsondetails['number'])
self.assertEqual(jsondetails['string'], 'string') self.assertEqual(jsondetails['string'], 'string')
def test_update_and_fetch(self): def test_update_and_fetch(self):
jsondetails = CourseDetails.fetch(self.course.location) jsondetails = CourseDetails.fetch(self.course_locator)
jsondetails.syllabus = "<a href='foo'>bar</a>" jsondetails.syllabus = "<a href='foo'>bar</a>"
# encode - decode to convert date fields and other data which changes form # encode - decode to convert date fields and other data which changes form
self.assertEqual( 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.syllabus, "After set syllabus"
) )
jsondetails.overview = "Overview" jsondetails.overview = "Overview"
self.assertEqual( 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.overview, "After set overview"
) )
jsondetails.intro_video = "intro_video" jsondetails.intro_video = "intro_video"
self.assertEqual( 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.intro_video, "After set intro_video"
) )
jsondetails.effort = "effort" jsondetails.effort = "effort"
self.assertEqual( 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.effort, "After set effort"
) )
jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC()) jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC())
self.assertEqual( 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.start_date
) )
jsondetails.course_image_name = "an_image.jpg" jsondetails.course_image_name = "an_image.jpg"
self.assertEqual( 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 jsondetails.course_image_name
) )
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) @override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
def test_marketing_site_fetch(self): def test_marketing_site_fetch(self):
settings_details_url = reverse( settings_details_url = self.course_locator.url_reverse('settings/details/')
'settings_details',
kwargs={
'org': self.course.location.org,
'name': self.course.location.name,
'course': self.course.location.course
}
)
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}): 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, "Course Summary Page")
self.assertNotContains(response, "Send a note to students via email") self.assertNotContains(response, "Send a note to students via email")
self.assertContains(response, "course summary page will not be viewable") self.assertContains(response, "course summary page will not be viewable")
...@@ -135,17 +125,10 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -135,17 +125,10 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertNotContains(response, "Requirements") self.assertNotContains(response, "Requirements")
def test_regular_site_fetch(self): def test_regular_site_fetch(self):
settings_details_url = reverse( settings_details_url = self.course_locator.url_reverse('settings/details/')
'settings_details',
kwargs={
'org': self.course.location.org,
'name': self.course.location.name,
'course': self.course.location.course
}
)
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}): 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, "Course Summary Page")
self.assertContains(response, "Send a note to students via email") self.assertContains(response, "Send a note to students via email")
self.assertNotContains(response, "course summary page will not be viewable") self.assertNotContains(response, "course summary page will not be viewable")
...@@ -168,10 +151,12 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -168,10 +151,12 @@ class CourseDetailsViewTest(CourseTestCase):
Tests for modifying content on the first course settings page (course dates, overview, etc.). Tests for modifying content on the first course settings page (course dates, overview, etc.).
""" """
def alter_field(self, url, details, field, val): 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) setattr(details, field, val)
# Need to partially serialize payload b/c the mock doesn't handle it correctly # Need to partially serialize payload b/c the mock doesn't handle it correctly
payload = copy.copy(details.__dict__) payload = copy.copy(details.__dict__)
payload['course_location'] = details.course_location.url()
payload['start_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.start_date) payload['start_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.start_date)
payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date) payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date)
payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start) payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start)
...@@ -181,16 +166,17 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -181,16 +166,17 @@ class CourseDetailsViewTest(CourseTestCase):
@staticmethod @staticmethod
def convert_datetime_to_iso(datetime_obj): def convert_datetime_to_iso(datetime_obj):
"""
Use the xblock serializer to convert the datetime
"""
return Date().to_json(datetime_obj) return Date().to_json(datetime_obj)
def test_update_and_fetch(self): def test_update_and_fetch(self):
loc = self.course.location details = CourseDetails.fetch(self.course_locator)
details = CourseDetails.fetch(loc)
# resp s/b json from here on # resp s/b json from here on
url = reverse('course_settings', kwargs={'org': loc.org, 'course': loc.course, url = self.course_locator.url_reverse('settings/details/')
'name': loc.name, 'section': 'details'}) resp = self.client.get_json(url)
resp = self.client.get(url)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get") self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
utc = UTC() utc = UTC()
...@@ -206,6 +192,9 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -206,6 +192,9 @@ class CourseDetailsViewTest(CourseTestCase):
self.alter_field(url, details, 'course_image_name', "course_image_name") self.alter_field(url, details, 'course_image_name', "course_image_name")
def compare_details_with_encoding(self, encoded, details, context): 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, 'start_date')
self.compare_date_fields(details, encoded, context, 'end_date') self.compare_date_fields(details, encoded, context, 'end_date')
self.compare_date_fields(details, encoded, context, 'enrollment_start') self.compare_date_fields(details, encoded, context, 'enrollment_start')
...@@ -216,6 +205,9 @@ class CourseDetailsViewTest(CourseTestCase): ...@@ -216,6 +205,9 @@ class CourseDetailsViewTest(CourseTestCase):
self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==") self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==")
def compare_date_fields(self, details, encoded, context, field): 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: if details[field] is not None:
date = Date() date = Date()
if field in encoded and encoded[field] is not None: if field in encoded and encoded[field] is not None:
...@@ -234,142 +226,191 @@ class CourseGradingTest(CourseTestCase): ...@@ -234,142 +226,191 @@ class CourseGradingTest(CourseTestCase):
Tests for the course settings grading page. Tests for the course settings grading page.
""" """
def test_initial_grader(self): def test_initial_grader(self):
descriptor = get_modulestore(self.course.location).get_item(self.course.location) test_grader = CourseGradingModel(self.course)
test_grader = CourseGradingModel(descriptor) self.assertIsNotNone(test_grader.graders)
# ??? How much should this test bake in expectations about defaults and thus fail if defaults change? self.assertIsNotNone(test_grader.grade_cutoffs)
self.assertEqual(self.course.location, test_grader.course_location, "Course locations")
self.assertIsNotNone(test_grader.graders, "No graders")
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
def test_fetch_grader(self): def test_fetch_grader(self):
test_grader = CourseGradingModel.fetch(self.course.location.url()) test_grader = CourseGradingModel.fetch(self.course_locator)
self.assertEqual(self.course.location, test_grader.course_location, "Course locations")
self.assertIsNotNone(test_grader.graders, "No graders")
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
test_grader = CourseGradingModel.fetch(self.course.location)
self.assertEqual(self.course.location, test_grader.course_location, "Course locations")
self.assertIsNotNone(test_grader.graders, "No graders") self.assertIsNotNone(test_grader.graders, "No graders")
self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs") self.assertIsNotNone(test_grader.grade_cutoffs, "No cutoffs")
for i, grader in enumerate(test_grader.graders): 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") 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): def test_update_from_json(self):
test_grader = CourseGradingModel.fetch(self.course.location) test_grader = CourseGradingModel.fetch(self.course_locator)
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__, "Noop update") self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Noop update")
test_grader.graders[0]['weight'] = test_grader.graders[0].get('weight') * 2 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") self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "Weight[0] * 2")
test_grader.grade_cutoffs['D'] = 0.3 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") self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0} 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") self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
def test_update_grader_from_json(self): def test_update_grader_from_json(self):
test_grader = CourseGradingModel.fetch(self.course.location) test_grader = CourseGradingModel.fetch(self.course_locator)
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, "Noop update") self.assertDictEqual(test_grader.graders[1], altered_grader, "Noop update")
test_grader.graders[1]['min_count'] = test_grader.graders[1].get('min_count') + 2 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") 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 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") self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
def test_update_cutoffs_from_json(self): def test_update_cutoffs_from_json(self):
test_grader = CourseGradingModel.fetch(self.course.location) test_grader = CourseGradingModel.fetch(self.course_locator)
CourseGradingModel.update_cutoffs_from_json(test_grader.course_location, test_grader.grade_cutoffs) 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 # 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. # 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") self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "Noop update")
test_grader.grade_cutoffs['D'] = 0.3 test_grader.grade_cutoffs['D'] = 0.3
CourseGradingModel.update_cutoffs_from_json(test_grader.course_location, test_grader.grade_cutoffs) CourseGradingModel.update_cutoffs_from_json(self.course_locator, test_grader.grade_cutoffs)
altered_grader = CourseGradingModel.fetch(self.course.location) altered_grader = CourseGradingModel.fetch(self.course_locator)
self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff add D") self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff add D")
test_grader.grade_cutoffs['Pass'] = 0.75 test_grader.grade_cutoffs['Pass'] = 0.75
CourseGradingModel.update_cutoffs_from_json(test_grader.course_location, test_grader.grade_cutoffs) CourseGradingModel.update_cutoffs_from_json(self.course_locator, test_grader.grade_cutoffs)
altered_grader = CourseGradingModel.fetch(self.course.location) altered_grader = CourseGradingModel.fetch(self.course_locator)
self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff change 'Pass'") self.assertDictEqual(test_grader.grade_cutoffs, altered_grader.grade_cutoffs, "cutoff change 'Pass'")
def test_delete_grace_period(self): def test_delete_grace_period(self):
test_grader = CourseGradingModel.fetch(self.course.location) test_grader = CourseGradingModel.fetch(self.course_locator)
CourseGradingModel.update_grace_period_from_json(test_grader.course_location, test_grader.grace_period) 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. # 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") self.assertEqual(test_grader.grace_period, altered_grader.grace_period, "Noop update")
test_grader.grace_period = {'hours': 15, 'minutes': 5, 'seconds': 30} test_grader.grace_period = {'hours': 15, 'minutes': 5, 'seconds': 30}
CourseGradingModel.update_grace_period_from_json(test_grader.course_location, test_grader.grace_period) CourseGradingModel.update_grace_period_from_json(self.course_locator, test_grader.grace_period)
altered_grader = CourseGradingModel.fetch(self.course.location) altered_grader = CourseGradingModel.fetch(self.course_locator)
self.assertDictEqual(test_grader.grace_period, altered_grader.grace_period, "Adding in a grace period") 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} test_grader.grace_period = {'hours': 1, 'minutes': 10, 'seconds': 0}
# Now delete the grace period # 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. # 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 # Once deleted, the grace period should simply be None
self.assertEqual(None, altered_grader.grace_period, "Delete grace period") self.assertEqual(None, altered_grader.grace_period, "Delete grace period")
def test_update_section_grader_type(self): def test_update_section_grader_type(self):
# Get the descriptor and the section_grader_type and assert they are the default values # 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) 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('Not Graded', section_grader_type['graderType'])
self.assertEqual(None, descriptor.format) self.assertEqual(None, descriptor.format)
self.assertEqual(False, descriptor.graded) self.assertEqual(False, descriptor.graded)
# Change the default grader type to Homework, which should also mark the section as 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) 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', section_grader_type['graderType'])
self.assertEqual('Homework', descriptor.format) self.assertEqual('Homework', descriptor.format)
self.assertEqual(True, descriptor.graded) self.assertEqual(True, descriptor.graded)
# Change the grader type back to Not Graded, which should also unmark the section as 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) 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('Not Graded', section_grader_type['graderType'])
self.assertEqual(None, descriptor.format) self.assertEqual(None, descriptor.format)
self.assertEqual(False, descriptor.graded) 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): class CourseMetadataEditingTest(CourseTestCase):
""" """
...@@ -436,25 +477,52 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -436,25 +477,52 @@ class CourseMetadataEditingTest(CourseTestCase):
class CourseGraderUpdatesTest(CourseTestCase): class CourseGraderUpdatesTest(CourseTestCase):
"""
Test getting, deleting, adding, & updating graders
"""
def setUp(self): def setUp(self):
"""Compute the url to use in tests"""
super(CourseGraderUpdatesTest, self).setUp() super(CourseGraderUpdatesTest, self).setUp()
self.url = reverse("course_settings", kwargs={ self.url = self.course_locator.url_reverse('settings/grading')
'org': self.course.location.org, self.starting_graders = CourseGradingModel(self.course).graders
'course': self.course.location.course,
'name': self.course.location.name,
'grader_index': 0,
})
def test_get(self): 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) self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content) obj = json.loads(resp.content)
self.assertEqual(self.starting_graders[0], obj)
def test_delete(self): 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) 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 = { grader = {
"type": "manual", "type": "manual",
"min_count": 5, "min_count": 5,
...@@ -462,6 +530,11 @@ class CourseGraderUpdatesTest(CourseTestCase): ...@@ -462,6 +530,11 @@ class CourseGraderUpdatesTest(CourseTestCase):
"short_label": "yo momma", "short_label": "yo momma",
"weight": 17.3, "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) self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content) 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))
...@@ -10,8 +10,9 @@ from django.test.client import Client ...@@ -10,8 +10,9 @@ from django.test.client import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase 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 contentstore.tests.modulestore_config import TEST_MODULESTORE
from xmodule.modulestore.django import loc_mapper
def parse_json(response): def parse_json(response):
...@@ -41,6 +42,7 @@ class AjaxEnabledTestClient(Client): ...@@ -41,6 +42,7 @@ class AjaxEnabledTestClient(Client):
if not isinstance(data, basestring): if not isinstance(data, basestring):
data = json.dumps(data or {}) data = json.dumps(data or {})
kwargs.setdefault("HTTP_X_REQUESTED_WITH", "XMLHttpRequest") 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) return self.post(path=path, data=data, content_type=content_type, **kwargs)
def get_html(self, path, data=None, follow=False, **extra): def get_html(self, path, data=None, follow=False, **extra):
...@@ -88,6 +90,9 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -88,6 +90,9 @@ class CourseTestCase(ModuleStoreTestCase):
display_name='Robot Super Course', display_name='Robot Super Course',
) )
self.course_location = self.course.location 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): def createNonStaffAuthedUserClient(self):
""" """
...@@ -106,3 +111,16 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -106,3 +111,16 @@ class CourseTestCase(ModuleStoreTestCase):
client = Client() client = Client()
client.login(username=uname, password=password) client.login(username=uname, password=password)
return client, nonstaff 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'])
...@@ -5,7 +5,6 @@ from util.json_request import JsonResponse ...@@ -5,7 +5,6 @@ from util.json_request import JsonResponse
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.core.urlresolvers import reverse
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from django.http import HttpResponseNotFound from django.http import HttpResponseNotFound
...@@ -22,6 +21,8 @@ from xmodule.modulestore.locator import BlockUsageLocator ...@@ -22,6 +21,8 @@ from xmodule.modulestore.locator import BlockUsageLocator
__all__ = ['checklists_handler'] __all__ = ['checklists_handler']
# pylint: disable=unused-argument
@require_http_methods(("GET", "POST", "PUT")) @require_http_methods(("GET", "POST", "PUT"))
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -85,8 +86,8 @@ def checklists_handler(request, tag=None, course_id=None, branch=None, version_g ...@@ -85,8 +86,8 @@ def checklists_handler(request, tag=None, course_id=None, branch=None, version_g
return JsonResponse(expanded_checklist) return JsonResponse(expanded_checklist)
else: else:
return HttpResponseBadRequest( return HttpResponseBadRequest(
( "Could not save checklist state because the checklist index " ("Could not save checklist state because the checklist index "
"was out of range or unspecified."), "was out of range or unspecified."),
content_type="text/plain" content_type="text/plain"
) )
else: else:
...@@ -113,14 +114,12 @@ def expand_checklist_action_url(course_module, checklist): ...@@ -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. The method does a copy of the input checklist and does not modify the input argument.
""" """
expanded_checklist = copy.deepcopy(checklist) expanded_checklist = copy.deepcopy(checklist)
oldurlconf_map = {
"SettingsDetails": "settings_details",
"SettingsGrading": "settings_grading"
}
urlconf_map = { urlconf_map = {
"ManageUsers": "course_team", "ManageUsers": "course_team",
"CourseOutline": "course" "CourseOutline": "course",
"SettingsDetails": "settings/details",
"SettingsGrading": "settings/grading",
} }
for item in expanded_checklist.get('items'): for item in expanded_checklist.get('items'):
...@@ -130,12 +129,5 @@ def expand_checklist_action_url(course_module, checklist): ...@@ -130,12 +129,5 @@ def expand_checklist_action_url(course_module, checklist):
ctx_loc = course_module.location ctx_loc = course_module.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True) location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
item['action_url'] = location.url_reverse(url_prefix, '') 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 return expanded_checklist
...@@ -2,15 +2,11 @@ import json ...@@ -2,15 +2,11 @@ import json
import logging import logging
from collections import defaultdict from collections import defaultdict
from django.http import (HttpResponse, HttpResponseBadRequest, from django.http import HttpResponse, HttpResponseBadRequest
HttpResponseForbidden)
from django.contrib.auth.decorators import login_required 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.core.exceptions import PermissionDenied
from django_future.csrf import ensure_csrf_cookie
from django.conf import settings from django.conf import settings
from xmodule.modulestore.exceptions import (ItemNotFoundError, from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
InvalidLocationError)
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -19,7 +15,7 @@ from xmodule.util.date_utils import get_default_time_display ...@@ -19,7 +15,7 @@ from xmodule.util.date_utils import get_default_time_display
from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.django import loc_mapper
from xblock.fields import Scope 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 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', ...@@ -35,7 +31,6 @@ __all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'ADVANCED_COMPONENT_POLICY_KEY', 'ADVANCED_COMPONENT_POLICY_KEY',
'edit_subsection', 'edit_subsection',
'edit_unit', 'edit_unit',
'assignment_type_update',
'create_draft', 'create_draft',
'publish_draft', 'publish_draft',
'unpublish_unit', 'unpublish_unit',
...@@ -75,12 +70,8 @@ def edit_subsection(request, location): ...@@ -75,12 +70,8 @@ def edit_subsection(request, location):
except ItemNotFoundError: except ItemNotFoundError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
lms_link = get_lms_link_for_item( lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
location, course_id=course.location.course_id preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
)
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 # make sure that location references a 'sequential', otherwise return
# BadRequest # BadRequest
...@@ -92,8 +83,8 @@ def edit_subsection(request, location): ...@@ -92,8 +83,8 @@ def edit_subsection(request, location):
# we're for now assuming a single parent # we're for now assuming a single parent
if len(parent_locs) != 1: if len(parent_locs) != 1:
logging.error( logging.error(
'Multiple (or none) parents have been found for %s', 'Multiple (or none) parents have been found for %s',
location location
) )
# this should blow up if we don't find any parents, which would be erroneous # this should blow up if we don't find any parents, which would be erroneous
...@@ -109,7 +100,7 @@ def edit_subsection(request, location): ...@@ -109,7 +100,7 @@ def edit_subsection(request, location):
for field for field
in fields.values() in fields.values()
if field.name not in ['display_name', 'start', 'due', 'format'] if field.name not in ['display_name', 'start', 'due', 'format']
and field.scope == Scope.settings and field.scope == Scope.settings
) )
can_view_live = False can_view_live = False
...@@ -120,6 +111,9 @@ def edit_subsection(request, location): ...@@ -120,6 +111,9 @@ def edit_subsection(request, location):
can_view_live = True can_view_live = True
break break
course_locator = loc_mapper().translate_location(
course.location.course_id, course.location, False, True
)
locator = loc_mapper().translate_location( locator = loc_mapper().translate_location(
course.location.course_id, item.location, False, True course.location.course_id, item.location, False, True
) )
...@@ -127,19 +121,17 @@ def edit_subsection(request, location): ...@@ -127,19 +121,17 @@ def edit_subsection(request, location):
return render_to_response( return render_to_response(
'edit_subsection.html', 'edit_subsection.html',
{ {
'subsection': item, 'subsection': item,
'context_course': course, 'context_course': course,
'new_unit_category': 'vertical', 'new_unit_category': 'vertical',
'lms_link': lms_link, 'lms_link': lms_link,
'preview_link': preview_link, 'preview_link': preview_link,
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders), 'course_graders': json.dumps(CourseGradingModel.fetch(course_locator).graders),
# For grader, which is not yet converted 'parent_item': parent,
'parent_location': course.location, 'locator': locator,
'parent_item': parent, 'policy_metadata': policy_metadata,
'locator': locator, 'subsection_units': subsection_units,
'policy_metadata': policy_metadata, 'can_view_live': can_view_live
'subsection_units': subsection_units,
'can_view_live': can_view_live
} }
) )
...@@ -175,8 +167,8 @@ def edit_unit(request, location): ...@@ -175,8 +167,8 @@ def edit_unit(request, location):
except ItemNotFoundError: except ItemNotFoundError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
lms_link = get_lms_link_for_item( lms_link = get_lms_link_for_item(
item.location, item.location,
course_id=course.location.course_id course_id=course.location.course_id
) )
# Note that the unit_state (draft, public, private) does not match up with the published value # 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): ...@@ -234,7 +226,7 @@ def edit_unit(request, location):
category, category,
False, False,
None # don't override default data None # don't override default data
)) ))
except PluginMissingError: except PluginMissingError:
# dhm: I got this once but it can happen any time the # dhm: I got this once but it can happen any time the
# course author configures an advanced component which does # course author configures an advanced component which does
...@@ -263,12 +255,10 @@ def edit_unit(request, location): ...@@ -263,12 +255,10 @@ def edit_unit(request, location):
# this will need to change to check permissions correctly so as # this will need to change to check permissions correctly so as
# to pick the correct parent subsection # to pick the correct parent subsection
containing_subsection_locs = modulestore().get_parent_locations( containing_subsection_locs = modulestore().get_parent_locations(location, None)
location, None
)
containing_subsection = modulestore().get_item(containing_subsection_locs[0]) containing_subsection = modulestore().get_item(containing_subsection_locs[0])
containing_section_locs = modulestore().get_parent_locations( containing_section_locs = modulestore().get_parent_locations(
containing_subsection.location, None containing_subsection.location, None
) )
containing_section = modulestore().get_item(containing_section_locs[0]) containing_section = modulestore().get_item(containing_section_locs[0])
...@@ -286,18 +276,18 @@ def edit_unit(request, location): ...@@ -286,18 +276,18 @@ def edit_unit(request, location):
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE') preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE')
preview_lms_link = ( preview_lms_link = (
'//{preview_lms_base}/courses/{org}/{course}/' '//{preview_lms_base}/courses/{org}/{course}/'
'{course_name}/courseware/{section}/{subsection}/{index}' '{course_name}/courseware/{section}/{subsection}/{index}'
).format( ).format(
preview_lms_base=preview_lms_base, preview_lms_base=preview_lms_base,
lms_base=settings.LMS_BASE, lms_base=settings.LMS_BASE,
org=course.location.org, org=course.location.org,
course=course.location.course, course=course.location.course,
course_name=course.location.name, course_name=course.location.name,
section=containing_section.location.name, section=containing_section.location.name,
subsection=containing_subsection.location.name, subsection=containing_subsection.location.name,
index=index index=index
) )
return render_to_response('unit.html', { return render_to_response('unit.html', {
'context_course': course, 'context_course': course,
...@@ -324,28 +314,6 @@ def edit_unit(request, location): ...@@ -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 @login_required
@expect_json @expect_json
def create_draft(request): def create_draft(request):
...@@ -377,8 +345,8 @@ def publish_draft(request): ...@@ -377,8 +345,8 @@ def publish_draft(request):
item = modulestore().get_item(location) item = modulestore().get_item(location)
_xmodule_recurse( _xmodule_recurse(
item, item,
lambda i: modulestore().publish(i.location, request.user.id) lambda i: modulestore().publish(i.location, request.user.id)
) )
return HttpResponse() return HttpResponse()
......
...@@ -32,8 +32,7 @@ from contentstore.course_info_model import ( ...@@ -32,8 +32,7 @@ from contentstore.course_info_model import (
from contentstore.utils import ( from contentstore.utils import (
get_lms_link_for_item, add_extra_panel_tab, remove_extra_panel_tab, get_lms_link_for_item, add_extra_panel_tab, remove_extra_panel_tab,
get_modulestore) get_modulestore)
from models.settings.course_details import ( from models.settings.course_details import CourseDetails, CourseSettingsEncoder
CourseDetails, CourseSettingsEncoder)
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
...@@ -53,13 +52,12 @@ from student.models import CourseEnrollment ...@@ -53,13 +52,12 @@ from student.models import CourseEnrollment
from xmodule.html_module import AboutDescriptor from xmodule.html_module import AboutDescriptor
from xmodule.modulestore.locator import BlockUsageLocator from xmodule.modulestore.locator import BlockUsageLocator
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested 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', __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler',
'get_course_settings', 'settings_handler',
'course_config_graders_page', 'grading_handler',
'course_config_advanced_page', 'course_config_advanced_page',
'course_settings_updates',
'course_grader_updates',
'course_advanced_updates', 'textbook_index', 'textbook_by_id', 'course_advanced_updates', 'textbook_index', 'textbook_by_id',
'create_textbook'] 'create_textbook']
...@@ -190,10 +188,8 @@ def course_index(request, course_id, branch, version_guid, block): ...@@ -190,10 +188,8 @@ def course_index(request, course_id, branch, version_guid, block):
'lms_link': lms_link, 'lms_link': lms_link,
'sections': sections, 'sections': sections,
'course_graders': json.dumps( '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, 'parent_locator': location,
'new_section_category': 'chapter', 'new_section_category': 'chapter',
'new_subsection_category': 'sequential', 'new_subsection_category': 'sequential',
...@@ -394,54 +390,106 @@ def course_info_update_handler( ...@@ -394,54 +390,106 @@ def course_info_update_handler(
@login_required @login_required
@ensure_csrf_cookie @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 Course settings for dates and about pages
the client. GET
html: get the page
org, course, name: Attributes of the Location for the item to edit 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 = locator.url_reverse('assets/')
upload_asset_url = new_loc.url_reverse('assets/', '')
return render_to_response('settings.html', { return render_to_response('settings.html', {
'context_course': course_module, 'context_course': course_module,
'course_location': location, 'course_locator': locator,
'details_url': reverse(course_settings_updates, 'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_old_location),
kwargs={"org": org, 'course_image_url': utils.course_image_url(course_module),
"course": course, 'details_url': locator.url_reverse('/settings/details/'),
"name": name, 'about_page_editable': not settings.MITX_FEATURES.get(
"section": "details"}), 'ENABLE_MKTG_SITE', False
'about_page_editable': not settings.MITX_FEATURES.get( ),
'ENABLE_MKTG_SITE', False 'upload_asset_url': upload_asset_url
), })
'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 @login_required
@ensure_csrf_cookie @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 Course Grading policy configuration
the client. GET
html: get the page
org, course, name: Attributes of the Location for the item to edit 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) if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
course_details = CourseGradingModel.fetch(location) 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', { return render_to_response('settings_graders.html', {
'context_course': course_module, 'context_course': course_module,
'course_location': location, 'course_locator': locator,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder) '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 @login_required
...@@ -460,75 +508,11 @@ def course_config_advanced_page(request, org, course, name): ...@@ -460,75 +508,11 @@ def course_config_advanced_page(request, org, course, name):
return render_to_response('settings_advanced.html', { return render_to_response('settings_advanced.html', {
'context_course': course_module, 'context_course': course_module,
'course_location': location, 'course_location': location,
'course_locator': loc_mapper().translate_location(location.course_id, location, False, True),
'advanced_dict': json.dumps(CourseMetadata.fetch(location)), '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")) @require_http_methods(("GET", "POST", "PUT", "DELETE"))
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
......
...@@ -31,6 +31,8 @@ from django.http import HttpResponseBadRequest ...@@ -31,6 +31,8 @@ from django.http import HttpResponseBadRequest
from xblock.fields import Scope from xblock.fields import Scope
from preview import handler_prefix, get_preview_html from preview import handler_prefix, get_preview_html
from mitxmako.shortcuts import render_to_response, render_to_string 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'] __all__ = ['orphan_handler', 'xblock_handler']
...@@ -55,18 +57,20 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -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. all children and "all_versions" to delete from all (mongo) versions.
GET GET
json: returns representation of the xblock (locator id, data, and metadata). 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) html: returns HTML for rendering the xblock (which includes both the "preview" view and the "editor" view)
PUT or POST 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: these fields, all optional:
:data: the new value for the data. :data: the new value for the data.
:children: the locator ids of children for this xblock. :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 :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. to None! Absent ones will be left alone.
:nullout: which metadata fields to set to None :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. 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: these fields:
:parent_locator: parent for new xblock, required :parent_locator: parent for new xblock, required
:category: type of xblock, required :category: type of xblock, required
...@@ -75,14 +79,19 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -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. The locator (and old-style id) for the created xblock (minus children) is returned.
""" """
if course_id is not None: if course_id is not None:
location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
if not has_access(request.user, location): if not has_access(request.user, locator):
raise PermissionDenied() 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 request.method == 'GET':
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'): 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) return JsonResponse(rsp)
else: else:
component = modulestore().get_item(old_location) component = modulestore().get_item(old_location)
...@@ -109,12 +118,13 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -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) 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. else: # Since we have a course_id, we are updating an existing xblock.
return _save_item( return _save_item(
location, locator,
old_location, old_location,
data=request.json.get('data'), data=request.json.get('data'),
children=request.json.get('children'), children=request.json.get('children'),
metadata=request.json.get('metadata'), 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'): elif request.method in ('PUT', 'POST'):
return _create_item(request) return _create_item(request)
...@@ -125,11 +135,15 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid= ...@@ -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) store = get_modulestore(item_location)
...@@ -194,12 +208,16 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None ...@@ -194,12 +208,16 @@ def _save_item(usage_loc, item_location, data=None, children=None, metadata=None
if existing_item.category == 'video': if existing_item.category == 'video':
manage_video_subtitles_save(existing_item, existing_item) manage_video_subtitles_save(existing_item, existing_item)
# Note that children aren't being returned until we have a use case. result = {
return JsonResponse({
'id': unicode(usage_loc), 'id': unicode(usage_loc),
'data': data, 'data': data,
'metadata': own_metadata(existing_item) '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 @login_required
......
import re
import logging
import datetime
import json
from json.encoder import JSONEncoder
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
import json
from json.encoder import JSONEncoder
from contentstore.utils import get_modulestore, course_image_url from contentstore.utils import get_modulestore, course_image_url
from models.settings import course_grading from models.settings import course_grading
from contentstore.utils import update_item from contentstore.utils import update_item
from xmodule.fields import Date from xmodule.fields import Date
import re from xmodule.modulestore.django import loc_mapper
import logging
import datetime
class CourseDetails(object): class CourseDetails(object):
def __init__(self, location): def __init__(self, org, course_id, run):
self.course_location = location # a Location obj # 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.start_date = None # 'start'
self.end_date = None # 'end' self.end_date = None # 'end'
self.enrollment_start = None self.enrollment_start = None
...@@ -31,12 +36,9 @@ class CourseDetails(object): ...@@ -31,12 +36,9 @@ class CourseDetails(object):
""" """
Fetch the course details for the given course from persistence and return a CourseDetails model. Fetch the course details for the given course from persistence and return a CourseDetails model.
""" """
if not isinstance(course_location, Location): course_old_location = loc_mapper().translate_locator_to_location(course_location)
course_location = 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 = cls(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
course.start_date = descriptor.start course.start_date = descriptor.start
course.end_date = descriptor.end course.end_date = descriptor.end
...@@ -45,7 +47,7 @@ class CourseDetails(object): ...@@ -45,7 +47,7 @@ class CourseDetails(object):
course.course_image_name = descriptor.course_image course.course_image_name = descriptor.course_image
course.course_image_asset_path = course_image_url(descriptor) 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: try:
course.syllabus = get_modulestore(temploc).get_item(temploc).data course.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
...@@ -73,14 +75,12 @@ class CourseDetails(object): ...@@ -73,14 +75,12 @@ class CourseDetails(object):
return course return course
@classmethod @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 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_old_location = loc_mapper().translate_locator_to_location(course_location)
course_location = Location(jsondict['course_location']) descriptor = get_modulestore(course_old_location).get_item(course_old_location)
# Will probably want to cache the inflight courses because every blur generates an update
descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False dirty = False
...@@ -134,11 +134,11 @@ class CourseDetails(object): ...@@ -134,11 +134,11 @@ class CourseDetails(object):
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
descriptor.save() 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 # 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. # 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']) update_item(temploc, jsondict['syllabus'])
temploc = temploc.replace(name='overview') temploc = temploc.replace(name='overview')
...@@ -151,7 +151,7 @@ class CourseDetails(object): ...@@ -151,7 +151,7 @@ class CourseDetails(object):
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag) 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 # it persisted correctly
return CourseDetails.fetch(course_location) return CourseDetails.fetch(course_location)
...@@ -188,6 +188,9 @@ class CourseDetails(object): ...@@ -188,6 +188,9 @@ class CourseDetails(object):
# TODO move to a more general util? # TODO move to a more general util?
class CourseSettingsEncoder(json.JSONEncoder): class CourseSettingsEncoder(json.JSONEncoder):
"""
Serialize CourseDetails, CourseGradingModel, datetime, and old Locations
"""
def default(self, obj): def default(self, obj):
if isinstance(obj, (CourseDetails, course_grading.CourseGradingModel)): if isinstance(obj, (CourseDetails, course_grading.CourseGradingModel)):
return obj.__dict__ return obj.__dict__
......
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
from datetime import timedelta 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): class CourseGradingModel(object):
...@@ -9,22 +10,20 @@ class CourseGradingModel(object): ...@@ -9,22 +10,20 @@ class CourseGradingModel(object):
""" """
# Within this class, allow access to protected members of client classes. # 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. # This comes up when accessing kvs data and caches during kvs saves and modulestore writes.
# pylint: disable=W0212
def __init__(self, course_descriptor): def __init__(self, course_descriptor):
self.course_location = course_descriptor.location self.graders = [
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100] 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.grade_cutoffs = course_descriptor.grade_cutoffs
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor) self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
@classmethod @classmethod
def fetch(cls, course_location): 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_old_location = loc_mapper().translate_locator_to_location(course_location)
course_location = Location(course_location) descriptor = get_modulestore(course_old_location).get_item(course_old_location)
descriptor = get_modulestore(course_location).get_item(course_location)
model = cls(descriptor) model = cls(descriptor)
return model return model
...@@ -35,12 +34,8 @@ class CourseGradingModel(object): ...@@ -35,12 +34,8 @@ class CourseGradingModel(object):
Fetch the course's nth grader Fetch the course's nth grader
Returns an empty dict if there's no such grader. Returns an empty dict if there's no such grader.
""" """
if not isinstance(course_location, Location): course_old_location = loc_mapper().translate_locator_to_location(course_location)
course_location = Location(course_location) descriptor = get_modulestore(course_old_location).get_item(course_old_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?
index = int(index) index = int(index)
if len(descriptor.raw_grader) > index: if len(descriptor.raw_grader) > index:
...@@ -57,44 +52,22 @@ class CourseGradingModel(object): ...@@ -57,44 +52,22 @@ class CourseGradingModel(object):
} }
@staticmethod @staticmethod
def fetch_cutoffs(course_location): def update_from_json(course_location, jsondict):
"""
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):
""" """
Decode the json into CourseGradingModel and save any changes. Returns the modified model. 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. Probably not the usual path for updates as it's too coarse grained.
""" """
course_location = Location(jsondict['course_location']) course_old_location = loc_mapper().translate_locator_to_location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_old_location).get_item(course_old_location)
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']] graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
descriptor.raw_grader = graders_parsed descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs'] descriptor.grade_cutoffs = jsondict['grade_cutoffs']
# Save the data that we've just changed to the underlying get_modulestore(course_old_location).update_item(
# MongoKeyValueStore before we update the mongo datastore. course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.content)
descriptor.save() )
get_modulestore(course_location).update_item(course_location, descriptor.xblock_kvs._data)
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period']) CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
...@@ -106,12 +79,8 @@ class CourseGradingModel(object): ...@@ -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 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) grader which is a full model on the client but not on the server (just a dict)
""" """
if not isinstance(course_location, Location): course_old_location = loc_mapper().translate_locator_to_location(course_location)
course_location = Location(course_location) descriptor = get_modulestore(course_old_location).get_item(course_old_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?
# parse removes the id; so, grab it before parse # parse removes the id; so, grab it before parse
index = int(grader.get('id', len(descriptor.raw_grader))) index = int(grader.get('id', len(descriptor.raw_grader)))
...@@ -122,10 +91,9 @@ class CourseGradingModel(object): ...@@ -122,10 +91,9 @@ class CourseGradingModel(object):
else: else:
descriptor.raw_grader.append(grader) descriptor.raw_grader.append(grader)
# Save the data that we've just changed to the underlying get_modulestore(course_old_location).update_item(
# MongoKeyValueStore before we update the mongo datastore. course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.content)
descriptor.save() )
get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data)
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index]) return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
...@@ -135,16 +103,13 @@ class CourseGradingModel(object): ...@@ -135,16 +103,13 @@ class CourseGradingModel(object):
Create or update the grade cutoffs for the given course. Returns sent in cutoffs (ie., no extra Create or update the grade cutoffs for the given course. Returns sent in cutoffs (ie., no extra
db fetch). db fetch).
""" """
if not isinstance(course_location, Location): course_old_location = loc_mapper().translate_locator_to_location(course_location)
course_location = Location(course_location) descriptor = get_modulestore(course_old_location).get_item(course_old_location)
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = cutoffs descriptor.grade_cutoffs = cutoffs
# Save the data that we've just changed to the underlying get_modulestore(course_old_location).update_item(
# MongoKeyValueStore before we update the mongo datastore. course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.content)
descriptor.save() )
get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data)
return cutoffs return cutoffs
...@@ -155,8 +120,8 @@ class CourseGradingModel(object): ...@@ -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 grace_period entry in an enclosing dict. It is also safe to call this method with a value of
None for graceperiodjson. None for graceperiodjson.
""" """
if not isinstance(course_location, Location): course_old_location = loc_mapper().translate_locator_to_location(course_location)
course_location = 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 # Before a graceperiod has ever been created, it will be None (once it has been
# created, it cannot be set back to None). # created, it cannot be set back to None).
...@@ -164,81 +129,67 @@ class CourseGradingModel(object): ...@@ -164,81 +129,67 @@ class CourseGradingModel(object):
if 'grace_period' in graceperiodjson: if 'grace_period' in graceperiodjson:
graceperiodjson = graceperiodjson['grace_period'] graceperiodjson = graceperiodjson['grace_period']
# lms requires these to be in a fixed order
grace_timedelta = timedelta(**graceperiodjson) grace_timedelta = timedelta(**graceperiodjson)
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.graceperiod = grace_timedelta descriptor.graceperiod = grace_timedelta
# Save the data that we've just changed to the underlying get_modulestore(course_old_location).update_metadata(
# MongoKeyValueStore before we update the mongo datastore. course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.settings)
descriptor.save() )
get_modulestore(course_location).update_metadata(course_location, descriptor._field_data._kvs._metadata)
@staticmethod @staticmethod
def delete_grader(course_location, index): def delete_grader(course_location, index):
""" """
Delete the grader of the given type from the given course. Delete the grader of the given type from the given course.
""" """
if not isinstance(course_location, Location): course_old_location = loc_mapper().translate_locator_to_location(course_location)
course_location = 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) index = int(index)
if index < len(descriptor.raw_grader): if index < len(descriptor.raw_grader):
del descriptor.raw_grader[index] del descriptor.raw_grader[index]
# force propagation to definition # force propagation to definition
descriptor.raw_grader = descriptor.raw_grader descriptor.raw_grader = descriptor.raw_grader
# Save the data that we've just changed to the underlying get_modulestore(course_old_location).update_item(
# MongoKeyValueStore before we update the mongo datastore. course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.content)
descriptor.save() )
get_modulestore(course_location).update_item(course_location, descriptor._field_data._kvs._data)
@staticmethod @staticmethod
def delete_grace_period(course_location): 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_old_location = loc_mapper().translate_locator_to_location(course_location)
course_location = 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 del descriptor.graceperiod
# Save the data that we've just changed to the underlying get_modulestore(course_old_location).update_metadata(
# MongoKeyValueStore before we update the mongo datastore. course_old_location, descriptor.get_explicitly_set_fields_by_scope(Scope.settings)
descriptor.save() )
get_modulestore(course_location).update_metadata(course_location, descriptor._field_data._kvs._metadata)
@staticmethod @staticmethod
def get_section_grader_type(location): def get_section_grader_type(location):
if not isinstance(location, Location): old_location = loc_mapper().translate_locator_to_location(location)
location = Location(location) descriptor = get_modulestore(old_location).get_item(old_location)
return {
descriptor = get_modulestore(location).get_item(location) "graderType": descriptor.format if descriptor.format is not None else 'Not Graded',
return {"graderType": descriptor.format if descriptor.format is not None else 'Not Graded', "location": unicode(location),
"location": location, }
"id": 99 # just an arbitrary value to
}
@staticmethod @staticmethod
def update_section_grader_type(location, jsondict): def update_section_grader_type(descriptor, grader_type):
if not isinstance(location, Location): if grader_type is not None and grader_type != u"Not Graded":
location = Location(location) descriptor.format = grader_type
descriptor = get_modulestore(location).get_item(location)
if 'graderType' in jsondict and jsondict['graderType'] != u"Not Graded":
descriptor.format = jsondict.get('graderType')
descriptor.graded = True descriptor.graded = True
else: else:
del descriptor.format del descriptor.format
del descriptor.graded del descriptor.graded
# Save the data that we've just changed to the underlying get_modulestore(descriptor.location).update_metadata(
# MongoKeyValueStore before we update the mongo datastore. descriptor.location, descriptor.get_explicitly_set_fields_by_scope(Scope.settings)
descriptor.save() )
get_modulestore(location).update_metadata(location, descriptor._field_data._kvs._metadata) return {'graderType': grader_type}
@staticmethod @staticmethod
def convert_set_grace_period(descriptor): def convert_set_grace_period(descriptor):
......
...@@ -36,7 +36,7 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base ...@@ -36,7 +36,7 @@ define ["js/views/overview", "js/views/feedback_notification", "sinon", "js/base
appendSetFixtures """ appendSetFixtures """
<section class="courseware-section branch" data-locator="a-location-goes-here"> <section class="courseware-section branch" data-locator="a-location-goes-here">
<li class="branch collapsed id-holder" data-id="an-id-goes-here" data-locator="an-id-goes-here"> <li class="branch collapsed id-holder" data-locator="an-id-goes-here">
<a href="#" class="delete-section-button"></a> <a href="#" class="delete-section-button"></a>
</li> </li>
</section> </section>
......
...@@ -2,10 +2,6 @@ define(["backbone", "js/models/settings/course_grader"], function(Backbone, Cour ...@@ -2,10 +2,6 @@ define(["backbone", "js/models/settings/course_grader"], function(Backbone, Cour
var CourseGraderCollection = Backbone.Collection.extend({ var CourseGraderCollection = Backbone.Collection.extend({
model : CourseGrader, 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() { sumWeights : function() {
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0); return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
} }
......
define(["backbone", "underscore", "js/models/location"], function(Backbone, _, Location) { define(["backbone", "underscore"], function(Backbone, _) {
var AssignmentGrade = Backbone.Model.extend({ var AssignmentGrade = Backbone.Model.extend({
defaults : { defaults : {
graderType : null, // the type label (string). May be "Not Graded" which implies None. I'd like to use id but that's ephemeral graderType : null, // the type label (string). May be "Not Graded" which implies None.
location : null // A location object locator : null // locator for the block
}, },
initialize : function(attrs) { idAttribute: 'locator',
if (attrs['assignmentUrl']) { urlRoot : '/xblock/',
this.set('location', new Location(attrs['assignmentUrl'], {parse: true})); 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'});
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 "";
} }
}); });
return AssignmentGrade; return AssignmentGrade;
......
define(["backbone", "underscore", "gettext", "js/models/location"], function(Backbone, _, gettext, Location) { define(["backbone", "underscore", "gettext"], function(Backbone, _, gettext) {
var CourseDetails = Backbone.Model.extend({ var CourseDetails = Backbone.Model.extend({
defaults: { defaults: {
location : null, // the course's Location model, required org : '',
course_id: '',
run: '',
start_date: null, // maps to 'start' start_date: null, // maps to 'start'
end_date: null, // maps to 'end' end_date: null, // maps to 'end'
enrollment_start: null, enrollment_start: null,
...@@ -17,9 +19,6 @@ var CourseDetails = Backbone.Model.extend({ ...@@ -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) // When init'g from html script, ensure you pass {parse: true} as an option (2nd arg to reset)
parse: function(attributes) { parse: function(attributes) {
if (attributes['course_location']) {
attributes.location = new Location(attributes.course_location, {parse:true});
}
if (attributes['start_date']) { if (attributes['start_date']) {
attributes.start_date = new Date(attributes.start_date); attributes.start_date = new Date(attributes.start_date);
} }
......
...@@ -3,15 +3,11 @@ define(["backbone", "js/models/location", "js/collections/course_grader"], ...@@ -3,15 +3,11 @@ define(["backbone", "js/models/location", "js/collections/course_grader"],
var CourseGradingPolicy = Backbone.Model.extend({ var CourseGradingPolicy = Backbone.Model.extend({
defaults : { defaults : {
course_location : null,
graders : null, // CourseGraderCollection graders : null, // CourseGraderCollection
grade_cutoffs : null, // CourseGradeCutoff model grade_cutoffs : null, // CourseGradeCutoff model
grace_period : null // either null or { hours: n, minutes: m, ...} grace_period : null // either null or { hours: n, minutes: m, ...}
}, },
parse: function(attributes) { parse: function(attributes) {
if (attributes['course_location']) {
attributes.course_location = new Location(attributes.course_location, {parse:true});
}
if (attributes['graders']) { if (attributes['graders']) {
var graderCollection; var graderCollection;
// interesting race condition: if {parse:true} when newing, then parse called before .attributes created // interesting race condition: if {parse:true} when newing, then parse called before .attributes created
...@@ -21,7 +17,6 @@ var CourseGradingPolicy = Backbone.Model.extend({ ...@@ -21,7 +17,6 @@ var CourseGradingPolicy = Backbone.Model.extend({
} }
else { else {
graderCollection = new CourseGraderCollection(attributes.graders, {parse:true}); graderCollection = new CourseGraderCollection(attributes.graders, {parse:true});
graderCollection.course_location = attributes['course_location'] || this.get('course_location');
} }
attributes.graders = graderCollection; attributes.graders = graderCollection;
} }
...@@ -35,10 +30,6 @@ var CourseGradingPolicy = Backbone.Model.extend({ ...@@ -35,10 +30,6 @@ var CourseGradingPolicy = Backbone.Model.extend({
} }
return attributes; 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() { gracePeriodToDate : function() {
var newDate = new Date(); var newDate = new Date();
if (this.has('grace_period') && this.get('grace_period')['hours']) if (this.has('grace_period') && this.get('grace_period')['hours'])
......
...@@ -21,7 +21,7 @@ define(["backbone", "underscore", "gettext", "js/models/assignment_grade", "js/v ...@@ -21,7 +21,7 @@ define(["backbone", "underscore", "gettext", "js/models/assignment_grade", "js/v
'<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' + '<li><a class="gradable-status-notgraded" href="#">Not Graded</a></li>' +
'</ul>'); '</ul>');
this.assignmentGrade = new AssignmentGrade({ 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')}); graderType : this.$el.data('initial-status')});
// TODO throw exception if graders is null // TODO throw exception if graders is null
this.graders = this.options['graders']; this.graders = this.options['graders'];
......
...@@ -21,9 +21,9 @@ var DetailsView = ValidatingView.extend({ ...@@ -21,9 +21,9 @@ var DetailsView = ValidatingView.extend({
initialize : function() { initialize : function() {
this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="icon-file"></i><%= filename %></a>'); this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="icon-file"></i><%= filename %></a>');
// fill in fields // fill in fields
this.$el.find("#course-name").val(this.model.get('location').get('name')); this.$el.find("#course-organization").val(this.model.get('org'));
this.$el.find("#course-organization").val(this.model.get('location').get('org')); this.$el.find("#course-number").val(this.model.get('course_id'));
this.$el.find("#course-number").val(this.model.get('location').get('course')); this.$el.find("#course-name").val(this.model.get('run'));
this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' }); this.$el.find('.set-date').datepicker({ 'dateFormat': 'm/d/yy' });
// Avoid showing broken image on mistyped/nonexistent image // Avoid showing broken image on mistyped/nonexistent image
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
</div> </div>
<div class="sidebar"> <div class="sidebar">
<div class="unit-settings window id-holder" data-id="${subsection.location}"> <div class="unit-settings window id-holder" data-locator="${locator}">
<h4 class="header">${_("Subsection Settings")}</h4> <h4 class="header">${_("Subsection Settings")}</h4>
<div class="window-contents"> <div class="window-contents">
<div class="scheduled-date-input row"> <div class="scheduled-date-input row">
...@@ -115,7 +115,6 @@ require(["domReady!", "jquery", "js/models/location", "js/views/overview_assignm ...@@ -115,7 +115,6 @@ require(["domReady!", "jquery", "js/models/location", "js/views/overview_assignm
// but we really should change that behavior. // but we really should change that behavior.
if (!window.graderTypes) { if (!window.graderTypes) {
window.graderTypes = new CourseGraderCollection(${course_graders|n}, {parse:true}); window.graderTypes = new CourseGraderCollection(${course_graders|n}, {parse:true});
window.graderTypes.course_location = new Location('${parent_location}');
} }
$(".gradable-status").each(function(index, ele) { $(".gradable-status").each(function(index, ele) {
......
...@@ -27,7 +27,6 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -27,7 +27,6 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
// but we really should change that behavior. // but we really should change that behavior.
if (!window.graderTypes) { if (!window.graderTypes) {
window.graderTypes = new CourseGraderCollection(${course_graders|n}, {parse:true}); window.graderTypes = new CourseGraderCollection(${course_graders|n}, {parse:true});
window.graderTypes.course_location = new Location('${parent_location}');
} }
$(".gradable-status").each(function(index, ele) { $(".gradable-status").each(function(index, ele) {
...@@ -200,7 +199,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v ...@@ -200,7 +199,7 @@ require(["domReady!", "jquery", "js/models/location", "js/models/section", "js/v
context_course.location.course_id, subsection.location, False, True context_course.location.course_id, subsection.location, False, True
) )
%> %>
<li class="courseware-subsection branch collapsed id-holder is-draggable" data-id="${subsection.location}" <li class="courseware-subsection branch collapsed id-holder is-draggable"
data-parent="${section_locator}" data-locator="${subsection_locator}"> data-parent="${section_locator}" data-locator="${subsection_locator}">
<%include file="widgets/_ui-dnd-indicator-before.html" /> <%include file="widgets/_ui-dnd-indicator-before.html" />
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! <%!
from contentstore import utils
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from xmodule.modulestore.django import loc_mapper from xmodule.modulestore.django import loc_mapper
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -69,17 +68,20 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s ...@@ -69,17 +68,20 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<ol class="list-input"> <ol class="list-input">
<li class="field text is-not-editable" id="field-course-organization"> <li class="field text is-not-editable" id="field-course-organization">
<label for="course-organization">${_("Organization")}</label> <label for="course-organization">${_("Organization")}</label>
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-organization" value="[Course Organization]" readonly /> <input title="${_('This field is disabled: this information cannot be changed.')}" type="text"
class="long" id="course-organization" readonly />
</li> </li>
<li class="field text is-not-editable" id="field-course-number"> <li class="field text is-not-editable" id="field-course-number">
<label for="course-number">${_("Course Number")}</label> <label for="course-number">${_("Course Number")}</label>
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="short" id="course-number" value="[Course No.]" readonly> <input title="${_('This field is disabled: this information cannot be changed.')}" type="text"
class="short" id="course-number" readonly>
</li> </li>
<li class="field text is-not-editable" id="field-course-name"> <li class="field text is-not-editable" id="field-course-name">
<label for="course-name">${_("Course Name")}</label> <label for="course-name">${_("Course Name")}</label>
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-name" value="[Course Name]" readonly /> <input title="${_('This field is disabled: this information cannot be changed.')}" type="text"
class="long" id="course-name" readonly />
</li> </li>
</ol> </ol>
...@@ -87,12 +89,14 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s ...@@ -87,12 +89,14 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<div class="note note-promotion note-promotion-courseURL has-actions"> <div class="note note-promotion note-promotion-courseURL has-actions">
<h3 class="title">${_("Course Summary Page")} <span class="tip">${_("(for student enrollment and access)")}</span></h3> <h3 class="title">${_("Course Summary Page")} <span class="tip">${_("(for student enrollment and access)")}</span></h3>
<div class="copy"> <div class="copy">
<p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" >https:${utils.get_lms_link_for_about_page(course_location)}</a></p> <p><a class="link-courseURL" rel="external" href="https:${lms_link_for_about_page}">https:${lms_link_for_about_page}</a></p>
</div> </div>
<ul class="list-actions"> <ul class="list-actions">
<li class="action-item"> <li class="action-item">
<a title="${_('Send a note to students via email')}" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20&quot;${context_course.display_name_with_default}&quot;,%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i>${_("Invite your students")}</a> <a title="${_('Send a note to students via email')}"
href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20&quot;${context_course.display_name_with_default}&quot;,%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${lms_link_for_about_page}%20to%20enroll." class="action action-primary">
<i class="icon-envelope-alt icon-inline"></i>${_("Invite your students")}</a>
</li> </li>
</ul> </ul>
</div> </div>
...@@ -199,7 +203,7 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s ...@@ -199,7 +203,7 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<%def name='overview_text()'><% <%def name='overview_text()'><%
a_link_start = '<a class="link-courseURL" rel="external" href="' a_link_start = '<a class="link-courseURL" rel="external" href="'
a_link_end = '">' + _("your course summary page") + '</a>' a_link_end = '">' + _("your course summary page") + '</a>'
a_link = a_link_start + utils.get_lms_link_for_about_page(course_location) + a_link_end a_link = a_link_start + lms_link_for_about_page + a_link_end
text = _("Introductions, prerequisites, FAQs that are used on %s (formatted in HTML)") % a_link text = _("Introductions, prerequisites, FAQs that are used on %s (formatted in HTML)") % a_link
%>${text}</%def> %>${text}</%def>
<span class="tip tip-stacked">${overview_text()}</span> <span class="tip tip-stacked">${overview_text()}</span>
...@@ -211,15 +215,16 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s ...@@ -211,15 +215,16 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<div class="current current-course-image"> <div class="current current-course-image">
% if context_course.course_image: % if context_course.course_image:
<span class="wrapper-course-image"> <span class="wrapper-course-image">
<img class="course-image" id="course-image" src="${utils.course_image_url(context_course)}" alt="${_('Course Image')}"/> <img class="course-image" id="course-image" src="${course_image_url}" alt="${_('Course Image')}"/>
</span> </span>
<% ctx_loc = context_course.location %> <span class="msg msg-help">
<span class="msg msg-help">${_("You can manage this image along with all of your other")} <a href='${upload_asset_url}'>${_("files &amp; uploads")}</a></span> ${_("You can manage this image along with all of your other <a href='{}'>files &amp; uploads</a>").format(upload_asset_url)}
</span>
% else: % else:
<span class="wrapper-course-image"> <span class="wrapper-course-image">
<img class="course-image placeholder" id="course-image" src="${utils.course_image_url(context_course)}" alt="${_('Course Image')}"/> <img class="course-image placeholder" id="course-image" src="${course_image_url}" alt="${_('Course Image')}"/>
</span> </span>
<span class="msg msg-empty">${_("Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall)")}</span> <span class="msg msg-empty">${_("Your course currently does not have an image. Please upload one (JPEG or PNG format, and minimum suggested dimensions are 375px wide by 200px tall)")}</span>
% endif % endif
...@@ -286,14 +291,14 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s ...@@ -286,14 +291,14 @@ require(["domReady!", "jquery", "js/models/settings/course_details", "js/views/s
<div class="bit"> <div class="bit">
% if context_course: % if context_course:
<% <%
course_team_url = course_locator.url_reverse('course_team/', '')
grading_config_url = course_locator.url_reverse('settings/grading/')
ctx_loc = context_course.location ctx_loc = context_course.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
course_team_url = location.url_reverse('course_team/', '')
%> %>
<h3 class="title-3">${_("Other Course Settings")}</h3> <h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related"> <nav class="nav-related">
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li> <li class="nav-item"><a href="${grading_config_url}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${course_team_url}">${_("Course Team")}</a></li> <li class="nav-item"><a href="${course_team_url}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li> <li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
</ul> </ul>
......
...@@ -96,8 +96,8 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting ...@@ -96,8 +96,8 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting
<h3 class="title-3">${_("Other Course Settings")}</h3> <h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related"> <nav class="nav-related">
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li> <li class="nav-item"><a href="${course_locator.url_reverse('settings/details/')}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li> <li class="nav-item"><a href="${course_locator.url_reverse('settings/grading/')}">${_("Grading")}</a></li>
<li class="nav-item"><a href="${course_team_url}">${_("Course Team")}</a></li> <li class="nav-item"><a href="${course_team_url}">${_("Course Team")}</a></li>
</ul> </ul>
</nav> </nav>
......
...@@ -28,9 +28,11 @@ require(["domReady!", "jquery", "js/views/settings/grading", "js/models/settings ...@@ -28,9 +28,11 @@ require(["domReady!", "jquery", "js/views/settings/grading", "js/models/settings
$("label").removeClass("is-focused"); $("label").removeClass("is-focused");
}); });
var model = new CourseGradingPolicyModel(${course_details|n},{parse:true});
model.urlRoot = '${grading_url}';
var editor = new GradingView({ var editor = new GradingView({
el: $('.settings-grading'), el: $('.settings-grading'),
model : new CourseGradingPolicyModel(${course_details|n},{parse:true}) model : model
}); });
editor.render(); editor.render();
...@@ -138,13 +140,12 @@ require(["domReady!", "jquery", "js/views/settings/grading", "js/models/settings ...@@ -138,13 +140,12 @@ require(["domReady!", "jquery", "js/views/settings/grading", "js/models/settings
% if context_course: % if context_course:
<% <%
ctx_loc = context_course.location ctx_loc = context_course.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True) course_team_url = course_locator.url_reverse('course_team/')
course_team_url = location.url_reverse('course_team/', '')
%> %>
<h3 class="title-3">${_("Other Course Settings")}</h3> <h3 class="title-3">${_("Other Course Settings")}</h3>
<nav class="nav-related"> <nav class="nav-related">
<ul> <ul>
<li class="nav-item"><a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Details &amp; Schedule")}</a></li> <li class="nav-item"><a href="${course_locator.url_reverse('settings/details/')}">${_("Details &amp; Schedule")}</a></li>
<li class="nav-item"><a href="${course_team_url}">${_("Course Team")}</a></li> <li class="nav-item"><a href="${course_team_url}">${_("Course Team")}</a></li>
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li> <li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
</ul> </ul>
......
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
import_url = location.url_reverse('import') import_url = location.url_reverse('import')
course_info_url = location.url_reverse('course_info') course_info_url = location.url_reverse('course_info')
export_url = location.url_reverse('export') export_url = location.url_reverse('export')
settings_url = location.url_reverse('settings/details/')
grading_url = location.url_reverse('settings/grading/')
tabs_url = location.url_reverse('tabs') tabs_url = location.url_reverse('tabs')
%> %>
<h2 class="info-course"> <h2 class="info-course">
...@@ -69,10 +71,10 @@ ...@@ -69,10 +71,10 @@
<div class="nav-sub"> <div class="nav-sub">
<ul> <ul>
<li class="nav-item nav-course-settings-schedule"> <li class="nav-item nav-course-settings-schedule">
<a href="${reverse('contentstore.views.get_course_settings', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Schedule &amp; Details")}</a> <a href="${settings_url}">${_("Schedule &amp; Details")}</a>
</li> </li>
<li class="nav-item nav-course-settings-grading"> <li class="nav-item nav-course-settings-grading">
<a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a> <a href="${grading_url}">${_("Grading")}</a>
</li> </li>
<li class="nav-item nav-course-settings-team"> <li class="nav-item nav-course-settings-team">
<a href="${course_team_url}">${_("Course Team")}</a> <a href="${course_team_url}">${_("Course Team")}</a>
......
...@@ -29,14 +29,6 @@ urlpatterns = patterns('', # nopep8 ...@@ -29,14 +29,6 @@ urlpatterns = patterns('', # nopep8
url(r'^preview/xblock/(?P<usage_id>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>[^/]*))?$', url(r'^preview/xblock/(?P<usage_id>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>[^/]*))?$',
'contentstore.views.preview_handler', name='preview_handler'), 'contentstore.views.preview_handler', name='preview_handler'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)$',
'contentstore.views.get_course_settings', name='settings_details'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)$',
'contentstore.views.course_config_graders_page', name='settings_grading'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$',
'contentstore.views.course_settings_updates', name='course_settings'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)/(?P<grader_index>.*)$',
'contentstore.views.course_grader_updates', name='course_settings'),
# This is the URL to initially render the course advanced settings. # This is the URL to initially render the course advanced settings.
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-advanced/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-advanced/(?P<name>[^/]+)$',
'contentstore.views.course_config_advanced_page', name='course_advanced_settings'), 'contentstore.views.course_config_advanced_page', name='course_advanced_settings'),
...@@ -44,9 +36,6 @@ urlpatterns = patterns('', # nopep8 ...@@ -44,9 +36,6 @@ urlpatterns = patterns('', # nopep8
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-advanced/(?P<name>[^/]+)/update.*$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-advanced/(?P<name>[^/]+)/update.*$',
'contentstore.views.course_advanced_updates', name='course_advanced_settings_updates'), 'contentstore.views.course_advanced_updates', name='course_advanced_settings_updates'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/(?P<category>[^/]+)/(?P<name>[^/]+)/gradeas.*$',
'contentstore.views.assignment_type_update', name='assignment_type_update'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$',
'contentstore.views.textbook_index', name='textbook_index'), 'contentstore.views.textbook_index', name='textbook_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/new$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/new$',
...@@ -108,6 +97,8 @@ urlpatterns += patterns( ...@@ -108,6 +97,8 @@ urlpatterns += patterns(
url(r'(?ix)^export/{}$'.format(parsers.URL_RE_SOURCE), 'export_handler'), url(r'(?ix)^export/{}$'.format(parsers.URL_RE_SOURCE), 'export_handler'),
url(r'(?ix)^xblock($|/){}$'.format(parsers.URL_RE_SOURCE), 'xblock_handler'), url(r'(?ix)^xblock($|/){}$'.format(parsers.URL_RE_SOURCE), 'xblock_handler'),
url(r'(?ix)^tabs/{}$'.format(parsers.URL_RE_SOURCE), 'tabs_handler'), url(r'(?ix)^tabs/{}$'.format(parsers.URL_RE_SOURCE), 'tabs_handler'),
url(r'(?ix)^settings/details/{}$'.format(parsers.URL_RE_SOURCE), 'settings_handler'),
url(r'(?ix)^settings/grading/{}(/)?(?P<grader_index>\d+)?$'.format(parsers.URL_RE_SOURCE), 'grading_handler'),
) )
js_info_dict = { js_info_dict = {
......
...@@ -597,6 +597,9 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -597,6 +597,9 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
@property @property
def raw_grader(self): def raw_grader(self):
# force the caching of the xblock value so that it can detect the change
# pylint: disable=pointless-statement
self.grading_policy['GRADER']
return self._grading_policy['RAW_GRADER'] return self._grading_policy['RAW_GRADER']
@raw_grader.setter @raw_grader.setter
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment