diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py
index b7611969c1..314793db72 100644
--- a/cms/djangoapps/contentstore/course_info_model.py
+++ b/cms/djangoapps/contentstore/course_info_model.py
@@ -1,20 +1,20 @@
+import re
+import logging
+from lxml import html, etree
+from django.http import HttpResponseBadRequest
+import django.utils
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore
-from lxml import html, etree
-import re
-from django.http import HttpResponseBadRequest
-import logging
-import django.utils
# # TODO store as array of { date, content } and override course_info_module.definition_from_xml
# # This should be in a class which inherits from XmlDescriptor
log = logging.getLogger(__name__)
-def get_course_updates(location):
+def get_course_updates(location, provided_id):
"""
Retrieve the relevant course_info updates and unpack into the model which the client expects:
- [{id : location.url() + idx to make unique, date : string, content : html string}]
+ [{id : index, date : string, content : html string}]
"""
try:
course_updates = modulestore('direct').get_item(location)
@@ -35,15 +35,23 @@ def get_course_updates(location):
# Confirm that root is
, iterate over
, pull out
subs and then rest of val
course_upd_collection = []
+ provided_id = get_idx(provided_id) if provided_id is not None else None
if course_html_parsed.tag == 'ol':
# 0 is the newest
for idx, update in enumerate(course_html_parsed):
if len(update) > 0:
content = _course_info_content(update)
# make the id on the client be 1..len w/ 1 being the oldest and len being the newest
- course_upd_collection.append({"id": location_base + "/" + str(len(course_html_parsed) - idx),
- "date": update.findtext("h2"),
- "content": content})
+ computed_id = len(course_html_parsed) - idx
+ payload = {
+ "id": computed_id,
+ "date": update.findtext("h2"),
+ "content": content
+ }
+ if provided_id is None:
+ course_upd_collection.append(payload)
+ elif provided_id == computed_id:
+ return payload
return course_upd_collection
@@ -57,7 +65,8 @@ def update_course_updates(location, update, passed_id=None):
try:
course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
- return HttpResponseBadRequest()
+ modulestore('direct').create_and_save_xmodule(location)
+ course_updates = modulestore('direct').get_item(location)
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try:
@@ -89,17 +98,17 @@ def update_course_updates(location, update, passed_id=None):
course_html_parsed[-idx] = new_html_parsed
else:
course_html_parsed.insert(0, new_html_parsed)
-
idx = len(course_html_parsed)
- passed_id = course_updates.location.url() + "/" + str(idx)
# update db record
course_updates.data = html.tostring(course_html_parsed)
modulestore('direct').update_item(location, course_updates.data)
- return {"id": passed_id,
- "date": update['date'],
- "content": _course_info_content(new_html_parsed)}
+ return {
+ "id": idx,
+ "date": update['date'],
+ "content": _course_info_content(new_html_parsed),
+ }
def _course_info_content(html_parsed):
@@ -115,6 +124,7 @@ def _course_info_content(html_parsed):
return content
+# pylint: disable=unused-argument
def delete_course_update(location, update, passed_id):
"""
Delete the given course_info update from the db.
@@ -150,7 +160,7 @@ def delete_course_update(location, update, passed_id):
store = modulestore('direct')
store.update_item(location, course_updates.data)
- return get_course_updates(location)
+ return get_course_updates(location, None)
def get_idx(passed_id):
@@ -160,3 +170,5 @@ def get_idx(passed_id):
idx_matcher = re.search(r'.*?/?(\d+)$', passed_id)
if idx_matcher:
return int(idx_matcher.group(1))
+ else:
+ return None
diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py
deleted file mode 100644
index b43a32f635..0000000000
--- a/cms/djangoapps/contentstore/module_info_model.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from static_replace import replace_static_urls
-from xmodule.modulestore.exceptions import ItemNotFoundError
-
-
-def get_module_info(store, location, rewrite_static_links=False):
- try:
- module = store.get_item(location)
- except ItemNotFoundError:
- # create a new one
- store.create_and_save_xmodule(location)
- module = store.get_item(location)
-
- data = module.data
- if rewrite_static_links:
- # we pass a partially bogus course_id as we don't have the RUN information passed yet
- # through the CMS. Also the contentstore is also not RUN-aware at this point in time.
- data = replace_static_urls(
- module.data,
- None,
- course_id=module.location.org + '/' + module.location.course + '/BOGUS_RUN_REPLACE_WHEN_AVAILABLE'
- )
-
- return {
- 'id': module.location.url(),
- 'data': data,
- # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
- # what's the intent here? all metadata incl inherited & namespaced?
- 'metadata': module.xblock_kvs._metadata
- }
-
-
-def set_module_info(store, location, post_data):
- module = None
- try:
- module = store.get_item(location)
- except ItemNotFoundError:
- # new module at this location: almost always used for the course about pages; thus, no parent. (there
- # are quite a handful of about page types available for a course and only the overview is pre-created)
- store.create_and_save_xmodule(location)
- module = store.get_item(location)
-
- if post_data.get('data') is not None:
- data = post_data['data']
- store.update_item(location, data)
-
- # cdodge: note calling request.POST.get('children') will return None if children is an empty array
- # so it lead to a bug whereby the last component to be deleted in the UI was not actually
- # deleting the children object from the children collection
- if 'children' in post_data and post_data['children'] is not None:
- children = post_data['children']
- store.update_children(location, children)
-
- # cdodge: also commit any metadata which might have been passed along in the
- # POST from the client, if it is there
- # NOTE, that the postback is not the complete metadata, as there's system metadata which is
- # not presented to the end-user for editing. So let's fetch the original and
- # 'apply' the submitted metadata, so we don't end up deleting system metadata
- if post_data.get('metadata') is not None:
- posted_metadata = post_data['metadata']
-
- # update existing metadata with submitted metadata (which can be partial)
- # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
- for metadata_key, value in posted_metadata.items():
-
- if posted_metadata[metadata_key] is None:
- # remove both from passed in collection as well as the collection read in from the modulestore
- if module._field_data.has(module, metadata_key):
- module._field_data.delete(module, metadata_key)
- del posted_metadata[metadata_key]
- else:
- module._field_data.set(module, metadata_key, value)
-
- # commit to datastore
- # TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
- store.update_metadata(location, module.xblock_kvs._metadata)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index a207b9283f..a6fa4e77b3 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -1200,9 +1200,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(module_store, 'common/test/data/', ['toy'])
handout_location = Location(['i4x', 'edX', 'toy', 'course_info', 'handouts'])
+ # get the translation
+ handouts_locator = loc_mapper().translate_location('edX/toy/2012_Fall', handout_location)
- # get module info
- resp = self.client.get_html(reverse('module_info', kwargs={'module_location': handout_location}))
+ # get module info (json)
+ resp = self.client.get(handouts_locator.url_reverse('/xblock', ''))
# make sure we got a successful response
self.assertEqual(resp.status_code, 200)
@@ -1600,10 +1602,7 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 200)
# course info
- resp = self.client.get(reverse('course_info',
- kwargs={'org': loc.org,
- 'course': loc.course,
- 'name': loc.name}))
+ resp = self.client.get(new_location.url_reverse('course_info'))
self.assertEqual(resp.status_code, 200)
# settings_details
@@ -1627,14 +1626,15 @@ class ContentStoreTest(ModuleStoreTestCase):
# go look at a subsection page
subsection_location = loc.replace(category='sequential', name='test_sequence')
- resp = self.client.get_html(reverse('edit_subsection',
- kwargs={'location': subsection_location.url()}))
+ resp = self.client.get_html(
+ reverse('edit_subsection', kwargs={'location': subsection_location.url()})
+ )
self.assertEqual(resp.status_code, 200)
# go look at the Edit page
unit_location = loc.replace(category='vertical', name='test_vertical')
- resp = self.client.get_html(reverse('edit_unit',
- kwargs={'location': unit_location.url()}))
+ resp = self.client.get_html(
+ reverse('edit_unit', kwargs={'location': unit_location.url()}))
self.assertEqual(resp.status_code, 200)
def delete_item(category, name):
diff --git a/cms/djangoapps/contentstore/tests/test_course_updates.py b/cms/djangoapps/contentstore/tests/test_course_updates.py
index 1ea500c6f0..5ee5f1289b 100644
--- a/cms/djangoapps/contentstore/tests/test_course_updates.py
+++ b/cms/djangoapps/contentstore/tests/test_course_updates.py
@@ -1,8 +1,7 @@
'''unit tests for course_info views and models.'''
from contentstore.tests.test_course_settings import CourseTestCase
-from django.core.urlresolvers import reverse
import json
-from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.django import modulestore, loc_mapper
class CourseUpdateTest(CourseTestCase):
@@ -15,61 +14,61 @@ class CourseUpdateTest(CourseTestCase):
Does not supply a provided_id.
"""
- payload = {'content': content,
- 'date': date}
- url = reverse('course_info_json',
- kwargs={'org': self.course.location.org,
- 'course': self.course.location.course,
- 'provided_id': ''})
+ payload = {'content': content, 'date': date}
+ url = update_locator.url_reverse('course_info_update/')
resp = self.client.ajax_post(url, payload)
+ self.assertContains(resp, '', status_code=200)
return json.loads(resp.content)
- # first get the update to force the creation
- url = reverse('course_info',
- kwargs={'org': self.course.location.org,
- 'course': self.course.location.course,
- 'name': self.course.location.name})
- self.client.get(url)
+ course_locator = loc_mapper().translate_location(
+ self.course.location.course_id, self.course.location, False, True
+ )
+ resp = self.client.get_html(course_locator.url_reverse('course_info/'))
+ self.assertContains(resp, 'Course Updates', status_code=200)
+ update_locator = loc_mapper().translate_location(
+ self.course.location.course_id, self.course.location.replace(category='course_info', name='updates'),
+ False, True
+ )
init_content = ''
payload = get_response(content, 'January 8, 2013')
self.assertHTMLEqual(payload['content'], content)
- first_update_url = reverse('course_info_json',
- kwargs={'org': self.course.location.org,
- 'course': self.course.location.course,
- 'provided_id': payload['id']})
+ first_update_url = update_locator.url_reverse('course_info_update', str(payload['id']))
content += '
div
p
'
payload['content'] = content
# POST requests were coming in w/ these header values causing an error; so, repro error here
- resp = self.client.post(first_update_url, json.dumps(payload),
- "application/json",
- HTTP_X_HTTP_METHOD_OVERRIDE="PUT",
- REQUEST_METHOD="POST")
+ resp = self.client.ajax_post(
+ first_update_url, payload, HTTP_X_HTTP_METHOD_OVERRIDE="PUT", REQUEST_METHOD="POST"
+ )
self.assertHTMLEqual(content, json.loads(resp.content)['content'],
"iframe w/ div")
+ # refetch using provided id
+ refetched = self.client.get_json(first_update_url)
+ self.assertHTMLEqual(
+ content, json.loads(refetched.content)['content'], "get w/ provided id"
+ )
+
# now put in an evil update
content = ''
payload = get_response(content, 'January 11, 2013')
self.assertHTMLEqual(content, payload['content'], "self closing ol")
- url = reverse('course_info_json',
- kwargs={'org': self.course.location.org,
- 'course': self.course.location.course,
- 'provided_id': ''})
- resp = self.client.get(url)
+ course_update_url = update_locator.url_reverse('course_info_update/')
+ resp = self.client.get_json(course_update_url)
payload = json.loads(resp.content)
self.assertTrue(len(payload) == 2)
# try json w/o required fields
- self.assertContains(self.client.post(url, json.dumps({'garbage': 1}),
- "application/json"),
- 'Failed to save', status_code=400)
+ self.assertContains(
+ self.client.ajax_post(course_update_url, {'garbage': 1}),
+ 'Failed to save', status_code=400
+ )
# test an update with text in the tail of the header
content = 'outside inside after'
@@ -77,28 +76,22 @@ class CourseUpdateTest(CourseTestCase):
self.assertHTMLEqual(content, payload['content'], "text outside tag")
# now try to update a non-existent update
- url = reverse('course_info_json',
- kwargs={'org': self.course.location.org,
- 'course': self.course.location.course,
- 'provided_id': '9'})
content = 'blah blah'
- payload = {'content': content,
- 'date': 'January 21, 2013'}
+ payload = {'content': content, 'date': 'January 21, 2013'}
self.assertContains(
- self.client.ajax_post(url, payload),
- 'Failed to save', status_code=400)
+ self.client.ajax_post(course_update_url + '/9', payload),
+ 'Failed to save', status_code=400
+ )
# update w/ malformed html
content = 'error'
payload = {'content': content,
'date': 'January 11, 2013'}
- url = reverse('course_info_json', kwargs={'org': self.course.location.org,
- 'course': self.course.location.course,
- 'provided_id': ''})
self.assertContains(
- self.client.ajax_post(url, payload),
- ''
- payload = {'content': content,
- 'date': 'January 8, 2013'}
- url = reverse('course_info_json',
- kwargs={'org': self.course.location.org,
- 'course': self.course.location.course,
- 'provided_id': ''})
+ payload = {'content': content, 'date': 'January 8, 2013'}
- resp = self.client.ajax_post(url, payload)
+ update_locator = loc_mapper().translate_location(
+ self.course.location.course_id, location, False, True
+ )
+ course_update_url = update_locator.url_reverse('course_info_update/')
+ resp = self.client.ajax_post(course_update_url, payload)
payload = json.loads(resp.content)
self.assertHTMLEqual(payload['content'], content)
# now confirm that the bad news and the iframe make up 2 updates
- url = reverse('course_info_json',
- kwargs={'org': self.course.location.org,
- 'course': self.course.location.course,
- 'provided_id': ''})
- resp = self.client.get(url)
+ resp = self.client.get_json(course_update_url)
payload = json.loads(resp.content)
self.assertTrue(len(payload) == 2)
diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py
index 68782a8181..b59f214054 100644
--- a/cms/djangoapps/contentstore/tests/utils.py
+++ b/cms/djangoapps/contentstore/tests/utils.py
@@ -49,6 +49,12 @@ class AjaxEnabledTestClient(Client):
"""
return self.get(path, data or {}, follow, HTTP_ACCEPT="text/html", **extra)
+ def get_json(self, path, data=None, follow=False, **extra):
+ """
+ Convenience method for client.get which sets the accept type to json
+ """
+ return self.get(path, data or {}, follow, HTTP_ACCEPT="application/json", **extra)
+
@override_settings(MODULESTORE=TEST_MODULESTORE)
class CourseTestCase(ModuleStoreTestCase):
def setUp(self):
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 2cc31bcea0..69d6a9e352 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -21,9 +21,7 @@ from xmodule.modulestore.django import loc_mapper
from xblock.fields import Scope
from util.json_request import expect_json, JsonResponse
-from contentstore.module_info_model import get_module_info, set_module_info
-from contentstore.utils import (get_modulestore, get_lms_link_for_item,
- compute_unit_state, UnitState, get_course_for_item)
+from contentstore.utils import get_lms_link_for_item, compute_unit_state, UnitState, get_course_for_item
from models.settings.course_grading import CourseGradingModel
@@ -41,7 +39,7 @@ __all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'create_draft',
'publish_draft',
'unpublish_unit',
- 'module_info']
+ ]
log = logging.getLogger(__name__)
@@ -240,7 +238,7 @@ def edit_unit(request, location):
pass
else:
log.error(
- "Improper format for course advanced keys! %",
+ "Improper format for course advanced keys! %s",
course_advanced_keys
)
@@ -393,39 +391,3 @@ def unpublish_unit(request):
_xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
return HttpResponse()
-
-
-@expect_json
-@require_http_methods(("GET", "POST", "PUT"))
-@login_required
-@ensure_csrf_cookie
-def module_info(request, module_location):
- "Get or set information for a module in the modulestore"
- location = Location(module_location)
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
- logging.debug('rewrite_static_links = {0} {1}'.format(
- request.GET.get('rewrite_url_links', False),
- rewrite_static_links)
- )
-
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
- raise PermissionDenied()
-
- if request.method == 'GET':
- rsp = get_module_info(
- get_modulestore(location),
- location,
- rewrite_static_links=rewrite_static_links
- )
- elif request.method in ("POST", "PUT"):
- rsp = set_module_info(
- get_modulestore(location),
- location, request.json
- )
- return JsonResponse(rsp)
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index 2a84d369a1..872eaf22c5 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -54,8 +54,8 @@ 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
-__all__ = ['course_info', 'course_handler',
- 'course_info_updates', 'get_course_settings',
+__all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler',
+ 'get_course_settings',
'course_config_graders_page',
'course_config_advanced_page',
'course_settings_updates',
@@ -64,6 +64,7 @@ __all__ = ['course_info', 'course_handler',
'create_textbook']
+# pylint: disable=unused-argument
@login_required
def course_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None):
"""
@@ -299,61 +300,80 @@ def create_new_course(request):
return JsonResponse({'url': new_location.url_reverse("course/", "")})
+# pylint: disable=unused-argument
@login_required
@ensure_csrf_cookie
-def course_info(request, org, course, name, provided_id=None):
+@require_http_methods(["GET"])
+def course_info_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 info to the
- client.
-
- org, course, name: Attributes of the Location for the item to edit
+ GET
+ html: return html for editing the course info handouts and updates.
"""
- location = get_location_and_verify_access(request, org, course, name)
+ 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)
+ 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(location)
+ course_module = modulestore().get_item(course_old_location)
- # get current updates
- location = Location(['i4x', org, course, 'course_info', "updates"])
+ handouts_old_location = course_old_location.replace(category='course_info', name='handouts')
+ handouts_locator = loc_mapper().translate_location(
+ course_old_location.course_id, handouts_old_location, False, True
+ )
- return render_to_response(
- 'course_info.html',
- {
- 'context_course': course_module,
- 'url_base': "/" + org + "/" + course + "/",
- 'course_updates': json.dumps(get_course_updates(location)),
- 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url(),
- 'base_asset_url': StaticContent.get_base_url_path_for_course_assets(location) + '/'
- }
- )
+ update_location = course_old_location.replace(category='course_info', name='updates')
+ update_locator = loc_mapper().translate_location(
+ course_old_location.course_id, update_location, False, True
+ )
-@expect_json
+ return render_to_response(
+ 'course_info.html',
+ {
+ '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) + '/'
+ }
+ )
+ else:
+ return HttpResponseBadRequest("Only supports html requests")
+
+
+# pylint: disable=unused-argument
+@login_required
+@ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT", "DELETE"))
-@login_required
-@ensure_csrf_cookie
-def course_info_updates(request, org, course, provided_id=None):
+@expect_json
+def course_info_update_handler(
+ request, tag=None, course_id=None, branch=None, version_guid=None, block=None, provided_id=None
+ ):
"""
restful CRUD operations on course_info updates.
-
- org, course: Attributes of the Location for the item to edit
- provided_id should be none if it's new (create) and a composite of the
- update db id + index otherwise.
+ provided_id should be none if it's new (create) and index otherwise.
+ GET
+ json: return the course info update models
+ POST
+ json: create an update
+ PUT or DELETE
+ json: change an existing update
"""
- # ??? No way to check for access permission afaik
- # get current updates
- location = ['i4x', org, course, 'course_info', "updates"]
-
+ if 'application/json' not in request.META.get('HTTP_ACCEPT', 'application/json'):
+ return HttpResponseBadRequest("Only supports json requests")
+ updates_locator = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
+ updates_location = loc_mapper().translate_locator_to_location(updates_locator)
if provided_id == '':
provided_id = None
- # check that logged in user has permissions to this item
- if not has_access(request.user, location):
+ # check that logged in user has permissions to this item (GET shouldn't require this level?)
+ if not has_access(request.user, updates_location):
raise PermissionDenied()
if request.method == 'GET':
- return JsonResponse(get_course_updates(location))
+ return JsonResponse(get_course_updates(updates_location, provided_id))
elif request.method == 'DELETE':
try:
- return JsonResponse(delete_course_update(location, request.json, provided_id))
+ return JsonResponse(delete_course_update(updates_location, request.json, provided_id))
except:
return HttpResponseBadRequest(
"Failed to delete",
@@ -362,7 +382,7 @@ def course_info_updates(request, org, course, provided_id=None):
# can be either and sometimes django is rewriting one to the other:
elif request.method in ('POST', 'PUT'):
try:
- return JsonResponse(update_course_updates(location, request.json, provided_id))
+ return JsonResponse(update_course_updates(updates_location, request.json, provided_id))
except:
return HttpResponseBadRequest(
"Failed to save",
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index a5a4ef7ba6..4a21b19a50 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -3,6 +3,7 @@
import logging
from uuid import uuid4
from requests.packages.urllib3.util import parse_url
+from static_replace import replace_static_urls
from django.core.exceptions import PermissionDenied, ValidationError
from django.contrib.auth.decorators import login_required
@@ -24,6 +25,7 @@ from xmodule.x_module import XModuleDescriptor
from django.views.decorators.http import require_http_methods
from xmodule.modulestore.locator import BlockUsageLocator
from student.models import CourseEnrollment
+from xblock.fields import Scope
__all__ = ['save_item', 'create_item', 'orphan', 'xblock_handler']
@@ -33,7 +35,8 @@ log = logging.getLogger(__name__)
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
-@require_http_methods(("DELETE"))
+# pylint: disable=unused-argument
+@require_http_methods(("DELETE", "GET", "PUT", "POST"))
@login_required
@expect_json
def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=None, block=None):
@@ -44,10 +47,19 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
json: delete this xblock instance from the course. Supports query parameters "recurse" to delete
all children and "all_versions" to delete from all (mongo) versions.
"""
- if request.method == 'DELETE':
- location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
- if not has_access(request.user, location):
- raise PermissionDenied()
+ location = BlockUsageLocator(course_id=course_id, branch=branch, version_guid=version_guid, usage_id=block)
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ if request.method == 'GET':
+ rewrite_static_links = request.GET.get('rewrite_url_links', 'True') in ['True', 'true']
+ rsp = _get_module_info(location, rewrite_static_links=rewrite_static_links)
+ return JsonResponse(rsp)
+ elif request.method in ("POST", "PUT"):
+ # Replace w/ save_item from below
+ rsp = _set_module_info(location, request.json)
+ return JsonResponse(rsp)
+ elif request.method == 'DELETE':
old_location = loc_mapper().translate_locator_to_location(location)
@@ -261,3 +273,104 @@ def orphan(request, tag=None, course_id=None, branch=None, version_guid=None, bl
return JsonResponse({'deleted': items})
else:
raise PermissionDenied()
+
+
+def _get_module_info(usage_loc, rewrite_static_links=False):
+ """
+ metadata, data, id representation of a leaf module fetcher.
+ :param usage_loc: A BlockUsageLocator
+ """
+ old_location = loc_mapper().translate_locator_to_location(usage_loc)
+ store = get_modulestore(old_location)
+ try:
+ module = store.get_item(old_location)
+ except ItemNotFoundError:
+ if old_location.category in ['course_info']:
+ # create a new one
+ store.create_and_save_xmodule(old_location)
+ module = store.get_item(old_location)
+ else:
+ raise
+
+ data = module.data
+ if rewrite_static_links:
+ # we pass a partially bogus course_id as we don't have the RUN information passed yet
+ # through the CMS. Also the contentstore is also not RUN-aware at this point in time.
+ data = replace_static_urls(
+ module.data,
+ None,
+ course_id=module.location.org + '/' + module.location.course + '/BOGUS_RUN_REPLACE_WHEN_AVAILABLE'
+ )
+
+ return {
+ 'id': unicode(usage_loc),
+ 'data': data,
+ 'metadata': module.get_explicitly_set_fields_by_scope(Scope.settings)
+ }
+
+
+def _set_module_info(usage_loc, post_data):
+ """
+ Old metadata, data, id representation leaf module updater.
+ :param usage_loc: a BlockUsageLocator
+ :param post_data: the payload with data, metadata, and possibly children (even tho the getter
+ doesn't support children)
+ """
+ # TODO replace with save_item: differences
+ # - this doesn't handle nullout
+ # - this returns the new model
+ old_location = loc_mapper().translate_locator_to_location(usage_loc)
+ store = get_modulestore(old_location)
+ module = None
+ try:
+ module = store.get_item(old_location)
+ except ItemNotFoundError:
+ # new module at this location: almost always used for the course about pages; thus, no parent. (there
+ # are quite a handful of about page types available for a course and only the overview is pre-created)
+ store.create_and_save_xmodule(old_location)
+ module = store.get_item(old_location)
+
+ if post_data.get('data') is not None:
+ data = post_data['data']
+ store.update_item(old_location, data)
+ else:
+ data = module.get_explicitly_set_fields_by_scope(Scope.content)
+
+ if post_data.get('children') is not None:
+ children = post_data['children']
+ store.update_children(old_location, children)
+
+ # cdodge: also commit any metadata which might have been passed along in the
+ # POST from the client, if it is there
+ # NOTE, that the postback is not the complete metadata, as there's system metadata which is
+ # not presented to the end-user for editing. So let's fetch the original and
+ # 'apply' the submitted metadata, so we don't end up deleting system metadata
+ if post_data.get('metadata') is not None:
+ posted_metadata = post_data['metadata']
+
+ # update existing metadata with submitted metadata (which can be partial)
+ # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
+ for metadata_key, value in posted_metadata.items():
+ field = module.fields[metadata_key]
+
+ if value is None:
+ # remove both from passed in collection as well as the collection read in from the modulestore
+ field.delete_from(module)
+ else:
+ try:
+ value = field.from_json(value)
+ except ValueError:
+ return JsonResponse({"error": "Invalid data"}, 400)
+ field.write_to(module, value)
+
+ # commit to datastore
+ metadata = module.get_explicitly_set_fields_by_scope(Scope.settings)
+ store.update_metadata(old_location, metadata)
+ else:
+ metadata = module.get_explicitly_set_fields_by_scope(Scope.settings)
+
+ return {
+ 'id': unicode(usage_loc),
+ 'data': data,
+ 'metadata': metadata
+ }
diff --git a/cms/static/coffee/spec/views/course_info_spec.coffee b/cms/static/coffee/spec/views/course_info_spec.coffee
index 49ad3b23e7..3c388fa593 100644
--- a/cms/static/coffee/spec/views/course_info_spec.coffee
+++ b/cms/static/coffee/spec/views/course_info_spec.coffee
@@ -34,6 +34,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
@xhrRestore = courseUpdatesXhr.restore
@collection = new CourseUpdateCollection()
+ @collection.url = 'course_info_update/'
@courseInfoEdit = new CourseInfoUpdateView({
el: $('.course-updates'),
collection: @collection,
diff --git a/cms/static/js/collections/course_update.js b/cms/static/js/collections/course_update.js
index 41bc3875aa..e78ef2d3fa 100644
--- a/cms/static/js/collections/course_update.js
+++ b/cms/static/js/collections/course_update.js
@@ -4,7 +4,7 @@ define(["backbone", "js/models/course_update"], function(Backbone, CourseUpdateM
collection of updates as [{ date : "month day", content : "html"}]
*/
var CourseUpdateCollection = Backbone.Collection.extend({
- url : function() {return this.urlbase + "course_info/updates/";},
+ // instantiator must set url
model : CourseUpdateModel
});
diff --git a/cms/static/js/models/module_info.js b/cms/static/js/models/module_info.js
index 5a9b782150..47a9ef1158 100644
--- a/cms/static/js/models/module_info.js
+++ b/cms/static/js/models/module_info.js
@@ -1,6 +1,6 @@
define(["backbone"], function(Backbone) {
var ModuleInfo = Backbone.Model.extend({
- url: function() {return "/module_info/" + this.id;},
+ urlRoot: "/xblock",
defaults: {
"id": null,
diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html
index 7be7a3a4a1..02a4953ef1 100644
--- a/cms/templates/course_info.html
+++ b/cms/templates/course_info.html
@@ -1,4 +1,6 @@
-<%! from django.utils.translation import ugettext as _ %>
+<%!
+ from django.utils.translation import ugettext as _
+%>
<%inherit file="base.html" />
<%namespace name='static' file='static_content.html'/>
@@ -17,16 +19,16 @@
<%block name="jsextra">