Merge pull request #1772 from edx/db/locator-refactor-textbooks
Refactor textbooks to use locator URLs
This commit is contained in:
@@ -1659,14 +1659,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
test_get_html('settings/details')
|
||||
test_get_html('settings/grading')
|
||||
test_get_html('settings/advanced')
|
||||
|
||||
# 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)
|
||||
test_get_html('textbooks')
|
||||
|
||||
# go look at a subsection page
|
||||
subsection_location = loc.replace(category='sequential', name='test_sequence')
|
||||
|
||||
@@ -14,11 +14,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
def setUp(self):
|
||||
"Set the URL for tests"
|
||||
super(TextbookIndexTestCase, self).setUp()
|
||||
self.url = reverse('textbook_index', kwargs={
|
||||
'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
})
|
||||
self.url = self.course_locator.url_reverse('textbooks')
|
||||
|
||||
def test_view_index(self):
|
||||
"Basic check that the textbook index page responds correctly"
|
||||
@@ -77,13 +73,13 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
obj = json.loads(resp.content)
|
||||
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"
|
||||
textbooks = [
|
||||
{"tab_title": "Hi, mom!"},
|
||||
{"tab_title": "Textbook 2"},
|
||||
]
|
||||
resp = self.client.post(
|
||||
resp = self.client.put(
|
||||
self.url,
|
||||
data=json.dumps(textbooks),
|
||||
content_type="application/json",
|
||||
@@ -102,9 +98,9 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
no_ids.append(textbook)
|
||||
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"
|
||||
resp = self.client.post(
|
||||
resp = self.client.put(
|
||||
self.url,
|
||||
data="invalid",
|
||||
content_type="application/json",
|
||||
@@ -122,11 +118,7 @@ class TextbookCreateTestCase(CourseTestCase):
|
||||
def setUp(self):
|
||||
"Set up a url and some textbook content for tests"
|
||||
super(TextbookCreateTestCase, self).setUp()
|
||||
self.url = reverse('create_textbook', kwargs={
|
||||
'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
})
|
||||
self.url = self.course_locator.url_reverse('textbooks')
|
||||
self.textbook = {
|
||||
"tab_title": "Economics",
|
||||
"chapters": {
|
||||
@@ -151,15 +143,6 @@ class TextbookCreateTestCase(CourseTestCase):
|
||||
del textbook["id"]
|
||||
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):
|
||||
"Textbook IDs must begin with a number; try a valid one"
|
||||
self.textbook["id"] = "7x5"
|
||||
@@ -188,12 +171,12 @@ class TextbookCreateTestCase(CourseTestCase):
|
||||
self.assertNotIn("Location", resp)
|
||||
|
||||
|
||||
class TextbookByIdTestCase(CourseTestCase):
|
||||
"Test cases for the `textbook_by_id` view"
|
||||
class TextbookDetailTestCase(CourseTestCase):
|
||||
"Test cases for the `textbook_detail_handler` view"
|
||||
|
||||
def setUp(self):
|
||||
"Set some useful content and URLs for tests"
|
||||
super(TextbookByIdTestCase, self).setUp()
|
||||
super(TextbookDetailTestCase, self).setUp()
|
||||
self.textbook1 = {
|
||||
"tab_title": "Economics",
|
||||
"id": 1,
|
||||
@@ -202,12 +185,7 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
"url": "/a/b/c/ch1.pdf",
|
||||
}
|
||||
}
|
||||
self.url1 = reverse('textbook_by_id', kwargs={
|
||||
'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
'tid': 1,
|
||||
})
|
||||
self.url1 = self.course_locator.url_reverse("textbooks", "1")
|
||||
self.textbook2 = {
|
||||
"tab_title": "Algebra",
|
||||
"id": 2,
|
||||
@@ -216,24 +194,14 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
"url": "/a/b/ch11.pdf",
|
||||
}
|
||||
}
|
||||
self.url2 = reverse('textbook_by_id', kwargs={
|
||||
'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
'tid': 2,
|
||||
})
|
||||
self.url2 = self.course_locator.url_reverse("textbooks", "2")
|
||||
self.course.pdf_textbooks = [self.textbook1, self.textbook2]
|
||||
# Save the data that we've just changed to the underlying
|
||||
# MongoKeyValueStore before we update the mongo datastore.
|
||||
self.course.save()
|
||||
self.store = get_modulestore(self.course.location)
|
||||
self.store.update_metadata(self.course.location, own_metadata(self.course))
|
||||
self.url_nonexist = reverse('textbook_by_id', kwargs={
|
||||
'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
'tid': 20,
|
||||
})
|
||||
self.url_nonexist = self.course_locator.url_reverse("textbooks", "20")
|
||||
|
||||
def test_get_1(self):
|
||||
"Get the first textbook"
|
||||
@@ -275,12 +243,7 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
"url": "supercool.pdf",
|
||||
"id": "1supercool",
|
||||
}
|
||||
url = reverse("textbook_by_id", kwargs={
|
||||
'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
'tid': "1supercool",
|
||||
})
|
||||
url = self.course_locator.url_reverse("textbooks", "1supercool")
|
||||
resp = self.client.post(
|
||||
url,
|
||||
data=json.dumps(textbook),
|
||||
|
||||
@@ -57,6 +57,7 @@ class AjaxEnabledTestClient(Client):
|
||||
"""
|
||||
return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_MODULESTORE)
|
||||
class CourseTestCase(ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
@@ -111,7 +112,7 @@ 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)
|
||||
|
||||
@@ -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 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 .component import (
|
||||
OPEN_ENDED_COMPONENT_TYPES, NOTE_COMPONENT_TYPES,
|
||||
@@ -57,8 +57,20 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler'
|
||||
'settings_handler',
|
||||
'grading_handler',
|
||||
'advanced_settings_handler',
|
||||
'textbook_index', 'textbook_by_id',
|
||||
'create_textbook']
|
||||
'textbooks_list_handler', 'textbooks_detail_handler']
|
||||
|
||||
|
||||
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
|
||||
@@ -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
|
||||
"""
|
||||
location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
|
||||
# TODO: when converting to split backend, if location does not have a usage_id,
|
||||
# we'll need to get the course's root block_id
|
||||
if not has_access(request.user, 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)
|
||||
locator, course = _get_locator_and_course(
|
||||
course_id, branch, version_guid, block, request.user, depth=3
|
||||
)
|
||||
lms_link = get_lms_link_for_item(course.location)
|
||||
sections = course.get_children()
|
||||
|
||||
return render_to_response('overview.html', {
|
||||
@@ -186,9 +191,9 @@ def course_index(request, course_id, branch, version_guid, block):
|
||||
'lms_link': lms_link,
|
||||
'sections': sections,
|
||||
'course_graders': json.dumps(
|
||||
CourseGradingModel.fetch(location).graders
|
||||
CourseGradingModel.fetch(locator).graders
|
||||
),
|
||||
'parent_locator': location,
|
||||
'parent_locator': locator,
|
||||
'new_section_category': 'chapter',
|
||||
'new_subsection_category': 'sequential',
|
||||
'new_unit_category': 'vertical',
|
||||
@@ -314,22 +319,18 @@ def course_info_handler(request, tag=None, course_id=None, branch=None, version_
|
||||
GET
|
||||
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_old_location = loc_mapper().translate_locator_to_location(course_location)
|
||||
__, course_module = _get_locator_and_course(
|
||||
course_id, branch, version_guid, block, request.user
|
||||
)
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
if not has_access(request.user, course_location):
|
||||
raise PermissionDenied()
|
||||
|
||||
course_module = modulestore().get_item(course_old_location)
|
||||
|
||||
handouts_old_location = course_old_location.replace(category='course_info', name='handouts')
|
||||
handouts_old_location = course_module.location.replace(category='course_info', name='handouts')
|
||||
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(
|
||||
course_old_location.course_id, update_location, False, True
|
||||
course_module.location.course_id, update_location, False, True
|
||||
)
|
||||
|
||||
return render_to_response(
|
||||
@@ -338,7 +339,7 @@ def course_info_handler(request, tag=None, course_id=None, branch=None, version_
|
||||
'context_course': course_module,
|
||||
'updates_url': update_locator.url_reverse('course_info_update/'),
|
||||
'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:
|
||||
@@ -407,20 +408,16 @@ def settings_handler(request, tag=None, course_id=None, branch=None, version_gui
|
||||
PUT
|
||||
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)
|
||||
if not has_access(request.user, locator):
|
||||
raise PermissionDenied()
|
||||
|
||||
locator, course_module = _get_locator_and_course(
|
||||
course_id, branch, version_guid, block, request.user
|
||||
)
|
||||
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/')
|
||||
|
||||
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),
|
||||
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_module.location),
|
||||
'course_image_url': utils.course_image_url(course_module),
|
||||
'details_url': locator.url_reverse('/settings/details/'),
|
||||
'about_page_editable': not settings.FEATURES.get(
|
||||
@@ -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 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)
|
||||
if not has_access(request.user, locator):
|
||||
raise PermissionDenied()
|
||||
locator, course_module = _get_locator_and_course(
|
||||
course_id, branch, version_guid, block, request.user
|
||||
)
|
||||
|
||||
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', {
|
||||
@@ -514,8 +509,8 @@ def _config_course_advanced_components(request, course_module):
|
||||
filter_tabs = True # Exceptional conditions will pull this to False
|
||||
if ADVANCED_COMPONENT_POLICY_KEY in request.json: # Maps tab types to components
|
||||
tab_component_map = {
|
||||
'open_ended':OPEN_ENDED_COMPONENT_TYPES,
|
||||
'notes':NOTE_COMPONENT_TYPES,
|
||||
'open_ended': OPEN_ENDED_COMPONENT_TYPES,
|
||||
'notes': NOTE_COMPONENT_TYPES,
|
||||
}
|
||||
# Check to see if the user instantiated any notes or open ended
|
||||
# components
|
||||
@@ -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
|
||||
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)
|
||||
if not has_access(request.user, locator):
|
||||
raise PermissionDenied()
|
||||
|
||||
course_old_location = loc_mapper().translate_locator_to_location(locator)
|
||||
course_module = modulestore().get_item(course_old_location)
|
||||
|
||||
locator, course_module = _get_locator_and_course(
|
||||
course_id, branch, version_guid, block, request.user
|
||||
)
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
|
||||
|
||||
return render_to_response('settings_advanced.html', {
|
||||
@@ -657,113 +648,103 @@ def assign_textbook_id(textbook, used_ids=()):
|
||||
return tid
|
||||
|
||||
|
||||
@require_http_methods(("GET", "POST", "PUT"))
|
||||
@login_required
|
||||
@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)
|
||||
store = get_modulestore(location)
|
||||
course_module = store.get_item(location, depth=3)
|
||||
locator, course = _get_locator_and_course(
|
||||
course_id, branch, version_guid, block, request.user
|
||||
)
|
||||
store = get_modulestore(course.location)
|
||||
|
||||
if request.is_ajax():
|
||||
if request.method == 'GET':
|
||||
return JsonResponse(course_module.pdf_textbooks)
|
||||
# can be either and sometimes django is rewriting one to the other:
|
||||
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,
|
||||
})
|
||||
if not "application/json" in request.META.get('HTTP_ACCEPT', 'text/html'):
|
||||
# return HTML page
|
||||
upload_asset_url = locator.url_reverse('assets/', '')
|
||||
textbook_url = locator.url_reverse('/textbooks')
|
||||
return render_to_response('textbooks.html', {
|
||||
'context_course': course_module,
|
||||
'course': course_module,
|
||||
'context_course': course,
|
||||
'textbooks': course.pdf_textbooks,
|
||||
'upload_asset_url': upload_asset_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
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def create_textbook(request, org, course, name):
|
||||
"""
|
||||
JSON API endpoint for creating a textbook. Used by the Backbone application.
|
||||
"""
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
store = get_modulestore(location)
|
||||
course_module = store.get_item(location, depth=0)
|
||||
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)
|
||||
|
||||
try:
|
||||
textbook = validate_textbook_json(request.body)
|
||||
except TextbookValidationError as err:
|
||||
return JsonResponse({"error": err.message}, status=400)
|
||||
if not textbook.get("id"):
|
||||
tids = set(t["id"] for t in course_module.pdf_textbooks if "id" in t)
|
||||
textbook["id"] = assign_textbook_id(textbook, tids)
|
||||
existing = course_module.pdf_textbooks
|
||||
existing.append(textbook)
|
||||
course_module.pdf_textbooks = existing
|
||||
if not any(tab['type'] == 'pdf_textbooks' for tab in course_module.tabs):
|
||||
tabs = course_module.tabs
|
||||
tabs.append({"type": "pdf_textbooks"})
|
||||
course_module.tabs = tabs
|
||||
# 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))
|
||||
resp = JsonResponse(textbook, status=201)
|
||||
resp["Location"] = reverse("textbook_by_id", kwargs={
|
||||
'org': org,
|
||||
'course': course,
|
||||
'name': name,
|
||||
'tid': textbook["id"],
|
||||
})
|
||||
return resp
|
||||
if not any(tab['type'] == 'pdf_textbooks' for tab in course.tabs):
|
||||
course.tabs.append({"type": "pdf_textbooks"})
|
||||
course.pdf_textbooks = textbooks
|
||||
store.update_metadata(
|
||||
course.location,
|
||||
own_metadata(course)
|
||||
)
|
||||
return JsonResponse(course.pdf_textbooks)
|
||||
elif request.method == 'POST':
|
||||
# create a new textbook for the course
|
||||
try:
|
||||
textbook = validate_textbook_json(request.body)
|
||||
except TextbookValidationError as err:
|
||||
return JsonResponse({"error": err.message}, status=400)
|
||||
if not textbook.get("id"):
|
||||
tids = set(t["id"] for t in course.pdf_textbooks if "id" in t)
|
||||
textbook["id"] = assign_textbook_id(textbook, tids)
|
||||
existing = course.pdf_textbooks
|
||||
existing.append(textbook)
|
||||
course.pdf_textbooks = existing
|
||||
if not any(tab['type'] == 'pdf_textbooks' for tab in course.tabs):
|
||||
tabs = course.tabs
|
||||
tabs.append({"type": "pdf_textbooks"})
|
||||
course.tabs = tabs
|
||||
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
|
||||
@ensure_csrf_cookie
|
||||
@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.
|
||||
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)
|
||||
store = get_modulestore(location)
|
||||
course_module = store.get_item(location, depth=3)
|
||||
matching_id = [tb for tb in course_module.pdf_textbooks
|
||||
__, course = _get_locator_and_course(
|
||||
course_id, branch, version_guid, block, request.user
|
||||
)
|
||||
store = get_modulestore(course.location)
|
||||
matching_id = [tb for tb in course.pdf_textbooks
|
||||
if str(tb.get("id")) == str(tid)]
|
||||
if matching_id:
|
||||
textbook = matching_id[0]
|
||||
@@ -782,32 +763,28 @@ def textbook_by_id(request, org, course, name, tid):
|
||||
return JsonResponse({"error": err.message}, status=400)
|
||||
new_textbook["id"] = tid
|
||||
if textbook:
|
||||
i = course_module.pdf_textbooks.index(textbook)
|
||||
new_textbooks = course_module.pdf_textbooks[0:i]
|
||||
i = course.pdf_textbooks.index(textbook)
|
||||
new_textbooks = course.pdf_textbooks[0:i]
|
||||
new_textbooks.append(new_textbook)
|
||||
new_textbooks.extend(course_module.pdf_textbooks[i + 1:])
|
||||
course_module.pdf_textbooks = new_textbooks
|
||||
new_textbooks.extend(course.pdf_textbooks[i + 1:])
|
||||
course.pdf_textbooks = new_textbooks
|
||||
else:
|
||||
course_module.pdf_textbooks.append(new_textbook)
|
||||
# Save the data that we've just changed to the underlying
|
||||
# MongoKeyValueStore before we update the mongo datastore.
|
||||
course_module.save()
|
||||
course.pdf_textbooks.append(new_textbook)
|
||||
store.update_metadata(
|
||||
course_module.location,
|
||||
own_metadata(course_module)
|
||||
course.location,
|
||||
own_metadata(course)
|
||||
)
|
||||
return JsonResponse(new_textbook, status=201)
|
||||
elif request.method == 'DELETE':
|
||||
if not textbook:
|
||||
return JsonResponse(status=404)
|
||||
i = course_module.pdf_textbooks.index(textbook)
|
||||
new_textbooks = course_module.pdf_textbooks[0:i]
|
||||
new_textbooks.extend(course_module.pdf_textbooks[i + 1:])
|
||||
course_module.pdf_textbooks = new_textbooks
|
||||
course_module.save()
|
||||
i = course.pdf_textbooks.index(textbook)
|
||||
new_textbooks = course.pdf_textbooks[0:i]
|
||||
new_textbooks.extend(course.pdf_textbooks[i + 1:])
|
||||
course.pdf_textbooks = new_textbooks
|
||||
store.update_metadata(
|
||||
course_module.location,
|
||||
own_metadata(course_module)
|
||||
course.location,
|
||||
own_metadata(course)
|
||||
)
|
||||
return JsonResponse()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require ["jquery", "backbone", "coffee/src/main", "sinon", "jasmine-stealth"],
|
||||
require ["jquery", "backbone", "coffee/src/main", "sinon", "jasmine-stealth", "jquery.cookie"],
|
||||
($, Backbone, main, sinon) ->
|
||||
describe "CMS", ->
|
||||
it "should initialize URL", ->
|
||||
|
||||
@@ -11,6 +11,10 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/
|
||||
beforeEach ->
|
||||
main()
|
||||
@model = new Textbook()
|
||||
CMS.URL.TEXTBOOKS = "/textbooks"
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.TEXTBOOKS
|
||||
|
||||
describe "Basic", ->
|
||||
it "should have an empty name by default", ->
|
||||
@@ -28,8 +32,9 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/
|
||||
it "should be empty by default", ->
|
||||
expect(@model.isEmpty()).toBeTruthy()
|
||||
|
||||
it "should have a URL set", ->
|
||||
expect(@model.url()).toBeTruthy()
|
||||
it "should have a URL root", ->
|
||||
urlRoot = _.result(@model, 'urlRoot')
|
||||
expect(urlRoot).toBeTruthy()
|
||||
|
||||
it "should be able to reset itself", ->
|
||||
@model.set("name", "foobar")
|
||||
@@ -135,12 +140,8 @@ define ["backbone", "js/models/textbook", "js/collections/textbook", "js/models/
|
||||
delete CMS.URL.TEXTBOOKS
|
||||
|
||||
it "should have a url set", ->
|
||||
expect(@collection.url()).toEqual("/textbooks")
|
||||
|
||||
it "can call save", ->
|
||||
spyOn(@collection, "sync")
|
||||
@collection.save()
|
||||
expect(@collection.sync).toHaveBeenCalledWith("update", @collection, undefined)
|
||||
url = _.result(@collection, 'url')
|
||||
expect(url).toEqual("/textbooks")
|
||||
|
||||
|
||||
describe "Chapter model", ->
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/section",
|
||||
define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js/models/course",
|
||||
"js/collections/textbook", "js/views/show_textbook", "js/views/edit_textbook", "js/views/list_textbooks",
|
||||
"js/views/edit_chapter", "js/views/feedback_prompt", "js/views/feedback_notification",
|
||||
"sinon", "jasmine-stealth"],
|
||||
(Textbook, Chapter, ChapterSet, Section, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, sinon) ->
|
||||
(Textbook, Chapter, ChapterSet, Course, TextbookSet, ShowTextbook, EditTextbook, ListTexbook, EditChapter, Prompt, Notification, sinon) ->
|
||||
feedbackTpl = readFixtures('system-feedback.underscore')
|
||||
|
||||
beforeEach ->
|
||||
@@ -30,7 +30,7 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
|
||||
|
||||
@promptSpies = spyOnConstructor(Prompt, "Warning", ["show", "hide"])
|
||||
@promptSpies.show.andReturn(@promptSpies)
|
||||
window.section = new Section({
|
||||
window.course = new Course({
|
||||
id: "5",
|
||||
name: "Course Name",
|
||||
url_name: "course_name",
|
||||
@@ -40,7 +40,7 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
|
||||
});
|
||||
|
||||
afterEach ->
|
||||
delete window.section
|
||||
delete window.course
|
||||
|
||||
describe "Basic", ->
|
||||
it "should render properly", ->
|
||||
@@ -81,9 +81,11 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
|
||||
@savingSpies = spyOnConstructor(Notification, "Mini",
|
||||
["show", "hide"])
|
||||
@savingSpies.show.andReturn(@savingSpies)
|
||||
CMS.URL.TEXTBOOKS = "/textbooks"
|
||||
|
||||
afterEach ->
|
||||
@xhr.restore()
|
||||
delete CMS.URL.TEXTBOOKS
|
||||
|
||||
it "should destroy itself on confirmation", ->
|
||||
@view.render().$(".delete").click()
|
||||
@@ -283,11 +285,11 @@ define ["js/models/textbook", "js/models/chapter", "js/collections/chapter", "js
|
||||
@view = new EditChapter({model: @model})
|
||||
spyOn(@view, "remove").andCallThrough()
|
||||
CMS.URL.UPLOAD_ASSET = "/upload"
|
||||
window.section = new Section({name: "abcde"})
|
||||
window.course = new Course({name: "abcde"})
|
||||
|
||||
afterEach ->
|
||||
delete CMS.URL.UPLOAD_ASSET
|
||||
delete window.section
|
||||
delete window.course
|
||||
|
||||
it "can render", ->
|
||||
@view.render()
|
||||
|
||||
@@ -2,10 +2,7 @@ define(["backbone", "js/models/textbook"],
|
||||
function(Backbone, TextbookModel) {
|
||||
var TextbookCollection = Backbone.Collection.extend({
|
||||
model: TextbookModel,
|
||||
url: function() { return CMS.URL.TEXTBOOKS; },
|
||||
save: function(options) {
|
||||
return this.sync('update', this, options);
|
||||
}
|
||||
url: function() { return CMS.URL.TEXTBOOKS; }
|
||||
});
|
||||
return TextbookCollection;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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) {
|
||||
|
||||
var Textbook = Backbone.AssociatedModel.extend({
|
||||
@@ -32,13 +33,7 @@ define(["backbone", "underscore", "js/models/chapter", "js/collections/chapter",
|
||||
isEmpty: function() {
|
||||
return !this.get('name') && this.get('chapters').isEmpty();
|
||||
},
|
||||
url: function() {
|
||||
if(this.isNew()) {
|
||||
return CMS.URL.TEXTBOOKS + "/new";
|
||||
} else {
|
||||
return CMS.URL.TEXTBOOKS + "/" + this.id;
|
||||
}
|
||||
},
|
||||
urlRoot: function() { return CMS.URL.TEXTBOOKS; },
|
||||
parse: function(response) {
|
||||
var ret = $.extend(true, {}, response);
|
||||
if("tab_title" in ret && !("name" in ret)) {
|
||||
|
||||
@@ -53,7 +53,7 @@ define(["backbone", "underscore", "underscore.string", "jquery", "gettext", "js/
|
||||
});
|
||||
var msg = new FileUploadModel({
|
||||
title: _.template(gettext("Upload a new PDF to “<%= name %>”"),
|
||||
{name: section.escape('name')}),
|
||||
{name: course.escape('name')}),
|
||||
message: "Files must be in PDF format.",
|
||||
mimeTypes: ['application/pdf']
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ define(["backbone", "underscore", "gettext", "js/views/feedback_notification", "
|
||||
render: function() {
|
||||
var attrs = $.extend({}, this.model.attributes);
|
||||
attrs.bookindex = this.model.collection.indexOf(this.model);
|
||||
attrs.course = window.section.attributes;
|
||||
attrs.course = window.course.attributes;
|
||||
this.$el.html(this.template(attrs));
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -23,14 +23,7 @@ CMS.URL.TEXTBOOKS = "${textbook_url}"
|
||||
CMS.URL.LMS_BASE = "${settings.LMS_BASE}"
|
||||
require(["js/models/section", "js/collections/textbook", "js/views/list_textbooks"],
|
||||
function(Section, TextbookCollection, ListTextbooksView) {
|
||||
window.section = new Section({
|
||||
name: "${course.display_name_with_default | h}",
|
||||
url_name: "${course.location.name | h}",
|
||||
org: "${course.location.org | h}",
|
||||
num: "${course.location.course | h}",
|
||||
revision: "${course.location.revision | h}"
|
||||
});
|
||||
var textbooks = new TextbookCollection(${json.dumps(course.pdf_textbooks)}, {parse: true});
|
||||
var textbooks = new TextbookCollection(${json.dumps(textbooks)}, {parse: true});
|
||||
var tbView = new ListTextbooksView({collection: textbooks});
|
||||
|
||||
$(function() {
|
||||
|
||||
@@ -13,13 +13,14 @@
|
||||
<h1 class="branding"><a href="/"><img src="${static.url("img/logo-edx-studio.png")}" alt="edX Studio" /></a></h1>
|
||||
|
||||
% if context_course:
|
||||
<%
|
||||
<%
|
||||
ctx_loc = context_course.location
|
||||
location = loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)
|
||||
index_url = location.url_reverse('course')
|
||||
checklists_url = location.url_reverse('checklists')
|
||||
course_team_url = location.url_reverse('course_team')
|
||||
assets_url = location.url_reverse('assets')
|
||||
textbooks_url = location.url_reverse('textbooks')
|
||||
import_url = location.url_reverse('import')
|
||||
course_info_url = location.url_reverse('course_info')
|
||||
export_url = location.url_reverse('export')
|
||||
@@ -58,7 +59,7 @@
|
||||
<a href="${assets_url}">${_("Files & Uploads")}</a>
|
||||
</li>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -23,13 +23,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>[^/]+)/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
|
||||
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$',
|
||||
'contentstore.views.landing', name='landing'),
|
||||
@@ -89,6 +82,8 @@ urlpatterns += patterns(
|
||||
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/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 = {
|
||||
|
||||
Reference in New Issue
Block a user