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
  1. , 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">