Merge pull request #1108 from MITx/feature/cdodge/course-info
Feature/cdodge/course info
This commit is contained in:
83
cms/djangoapps/contentstore/module_info_model.py
Normal file
83
cms/djangoapps/contentstore/module_info_model.py
Normal file
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
<a href="#" class="edit-button"><span class="edit-icon"></span>Edit</a>
|
||||
<div class="handouts-content">
|
||||
<h2>Course Handouts</h2>
|
||||
<ol class="treeview-handoutsnav">
|
||||
<li>
|
||||
<a href="/static/content-mit-6002x/handouts/syllabus.a477535058a1.pdf">Syllabus</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/static/content-mit-6002x/handouts/at-a-glance.9674fe7f677e.pdf">6.002x At-A-Glance</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/static/content-mit-6002x/handouts/syllabus.a477535058a1.pdf">Syllabus</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/static/content-mit-6002x/handouts/at-a-glance.9674fe7f677e.pdf">6.002x At-A-Glance</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/static/content-mit-6002x/handouts/syllabus.a477535058a1.pdf">Syllabus</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h2>Course Handouts</h2>
|
||||
<%if (model.get('data') != null) { %>
|
||||
<div class="handouts-content">
|
||||
<%= model.get('data') %>
|
||||
</div>
|
||||
<% } else {%>
|
||||
<p>You have no handouts defined</p>
|
||||
<% } %>
|
||||
<form class="edit-handouts-form" style="display: block;">
|
||||
<div class="row">
|
||||
<textarea class="handouts-content-editor text-editor"></textarea>
|
||||
|
||||
@@ -29,6 +29,8 @@ CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
|
||||
|
||||
model : CMS.Models.CourseUpdate
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
10
cms/static/js/models/module_info.js
Normal file
10
cms/static/js/models/module_info.js
Normal file
@@ -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
|
||||
},
|
||||
});
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,16 @@ 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();
|
||||
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);
|
||||
$newForm.addClass('editing');
|
||||
@@ -85,7 +95,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 +112,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() {
|
||||
@@ -185,11 +205,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 +223,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');
|
||||
@@ -207,9 +238,16 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
|
||||
},
|
||||
|
||||
onEdit: function(event) {
|
||||
var self = this;
|
||||
this.$editor.val(this.$preview.html());
|
||||
this.$form.show();
|
||||
this.$preview.hide();
|
||||
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);
|
||||
@@ -217,6 +255,9 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
|
||||
},
|
||||
|
||||
onSave: function(event) {
|
||||
this.model.set('data', this.$codeMirror.getValue());
|
||||
this.render();
|
||||
this.model.save();
|
||||
this.$form.hide();
|
||||
this.closeEditor(this);
|
||||
},
|
||||
@@ -227,8 +268,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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<%block name="jsextra">
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_info.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/module_info.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/course_info_edit.js')}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -44,6 +44,10 @@ urlpatterns = ('',
|
||||
url(r'^edit_tabs/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_tabs', name='edit_tabs'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/assets/(?P<name>[^/]+)$', '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<module_location>.*)$', 'contentstore.views.module_info', name='module_info'),
|
||||
|
||||
|
||||
# temporary landing page for a course
|
||||
url(r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing'),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user