From 130de49b4967515157455a8d4f71492d824d9eec Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 7 Dec 2012 09:45:12 -0500 Subject: [PATCH 1/3] support the editing of course handouts via a generic editor until we have implemented the Rich HTML editor completed --- .../contentstore/module_info_model.py | 83 +++++++++++++++++++ cms/djangoapps/contentstore/views.py | 29 ++++++- .../course_info_handouts.html | 29 ++----- cms/static/js/models/course_info.js | 2 + cms/static/js/models/module_info.js | 10 +++ cms/static/js/template_loader.js | 2 +- cms/static/js/views/course_info_edit.js | 33 +++++--- cms/templates/course_info.html | 10 ++- cms/urls.py | 4 + 9 files changed, 167 insertions(+), 35 deletions(-) create mode 100644 cms/djangoapps/contentstore/module_info_model.py create mode 100644 cms/static/js/models/module_info.js 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 -
-

Course Handouts

-
    -
  1. - Syllabus -
  2. -
  3. - 6.002x At-A-Glance -
  4. -
  5. - Syllabus -
  6. -
  7. - 6.002x At-A-Glance -
  8. -
  9. - Syllabus -
  10. -
-
+ +

Course Handouts

+<%if (model.get('data') != null) { %> +
+ <%= model.get('data') %> +
+<% } else {%> +

You have no handouts defined

+<% } %>
diff --git a/cms/static/js/models/course_info.js b/cms/static/js/models/course_info.js index e776d65107..8cb5a654cb 100644 --- a/cms/static/js/models/course_info.js +++ b/cms/static/js/models/course_info.js @@ -29,6 +29,8 @@ CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({ model : CMS.Models.CourseUpdate }); + + \ No newline at end of file diff --git a/cms/static/js/models/module_info.js b/cms/static/js/models/module_info.js new file mode 100644 index 0000000000..6a593372c4 --- /dev/null +++ b/cms/static/js/models/module_info.js @@ -0,0 +1,10 @@ +CMS.Models.ModuleInfo = Backbone.Model.extend({ + url: function() {return "/module_info/" + this.id;}, + + defaults: { + "id": null, + "data": null, + "metadata" : null, + "children" : null + }, +}); \ No newline at end of file diff --git a/cms/static/js/template_loader.js b/cms/static/js/template_loader.js index bd8d249e6b..a18ddf3dfe 100644 --- a/cms/static/js/template_loader.js +++ b/cms/static/js/template_loader.js @@ -5,7 +5,7 @@ if (typeof window.templateLoader == 'function') return; var templateLoader = { - templateVersion: "0.0.4", + templateVersion: "0.0.6", templates: {}, loadRemoteTemplate: function(templateName, filename, callback) { if (!this.templates[templateName]) { diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js index 86fcc0a646..c1b08b0142 100644 --- a/cms/static/js/views/course_info_edit.js +++ b/cms/static/js/views/course_info_edit.js @@ -15,8 +15,8 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({ }); new CMS.Views.ClassInfoHandoutsView({ - el: this.$('#course-handouts-view') - // collection: this.model.get('') + el: this.$('#course-handouts-view'), + model: this.model.get('handouts') }); return this; } @@ -185,11 +185,17 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ initialize: function() { var self = this; - window.templateLoader.loadRemoteTemplate("course_info_handouts", - "/static/coffee/src/client_templates/course_info_handouts.html", - function (raw_template) { - self.template = _.template(raw_template); - self.render(); + this.model.fetch( + { + complete: function() { + window.templateLoader.loadRemoteTemplate("course_info_handouts", + "/static/coffee/src/client_templates/course_info_handouts.html", + function (raw_template) { + self.template = _.template(raw_template); + self.render(); + } + ); + } } ); }, @@ -197,7 +203,12 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ render: function () { var updateEle = this.$el; var self = this; - this.$el.append($(this.template())); + this.$el.html( + $(this.template( { + model: this.model + }) + ) + ); this.$preview = this.$el.find('.handouts-content'); this.$form = this.$el.find(".edit-handouts-form"); this.$editor = this.$form.find('.handouts-content-editor'); @@ -209,7 +220,6 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ onEdit: function(event) { this.$editor.val(this.$preview.html()); this.$form.show(); - this.$preview.hide(); $modalCover.show(); $modalCover.bind('click', function() { self.closeEditor(self); @@ -217,6 +227,9 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ }, onSave: function(event) { + this.model.set('data', this.$editor.val()); + this.render(); + this.model.save(); this.$form.hide(); this.closeEditor(this); }, @@ -227,8 +240,6 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ }, closeEditor: function(self) { - this.$preview.html(this.$editor.val()); - this.$preview.show(); this.$form.hide(); $modalCover.unbind('click'); $modalCover.hide(); diff --git a/cms/templates/course_info.html b/cms/templates/course_info.html index e045814d72..f4fa661b6e 100644 --- a/cms/templates/course_info.html +++ b/cms/templates/course_info.html @@ -8,6 +8,7 @@ <%block name="jsextra"> + @@ -19,14 +20,19 @@ var course_updates = new CMS.Models.CourseUpdateCollection(); course_updates.reset(${course_updates|n}); course_updates.urlbase = '${url_base}'; + + var course_handouts = new CMS.Models.ModuleInfo({ + id: '${handouts_location}' + }); + course_handouts.urlbase = '${url_base}'; var editor = new CMS.Views.CourseInfoEdit({ el: $('.main-wrapper'), model : new CMS.Models.CourseInfo({ courseId : '${context_course.location}', updates : course_updates, - // FIXME add handouts - handouts : null}) + handouts : course_handouts + }) }); editor.render(); }); diff --git a/cms/urls.py b/cms/urls.py index 4c2f2b361e..5df3215d12 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -44,6 +44,10 @@ urlpatterns = ('', url(r'^edit_tabs/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.edit_tabs', name='edit_tabs'), url(r'^(?P[^/]+)/(?P[^/]+)/assets/(?P[^/]+)$', 'contentstore.views.asset_index', name='asset_index'), + # this is a generic method to return the data/metadata associated with a xmodule + url(r'^module_info/(?P.*)$', 'contentstore.views.module_info', name='module_info'), + + # temporary landing page for a course url(r'^edge/(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.landing', name='landing'), From b81f802b1da25f47333ae82b538a123664dd4afe Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 7 Dec 2012 14:38:03 -0500 Subject: [PATCH 2/3] get CodeMirror to work - set the right background in the .scss, make sure the textarea is visible before binding it to CodeMirror --- cms/static/js/views/course_info_edit.js | 34 ++++++++++++++++++++++--- cms/static/sass/_course-info.scss | 6 +++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js index c1b08b0142..19ed03fff9 100644 --- a/cms/static/js/views/course_info_edit.js +++ b/cms/static/js/views/course_info_edit.js @@ -41,7 +41,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ "/static/coffee/src/client_templates/course_info_update.html", function (raw_template) { self.template = _.template(raw_template); - self.render(); + self.render(); } ); }, @@ -68,6 +68,14 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ this.collection.add(newModel, {at : 0}); var $newForm = $(this.template({ updateModel : newModel })); + + var $textArea = $newForm.find(".new-update-content").first(); + this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), { + mode: "text/html", + lineNumbers: true, + lineWrapping: true, + }); + var updateEle = this.$el.find("#course-update-list"); $(updateEle).prepend($newForm); $newForm.addClass('editing'); @@ -85,7 +93,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ onSave: function(event) { var targetModel = this.eventModel(event); console.log(this.contentEntry(event).val()); - targetModel.set({ date : this.dateEntry(event).val(), content : this.contentEntry(event).val() }); + targetModel.set({ date : this.dateEntry(event).val(), content : this.$codeMirror.getValue() }); // push change to display, hide the editor, submit the change this.closeEditor(this); targetModel.save(); @@ -102,7 +110,17 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ var self = this; this.$currentPost = $(event.target).closest('li'); this.$currentPost.addClass('editing'); - $(this.editor(event)).slideDown(150); + + $(this.editor(event)).show(); + var $textArea = this.$currentPost.find(".new-update-content").first(); + if (this.$codeMirror == null ) { + this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), { + mode: "text/html", + lineNumbers: true, + lineWrapping: true, + }); + } + $modalCover.show(); var targetModel = this.eventModel(event); $modalCover.bind('click', function() { @@ -218,8 +236,16 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ }, onEdit: function(event) { + var self = this; this.$editor.val(this.$preview.html()); this.$form.show(); + if (this.$codeMirror == null) { + this.$codeMirror = CodeMirror.fromTextArea(this.$editor.get(0), { + mode: "text/html", + lineNumbers: true, + lineWrapping: true, + }); + } $modalCover.show(); $modalCover.bind('click', function() { self.closeEditor(self); @@ -227,7 +253,7 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ }, onSave: function(event) { - this.model.set('data', this.$editor.val()); + this.model.set('data', this.$codeMirror.getValue()); this.render(); this.model.save(); this.$form.hide(); diff --git a/cms/static/sass/_course-info.scss b/cms/static/sass/_course-info.scss index 809ff6d4fc..2ec22ebfea 100644 --- a/cms/static/sass/_course-info.scss +++ b/cms/static/sass/_course-info.scss @@ -21,6 +21,12 @@ border-radius: 3px 0 0 3px; border-right-color: $mediumGrey; } + + .CodeMirror { + border: 1px solid #3c3c3c; + background: #fff; + color: #3c3c3c; + } } .course-updates { From 402e031eccf5b6e9299bb547f965c0afe31ce4bc Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Fri, 7 Dec 2012 15:27:00 -0500 Subject: [PATCH 3/3] forgot to add a if {} guard on the CodeMirror instantiation on one use case --- cms/static/js/views/course_info_edit.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js index 19ed03fff9..9f662a0697 100644 --- a/cms/static/js/views/course_info_edit.js +++ b/cms/static/js/views/course_info_edit.js @@ -70,11 +70,13 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ var $newForm = $(this.template({ updateModel : newModel })); var $textArea = $newForm.find(".new-update-content").first(); - this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), { - mode: "text/html", - lineNumbers: true, - lineWrapping: true, - }); + if (this.$codeMirror == null ) { + this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), { + mode: "text/html", + lineNumbers: true, + lineWrapping: true, + }); + } var updateEle = this.$el.find("#course-update-list"); $(updateEle).prepend($newForm);