diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py new file mode 100644 index 0000000000..cd07e4556d --- /dev/null +++ b/cms/djangoapps/contentstore/module_info_model.py @@ -0,0 +1,83 @@ +import logging + +from xmodule.modulestore.exceptions import ItemNotFoundError +from xmodule.modulestore import Location +from xmodule.modulestore.django import modulestore +from lxml import etree +import re +from django.http import HttpResponseBadRequest, Http404 + +def get_module_info(store, location, parent_location = None): + try: + if location.revision is None: + module = store.get_item(location) + else: + module = store.get_item(location) + except ItemNotFoundError: + raise Http404 + + return { + 'id': module.location.url(), + 'data': module.definition['data'], + 'metadata': module.metadata + } + +def set_module_info(store, location, post_data): + module = None + isNew = False + try: + if location.revision is None: + module = store.get_item(location) + else: + module = store.get_item(location) + except: + pass + + if module is None: + # new module at this location + # presume that we have an 'Empty' template + template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) + module = store.clone_item(template_location, location) + isNew = True + + logging.debug('post = {0}'.format(post_data)) + + if post_data.get('data') is not None: + data = post_data['data'] + logging.debug('data = {0}'.format(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 in posted_metadata.keys(): + + # let's strip out any metadata fields from the postback which have been identified as system metadata + # and therefore should not be user-editable, so we should accept them back from the client + if metadata_key in module.system_metadata_fields: + del posted_metadata[metadata_key] + elif posted_metadata[metadata_key] is None: + # remove both from passed in collection as well as the collection read in from the modulestore + if metadata_key in module.metadata: + del module.metadata[metadata_key] + del posted_metadata[metadata_key] + + # overlay the new metadata over the modulestore sourced collection to support partial updates + module.metadata.update(posted_metadata) + + # commit to datastore + store.update_metadata(location, module.metadata) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index a008ac96a7..780a92035c 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -49,6 +49,7 @@ from contentstore.course_info_model import get_course_updates,\ update_course_updates, delete_course_update from cache_toolbox.core import del_cached_content from xmodule.timeparse import stringify_time +from contentstore.module_info_model import get_module_info, set_module_info log = logging.getLogger(__name__) @@ -927,7 +928,8 @@ def course_info(request, org, course, name, provided_id=None): 'active_tab': 'courseinfo-tab', 'context_course': course_module, 'url_base' : "/" + org + "/" + course + "/", - 'course_updates' : json.dumps(get_course_updates(location)) + 'course_updates' : json.dumps(get_course_updates(location)), + 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url() }) @expect_json @@ -959,6 +961,31 @@ def course_info_updates(request, org, course, provided_id=None): elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE return HttpResponse(json.dumps(delete_course_update(location, request.POST, provided_id)), mimetype="application/json") + +@expect_json +@login_required +@ensure_csrf_cookie +def module_info(request, module_location): + location = Location(module_location) + + # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!! + if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META: + real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE'] + else: + real_method = request.method + + # check that logged in user has permissions to this item + if not has_access(request.user, location): + raise PermissionDenied() + + if real_method == 'GET': + return HttpResponse(json.dumps(get_module_info(_modulestore(location), location)), mimetype="application/json") + elif real_method == 'POST' or real_method == 'PUT': + return HttpResponse(json.dumps(set_module_info(_modulestore(location), location, request.POST)), mimetype="application/json") + else: + raise Http400 + + @login_required @ensure_csrf_cookie def asset_index(request, org, course, name): diff --git a/cms/static/coffee/src/client_templates/course_info_handouts.html b/cms/static/coffee/src/client_templates/course_info_handouts.html index f4eccf6857..958a1c77d6 100644 --- a/cms/static/coffee/src/client_templates/course_info_handouts.html +++ b/cms/static/coffee/src/client_templates/course_info_handouts.html @@ -1,24 +1,13 @@ Edit -
You have no handouts defined
+<% } %>