Commit 0d349037 by David Baumgold

Refactor textbooks to use locator URLs

STUD-945
parent f7d86bff
...@@ -1659,14 +1659,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1659,14 +1659,7 @@ class ContentStoreTest(ModuleStoreTestCase):
test_get_html('settings/details') test_get_html('settings/details')
test_get_html('settings/grading') test_get_html('settings/grading')
test_get_html('settings/advanced') test_get_html('settings/advanced')
test_get_html('textbooks')
# textbook index
resp = self.client.get_html(reverse('textbook_index',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assertEqual(resp.status_code, 200)
_test_no_locations(self, resp)
# go look at a subsection page # go look at a subsection page
subsection_location = loc.replace(category='sequential', name='test_sequence') subsection_location = loc.replace(category='sequential', name='test_sequence')
......
...@@ -14,11 +14,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -14,11 +14,7 @@ class TextbookIndexTestCase(CourseTestCase):
def setUp(self): def setUp(self):
"Set the URL for tests" "Set the URL for tests"
super(TextbookIndexTestCase, self).setUp() super(TextbookIndexTestCase, self).setUp()
self.url = reverse('textbook_index', kwargs={ self.url = self.course_locator.url_reverse('textbooks')
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
})
def test_view_index(self): def test_view_index(self):
"Basic check that the textbook index page responds correctly" "Basic check that the textbook index page responds correctly"
...@@ -77,13 +73,13 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -77,13 +73,13 @@ class TextbookIndexTestCase(CourseTestCase):
obj = json.loads(resp.content) obj = json.loads(resp.content)
self.assertEqual(content, obj) self.assertEqual(content, obj)
def test_view_index_xhr_post(self): def test_view_index_xhr_put(self):
"Check that you can save information to the server" "Check that you can save information to the server"
textbooks = [ textbooks = [
{"tab_title": "Hi, mom!"}, {"tab_title": "Hi, mom!"},
{"tab_title": "Textbook 2"}, {"tab_title": "Textbook 2"},
] ]
resp = self.client.post( resp = self.client.put(
self.url, self.url,
data=json.dumps(textbooks), data=json.dumps(textbooks),
content_type="application/json", content_type="application/json",
...@@ -102,9 +98,9 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -102,9 +98,9 @@ class TextbookIndexTestCase(CourseTestCase):
no_ids.append(textbook) no_ids.append(textbook)
self.assertEqual(no_ids, textbooks) self.assertEqual(no_ids, textbooks)
def test_view_index_xhr_post_invalid(self): def test_view_index_xhr_put_invalid(self):
"Check that you can't save invalid JSON" "Check that you can't save invalid JSON"
resp = self.client.post( resp = self.client.put(
self.url, self.url,
data="invalid", data="invalid",
content_type="application/json", content_type="application/json",
...@@ -122,11 +118,7 @@ class TextbookCreateTestCase(CourseTestCase): ...@@ -122,11 +118,7 @@ class TextbookCreateTestCase(CourseTestCase):
def setUp(self): def setUp(self):
"Set up a url and some textbook content for tests" "Set up a url and some textbook content for tests"
super(TextbookCreateTestCase, self).setUp() super(TextbookCreateTestCase, self).setUp()
self.url = reverse('create_textbook', kwargs={ self.url = self.course_locator.url_reverse('textbooks')
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
})
self.textbook = { self.textbook = {
"tab_title": "Economics", "tab_title": "Economics",
"chapters": { "chapters": {
...@@ -151,15 +143,6 @@ class TextbookCreateTestCase(CourseTestCase): ...@@ -151,15 +143,6 @@ class TextbookCreateTestCase(CourseTestCase):
del textbook["id"] del textbook["id"]
self.assertEqual(self.textbook, textbook) self.assertEqual(self.textbook, textbook)
def test_get(self):
"Test that GET is not allowed"
resp = self.client.get(
self.url,
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(resp.status_code, 405)
def test_valid_id(self): def test_valid_id(self):
"Textbook IDs must begin with a number; try a valid one" "Textbook IDs must begin with a number; try a valid one"
self.textbook["id"] = "7x5" self.textbook["id"] = "7x5"
...@@ -188,12 +171,12 @@ class TextbookCreateTestCase(CourseTestCase): ...@@ -188,12 +171,12 @@ class TextbookCreateTestCase(CourseTestCase):
self.assertNotIn("Location", resp) self.assertNotIn("Location", resp)
class TextbookByIdTestCase(CourseTestCase): class TextbookDetailTestCase(CourseTestCase):
"Test cases for the `textbook_by_id` view" "Test cases for the `textbook_detail_handler` view"
def setUp(self): def setUp(self):
"Set some useful content and URLs for tests" "Set some useful content and URLs for tests"
super(TextbookByIdTestCase, self).setUp() super(TextbookDetailTestCase, self).setUp()
self.textbook1 = { self.textbook1 = {
"tab_title": "Economics", "tab_title": "Economics",
"id": 1, "id": 1,
...@@ -202,12 +185,7 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -202,12 +185,7 @@ class TextbookByIdTestCase(CourseTestCase):
"url": "/a/b/c/ch1.pdf", "url": "/a/b/c/ch1.pdf",
} }
} }
self.url1 = reverse('textbook_by_id', kwargs={ self.url1 = self.course_locator.url_reverse("textbooks", "1")
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'tid': 1,
})
self.textbook2 = { self.textbook2 = {
"tab_title": "Algebra", "tab_title": "Algebra",
"id": 2, "id": 2,
...@@ -216,24 +194,14 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -216,24 +194,14 @@ class TextbookByIdTestCase(CourseTestCase):
"url": "/a/b/ch11.pdf", "url": "/a/b/ch11.pdf",
} }
} }
self.url2 = reverse('textbook_by_id', kwargs={ self.url2 = self.course_locator.url_reverse("textbooks", "2")
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'tid': 2,
})
self.course.pdf_textbooks = [self.textbook1, self.textbook2] self.course.pdf_textbooks = [self.textbook1, self.textbook2]
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
self.course.save() self.course.save()
self.store = get_modulestore(self.course.location) self.store = get_modulestore(self.course.location)
self.store.update_metadata(self.course.location, own_metadata(self.course)) self.store.update_metadata(self.course.location, own_metadata(self.course))
self.url_nonexist = reverse('textbook_by_id', kwargs={ self.url_nonexist = self.course_locator.url_reverse("textbooks", "20")
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'tid': 20,
})
def test_get_1(self): def test_get_1(self):
"Get the first textbook" "Get the first textbook"
...@@ -275,12 +243,7 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -275,12 +243,7 @@ class TextbookByIdTestCase(CourseTestCase):
"url": "supercool.pdf", "url": "supercool.pdf",
"id": "1supercool", "id": "1supercool",
} }
url = reverse("textbook_by_id", kwargs={ url = self.course_locator.url_reverse("textbooks", "1supercool")
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
'tid': "1supercool",
})
resp = self.client.post( resp = self.client.post(
url, url,
data=json.dumps(textbook), data=json.dumps(textbook),
......
...@@ -57,6 +57,7 @@ class AjaxEnabledTestClient(Client): ...@@ -57,6 +57,7 @@ class AjaxEnabledTestClient(Client):
""" """
return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra) return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra)
@override_settings(MODULESTORE=TEST_MODULESTORE) @override_settings(MODULESTORE=TEST_MODULESTORE)
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
def setUp(self): def setUp(self):
...@@ -111,7 +112,7 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -111,7 +112,7 @@ class CourseTestCase(ModuleStoreTestCase):
client = Client() client = Client()
client.login(username=uname, password=password) client.login(username=uname, password=password)
return client, nonstaff return client, nonstaff
def populateCourse(self): def populateCourse(self):
""" """
Add 2 chapters, 4 sections, 8 verticals, 16 problems to self.course (branching 2) Add 2 chapters, 4 sections, 8 verticals, 16 problems to self.course (branching 2)
......
...@@ -38,7 +38,7 @@ from models.settings.course_metadata import CourseMetadata ...@@ -38,7 +38,7 @@ from models.settings.course_metadata import CourseMetadata
from auth.authz import create_all_course_groups, is_user_in_creator_group from auth.authz import create_all_course_groups, is_user_in_creator_group
from util.json_request import expect_json from util.json_request import expect_json
from .access import has_access, get_location_and_verify_access from .access import has_access
from .tabs import initialize_course_tabs from .tabs import initialize_course_tabs
from .component import ( from .component import (
OPEN_ENDED_COMPONENT_TYPES, NOTE_COMPONENT_TYPES, OPEN_ENDED_COMPONENT_TYPES, NOTE_COMPONENT_TYPES,
...@@ -57,8 +57,20 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler' ...@@ -57,8 +57,20 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler'
'settings_handler', 'settings_handler',
'grading_handler', 'grading_handler',
'advanced_settings_handler', 'advanced_settings_handler',
'textbook_index', 'textbook_by_id', 'textbooks_list_handler', 'textbooks_detail_handler']
'create_textbook']
def _get_locator_and_course(course_id, branch, version_guid, usage_id, user, depth=0):
"""
Internal method used to calculate and return the locator and course module
for the view functions in this file.
"""
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=usage_id)
if not has_access(user, locator):
raise PermissionDenied()
course_location = loc_mapper().translate_locator_to_location(locator)
course_module = modulestore().get_item(course_location, depth=depth)
return locator, course_module
# pylint: disable=unused-argument # pylint: disable=unused-argument
...@@ -168,17 +180,10 @@ def course_index(request, course_id, branch, version_guid, block): ...@@ -168,17 +180,10 @@ def course_index(request, course_id, branch, version_guid, block):
org, course, name: Attributes of the Location for the item to edit org, course, name: Attributes of the Location for the item to edit
""" """
location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) locator, course = _get_locator_and_course(
# TODO: when converting to split backend, if location does not have a usage_id, course_id, branch, version_guid, block, request.user, depth=3
# we'll need to get the course's root block_id )
if not has_access(request.user, location): lms_link = get_lms_link_for_item(course.location)
raise PermissionDenied()
old_location = loc_mapper().translate_locator_to_location(location)
lms_link = get_lms_link_for_item(old_location)
course = modulestore().get_item(old_location, depth=3)
sections = course.get_children() sections = course.get_children()
return render_to_response('overview.html', { return render_to_response('overview.html', {
...@@ -186,9 +191,9 @@ def course_index(request, course_id, branch, version_guid, block): ...@@ -186,9 +191,9 @@ def course_index(request, course_id, branch, version_guid, block):
'lms_link': lms_link, 'lms_link': lms_link,
'sections': sections, 'sections': sections,
'course_graders': json.dumps( 'course_graders': json.dumps(
CourseGradingModel.fetch(location).graders CourseGradingModel.fetch(locator).graders
), ),
'parent_locator': location, 'parent_locator': locator,
'new_section_category': 'chapter', 'new_section_category': 'chapter',
'new_subsection_category': 'sequential', 'new_subsection_category': 'sequential',
'new_unit_category': 'vertical', 'new_unit_category': 'vertical',
...@@ -314,22 +319,18 @@ def course_info_handler(request, tag=None, course_id=None, branch=None, version_ ...@@ -314,22 +319,18 @@ def course_info_handler(request, tag=None, course_id=None, branch=None, version_
GET GET
html: return html for editing the course info handouts and updates. html: return html for editing the course info handouts and updates.
""" """
course_location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) __, course_module = _get_locator_and_course(
course_old_location = loc_mapper().translate_locator_to_location(course_location) course_id, branch, version_guid, block, request.user
)
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
if not has_access(request.user, course_location): handouts_old_location = course_module.location.replace(category='course_info', name='handouts')
raise PermissionDenied()
course_module = modulestore().get_item(course_old_location)
handouts_old_location = course_old_location.replace(category='course_info', name='handouts')
handouts_locator = loc_mapper().translate_location( handouts_locator = loc_mapper().translate_location(
course_old_location.course_id, handouts_old_location, False, True course_module.location.course_id, handouts_old_location, False, True
) )
update_location = course_old_location.replace(category='course_info', name='updates') update_location = course_module.location.replace(category='course_info', name='updates')
update_locator = loc_mapper().translate_location( update_locator = loc_mapper().translate_location(
course_old_location.course_id, update_location, False, True course_module.location.course_id, update_location, False, True
) )
return render_to_response( return render_to_response(
...@@ -338,7 +339,7 @@ def course_info_handler(request, tag=None, course_id=None, branch=None, version_ ...@@ -338,7 +339,7 @@ def course_info_handler(request, tag=None, course_id=None, branch=None, version_
'context_course': course_module, 'context_course': course_module,
'updates_url': update_locator.url_reverse('course_info_update/'), 'updates_url': update_locator.url_reverse('course_info_update/'),
'handouts_locator': handouts_locator, 'handouts_locator': handouts_locator,
'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_old_location) + '/' 'base_asset_url': StaticContent.get_base_url_path_for_course_assets(course_module.location) + '/'
} }
) )
else: else:
...@@ -407,20 +408,16 @@ def settings_handler(request, tag=None, course_id=None, branch=None, version_gui ...@@ -407,20 +408,16 @@ def settings_handler(request, tag=None, course_id=None, branch=None, version_gui
PUT PUT
json: update the Course and About xblocks through the CourseDetails model json: update the Course and About xblocks through the CourseDetails model
""" """
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) locator, course_module = _get_locator_and_course(
if not has_access(request.user, locator): course_id, branch, version_guid, block, request.user
raise PermissionDenied() )
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': 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)
upload_asset_url = locator.url_reverse('assets/') upload_asset_url = locator.url_reverse('assets/')
return render_to_response('settings.html', { return render_to_response('settings.html', {
'context_course': course_module, 'context_course': course_module,
'course_locator': locator, 'course_locator': locator,
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_old_location), 'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_module.location),
'course_image_url': utils.course_image_url(course_module), 'course_image_url': utils.course_image_url(course_module),
'details_url': locator.url_reverse('/settings/details/'), 'details_url': locator.url_reverse('/settings/details/'),
'about_page_editable': not settings.FEATURES.get( 'about_page_editable': not settings.FEATURES.get(
...@@ -457,13 +454,11 @@ def grading_handler(request, tag=None, course_id=None, branch=None, version_guid ...@@ -457,13 +454,11 @@ def grading_handler(request, tag=None, course_id=None, branch=None, version_guid
json no grader_index: update the Course through the CourseGrading model 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) json w/ grader_index: create or update the specific grader (create if index out of range)
""" """
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) locator, course_module = _get_locator_and_course(
if not has_access(request.user, locator): course_id, branch, version_guid, block, request.user
raise PermissionDenied() )
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': 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) course_details = CourseGradingModel.fetch(locator)
return render_to_response('settings_graders.html', { return render_to_response('settings_graders.html', {
...@@ -514,8 +509,8 @@ def _config_course_advanced_components(request, course_module): ...@@ -514,8 +509,8 @@ def _config_course_advanced_components(request, course_module):
filter_tabs = True # Exceptional conditions will pull this to False filter_tabs = True # Exceptional conditions will pull this to False
if ADVANCED_COMPONENT_POLICY_KEY in request.json: # Maps tab types to components if ADVANCED_COMPONENT_POLICY_KEY in request.json: # Maps tab types to components
tab_component_map = { tab_component_map = {
'open_ended':OPEN_ENDED_COMPONENT_TYPES, 'open_ended': OPEN_ENDED_COMPONENT_TYPES,
'notes':NOTE_COMPONENT_TYPES, 'notes': NOTE_COMPONENT_TYPES,
} }
# Check to see if the user instantiated any notes or open ended # Check to see if the user instantiated any notes or open ended
# components # components
...@@ -565,13 +560,9 @@ def advanced_settings_handler(request, course_id=None, branch=None, version_guid ...@@ -565,13 +560,9 @@ def advanced_settings_handler(request, course_id=None, branch=None, version_guid
metadata dicts. The dict can include a "unsetKeys" entry which is a list metadata dicts. The dict can include a "unsetKeys" entry which is a list
of keys whose values to unset: i.e., revert to default of keys whose values to unset: i.e., revert to default
""" """
locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block) locator, course_module = _get_locator_and_course(
if not has_access(request.user, locator): course_id, branch, version_guid, block, request.user
raise PermissionDenied() )
course_old_location = loc_mapper().translate_locator_to_location(locator)
course_module = modulestore().get_item(course_old_location)
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
return render_to_response('settings_advanced.html', { return render_to_response('settings_advanced.html', {
...@@ -657,113 +648,109 @@ def assign_textbook_id(textbook, used_ids=()): ...@@ -657,113 +648,109 @@ def assign_textbook_id(textbook, used_ids=()):
return tid return tid
@require_http_methods(("GET", "POST", "PUT"))
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def textbook_index(request, org, course, name): def textbooks_list_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None):
""" """
Display an editable textbook overview. A RESTful handler for textbook collections.
org, course, name: Attributes of the Location for the item to edit GET
html: return textbook list page (Backbone application)
json: return JSON representation of all textbooks in this course
POST
json: create a new textbook for this course
PUT
json: overwrite all textbooks in the course with the given list
""" """
location = get_location_and_verify_access(request, org, course, name) locator, course = _get_locator_and_course(
store = get_modulestore(location) course_id, branch, version_guid, block, request.user
course_module = store.get_item(location, depth=3) )
store = get_modulestore(course.location)
if request.is_ajax(): if not "application/json" in request.META.get('HTTP_ACCEPT', 'text/html'):
if request.method == 'GET': # return HTML page
return JsonResponse(course_module.pdf_textbooks) upload_asset_url = locator.url_reverse('assets/', '')
# can be either and sometimes django is rewriting one to the other: textbook_url = locator.url_reverse('/textbooks')
elif request.method in ('POST', 'PUT'):
try:
textbooks = validate_textbooks_json(request.body)
except TextbookValidationError as err:
return JsonResponse({"error": err.message}, status=400)
tids = set(t["id"] for t in textbooks if "id" in t)
for textbook in textbooks:
if not "id" in textbook:
tid = assign_textbook_id(textbook, tids)
textbook["id"] = tid
tids.add(tid)
if not any(tab['type'] == 'pdf_textbooks' for tab in course_module.tabs):
course_module.tabs.append({"type": "pdf_textbooks"})
course_module.pdf_textbooks = textbooks
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
course_module.save()
store.update_metadata(
course_module.location,
own_metadata(course_module)
)
return JsonResponse(course_module.pdf_textbooks)
else:
new_loc = loc_mapper().translate_location(location.course_id, location, False, True)
upload_asset_url = new_loc.url_reverse('assets/', '')
textbook_url = reverse('textbook_index', kwargs={
'org': org,
'course': course,
'name': name,
})
return render_to_response('textbooks.html', { return render_to_response('textbooks.html', {
'context_course': course_module, 'context_course': course,
'course': course_module, 'course': course,
'upload_asset_url': upload_asset_url, 'upload_asset_url': upload_asset_url,
'textbook_url': textbook_url, 'textbook_url': textbook_url,
}) })
# from here on down, we know the client has requested JSON
if request.method == 'GET':
return JsonResponse(course.pdf_textbooks)
elif request.method == 'PUT':
try:
textbooks = validate_textbooks_json(request.body)
except TextbookValidationError as err:
return JsonResponse({"error": err.message}, status=400)
@require_POST tids = set(t["id"] for t in textbooks if "id" in t)
@login_required for textbook in textbooks:
@ensure_csrf_cookie if not "id" in textbook:
def create_textbook(request, org, course, name): tid = assign_textbook_id(textbook, tids)
""" textbook["id"] = tid
JSON API endpoint for creating a textbook. Used by the Backbone application. tids.add(tid)
"""
location = get_location_and_verify_access(request, org, course, name)
store = get_modulestore(location)
course_module = store.get_item(location, depth=0)
try: if not any(tab['type'] == 'pdf_textbooks' for tab in course.tabs):
textbook = validate_textbook_json(request.body) course.tabs.append({"type": "pdf_textbooks"})
except TextbookValidationError as err: course.pdf_textbooks = textbooks
return JsonResponse({"error": err.message}, status=400) # Save the data that we've just changed to the underlying
if not textbook.get("id"): # MongoKeyValueStore before we update the mongo datastore.
tids = set(t["id"] for t in course_module.pdf_textbooks if "id" in t) course.save()
textbook["id"] = assign_textbook_id(textbook, tids) store.update_metadata(
existing = course_module.pdf_textbooks course.location,
existing.append(textbook) own_metadata(course)
course_module.pdf_textbooks = existing )
if not any(tab['type'] == 'pdf_textbooks' for tab in course_module.tabs): return JsonResponse(course.pdf_textbooks)
tabs = course_module.tabs elif request.method == 'POST':
tabs.append({"type": "pdf_textbooks"}) # create a new textbook for the course
course_module.tabs = tabs try:
# Save the data that we've just changed to the underlying textbook = validate_textbook_json(request.body)
# MongoKeyValueStore before we update the mongo datastore. except TextbookValidationError as err:
course_module.save() return JsonResponse({"error": err.message}, status=400)
store.update_metadata(course_module.location, own_metadata(course_module)) if not textbook.get("id"):
resp = JsonResponse(textbook, status=201) tids = set(t["id"] for t in course.pdf_textbooks if "id" in t)
resp["Location"] = reverse("textbook_by_id", kwargs={ textbook["id"] = assign_textbook_id(textbook, tids)
'org': org, existing = course.pdf_textbooks
'course': course, existing.append(textbook)
'name': name, course.pdf_textbooks = existing
'tid': textbook["id"], if not any(tab['type'] == 'pdf_textbooks' for tab in course.tabs):
}) tabs = course.tabs
return resp tabs.append({"type": "pdf_textbooks"})
course.tabs = tabs
# Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore.
course.save()
store.update_metadata(course.location, own_metadata(course))
resp = JsonResponse(textbook, status=201)
resp["Location"] = locator.url_reverse('textbooks', textbook["id"])
return resp
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT", "DELETE")) @require_http_methods(("GET", "POST", "PUT", "DELETE"))
def textbook_by_id(request, org, course, name, tid): def textbooks_detail_handler(request, tid, tag=None, course_id=None, branch=None, version_guid=None, block=None):
""" """
JSON API endpoint for manipulating a textbook via its internal ID. JSON API endpoint for manipulating a textbook via its internal ID.
Used by the Backbone application. Used by the Backbone application.
GET
json: return JSON representation of textbook
POST or PUT
json: update textbook based on provided information
DELETE
json: remove textbook
""" """
location = get_location_and_verify_access(request, org, course, name) __, course = _get_locator_and_course(
store = get_modulestore(location) course_id, branch, version_guid, block, request.user
course_module = store.get_item(location, depth=3) )
matching_id = [tb for tb in course_module.pdf_textbooks store = get_modulestore(course.location)
matching_id = [tb for tb in course.pdf_textbooks
if str(tb.get("id")) == str(tid)] if str(tb.get("id")) == str(tid)]
if matching_id: if matching_id:
textbook = matching_id[0] textbook = matching_id[0]
...@@ -782,32 +769,32 @@ def textbook_by_id(request, org, course, name, tid): ...@@ -782,32 +769,32 @@ def textbook_by_id(request, org, course, name, tid):
return JsonResponse({"error": err.message}, status=400) return JsonResponse({"error": err.message}, status=400)
new_textbook["id"] = tid new_textbook["id"] = tid
if textbook: if textbook:
i = course_module.pdf_textbooks.index(textbook) i = course.pdf_textbooks.index(textbook)
new_textbooks = course_module.pdf_textbooks[0:i] new_textbooks = course.pdf_textbooks[0:i]
new_textbooks.append(new_textbook) new_textbooks.append(new_textbook)
new_textbooks.extend(course_module.pdf_textbooks[i + 1:]) new_textbooks.extend(course.pdf_textbooks[i + 1:])
course_module.pdf_textbooks = new_textbooks course.pdf_textbooks = new_textbooks
else: else:
course_module.pdf_textbooks.append(new_textbook) course.pdf_textbooks.append(new_textbook)
# Save the data that we've just changed to the underlying # Save the data that we've just changed to the underlying
# MongoKeyValueStore before we update the mongo datastore. # MongoKeyValueStore before we update the mongo datastore.
course_module.save() course.save()
store.update_metadata( store.update_metadata(
course_module.location, course.location,
own_metadata(course_module) own_metadata(course)
) )
return JsonResponse(new_textbook, status=201) return JsonResponse(new_textbook, status=201)
elif request.method == 'DELETE': elif request.method == 'DELETE':
if not textbook: if not textbook:
return JsonResponse(status=404) return JsonResponse(status=404)
i = course_module.pdf_textbooks.index(textbook) i = course.pdf_textbooks.index(textbook)
new_textbooks = course_module.pdf_textbooks[0:i] new_textbooks = course.pdf_textbooks[0:i]
new_textbooks.extend(course_module.pdf_textbooks[i + 1:]) new_textbooks.extend(course.pdf_textbooks[i + 1:])
course_module.pdf_textbooks = new_textbooks course.pdf_textbooks = new_textbooks
course_module.save() course.save()
store.update_metadata( store.update_metadata(
course_module.location, course.location,
own_metadata(course_module) own_metadata(course)
) )
return JsonResponse() return JsonResponse()
......
require ["jquery", "backbone", "coffee/src/main", "sinon", "jasmine-stealth"], require ["jquery", "backbone", "coffee/src/main", "sinon", "jasmine-stealth", "jquery.cookie"],
($, Backbone, main, sinon) -> ($, Backbone, main, sinon) ->
describe "CMS", -> describe "CMS", ->
it "should initialize URL", -> it "should initialize URL", ->
......
...@@ -11,6 +11,10 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/ ...@@ -11,6 +11,10 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/
beforeEach -> beforeEach ->
main() main()
@model = new Textbook() @model = new Textbook()
CMS.URL.TEXTBOOKS = "/textbooks"
afterEach ->
delete CMS.URL.TEXTBOOKS
describe "Basic", -> describe "Basic", ->
it "should have an empty name by default", -> it "should have an empty name by default", ->
...@@ -28,8 +32,9 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/ ...@@ -28,8 +32,9 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/
it "should be empty by default", -> it "should be empty by default", ->
expect(@model.isEmpty()).toBeTruthy() expect(@model.isEmpty()).toBeTruthy()
it "should have a URL set", -> it "should have a URL root", ->
expect(@model.url()).toBeTruthy() urlRoot = _.result(@model, 'urlRoot')
expect(urlRoot).toBeTruthy()
it "should be able to reset itself", -> it "should be able to reset itself", ->
@model.set("name", "foobar") @model.set("name", "foobar")
...@@ -135,12 +140,8 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/ ...@@ -135,12 +140,8 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/
delete CMS.URL.TEXTBOOKS delete CMS.URL.TEXTBOOKS
it "should have a url set", -> it "should have a url set", ->
expect(@collection.url()).toEqual("/textbooks") url = _.result(@collection, 'url')
expect(url).toEqual("/textbooks")
it "can call save", ->
spyOn(@collection, "sync")
@collection.save()
expect(@collection.sync).toHaveBeenCalledWith("update", @collection, undefined)
describe "Chapter model", -> describe "Chapter model", ->
......
...@@ -81,9 +81,11 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js ...@@ -81,9 +81,11 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
@savingSpies = spyOnConstructor(Notification, "Mini", @savingSpies = spyOnConstructor(Notification, "Mini",
["show", "hide"]) ["show", "hide"])
@savingSpies.show.andReturn(@savingSpies) @savingSpies.show.andReturn(@savingSpies)
CMS.URL.TEXTBOOKS = "/textbooks"
afterEach -> afterEach ->
@xhr.restore() @xhr.restore()
delete CMS.URL.TEXTBOOKS
it "should destroy itself on confirmation", -> it "should destroy itself on confirmation", ->
@view.render().$(".delete").click() @view.render().$(".delete").click()
......
...@@ -2,10 +2,7 @@ define(["backbone", "js/models/textbook"], ...@@ -2,10 +2,7 @@ define(["backbone", "js/models/textbook"],
function(Backbone, TextbookModel) { function(Backbone, TextbookModel) {
var TextbookCollection = Backbone.Collection.extend({ var TextbookCollection = Backbone.Collection.extend({
model: TextbookModel, model: TextbookModel,
url: function() { return CMS.URL.TEXTBOOKS; }, url: function() { return CMS.URL.TEXTBOOKS; }
save: function(options) {
return this.sync('update', this, options);
}
}); });
return TextbookCollection; return TextbookCollection;
}); });
define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter", "backbone.associations"], define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter",
"backbone.associations", "coffee/src/main"],
function(Backbone, _, ChapterModel, ChapterCollection) { function(Backbone, _, ChapterModel, ChapterCollection) {
var Textbook = Backbone.AssociatedModel.extend({ var Textbook = Backbone.AssociatedModel.extend({
...@@ -32,13 +33,7 @@ define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter", ...@@ -32,13 +33,7 @@ define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter",
isEmpty: function() { isEmpty: function() {
return !this.get('name') && this.get('chapters').isEmpty(); return !this.get('name') && this.get('chapters').isEmpty();
}, },
url: function() { urlRoot: function() { return CMS.URL.TEXTBOOKS; },
if(this.isNew()) {
return CMS.URL.TEXTBOOKS + "/new";
} else {
return CMS.URL.TEXTBOOKS + "/" + this.id;
}
},
parse: function(response) { parse: function(response) {
var ret = $.extend(true, {}, response); var ret = $.extend(true, {}, response);
if("tab_title" in ret && !("name" in ret)) { if("tab_title" in ret && !("name" in ret)) {
......
...@@ -13,13 +13,14 @@ ...@@ -13,13 +13,14 @@
<h1 class="branding"><a href="/"><img src="${static.url("img/logo-edx-studio.png")}" alt="edX Studio" /></a></h1> <h1 class="branding"><a href="/"><img src="${static.url("img/logo-edx-studio.png")}" alt="edX Studio" /></a></h1>
% if context_course: % if context_course:
<% <%
ctx_loc = context_course.location ctx_loc = context_course.location
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True) location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
index_url = location.url_reverse('course') index_url = location.url_reverse('course')
checklists_url = location.url_reverse('checklists') checklists_url = location.url_reverse('checklists')
course_team_url = location.url_reverse('course_team') course_team_url = location.url_reverse('course_team')
assets_url = location.url_reverse('assets') assets_url = location.url_reverse('assets')
textbooks_url = location.url_reverse('textbooks')
import_url = location.url_reverse('import') import_url = location.url_reverse('import')
course_info_url = location.url_reverse('course_info') course_info_url = location.url_reverse('course_info')
export_url = location.url_reverse('export') export_url = location.url_reverse('export')
...@@ -58,7 +59,7 @@ ...@@ -58,7 +59,7 @@
<a href="${assets_url}">${_("Files &amp; Uploads")}</a> <a href="${assets_url}">${_("Files &amp; Uploads")}</a>
</li> </li>
<li class="nav-item nav-course-courseware-textbooks"> <li class="nav-item nav-course-courseware-textbooks">
<a href="${reverse('textbook_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Textbooks")}</a> <a href="${textbooks_url}">${_("Textbooks")}</a>
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -23,13 +23,6 @@ urlpatterns = patterns('', # nopep8 ...@@ -23,13 +23,6 @@ urlpatterns = patterns('', # nopep8
url(r'^preview/xblock/(?P<usage_id>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>[^/]*))?$', url(r'^preview/xblock/(?P<usage_id>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>[^/]*))?$',
'contentstore.views.preview_handler', name='preview_handler'), 'contentstore.views.preview_handler', name='preview_handler'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)$',
'contentstore.views.textbook_index', name='textbook_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/new$',
'contentstore.views.create_textbook', name='create_textbook'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/(?P<tid>\d[^/]*)$',
'contentstore.views.textbook_by_id', name='textbook_by_id'),
# temporary landing page for a course # temporary landing page for a course
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
'contentstore.views.landing', name='landing'), 'contentstore.views.landing', name='landing'),
...@@ -89,6 +82,8 @@ urlpatterns += patterns( ...@@ -89,6 +82,8 @@ urlpatterns += patterns(
url(r'(?ix)^settings/details/{}$'.format(parsers.URL_RE_SOURCE), 'settings_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'), url(r'(?ix)^settings/grading/{}(/)?(?P<grader_index>\d+)?$'.format(parsers.URL_RE_SOURCE), 'grading_handler'),
url(r'(?ix)^settings/advanced/{}$'.format(parsers.URL_RE_SOURCE), 'advanced_settings_handler'), url(r'(?ix)^settings/advanced/{}$'.format(parsers.URL_RE_SOURCE), 'advanced_settings_handler'),
url(r'(?ix)^textbooks/{}$'.format(parsers.URL_RE_SOURCE), 'textbooks_list_handler'),
url(r'(?ix)^textbooks/{}/(?P<tid>\d[^/]*)$'.format(parsers.URL_RE_SOURCE), 'textbooks_detail_handler'),
) )
js_info_dict = { js_info_dict = {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment