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