Merge pull request #1108 from MITx/feature/cdodge/course-info

Feature/cdodge/course info
This commit is contained in:
Don Mitchell
2012-12-07 12:51:09 -08:00
10 changed files with 204 additions and 38 deletions

View 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)

View File

@@ -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):

View File

@@ -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>

View File

@@ -29,6 +29,8 @@ CMS.Models.CourseUpdateCollection = Backbone.Collection.extend({
model : CMS.Models.CourseUpdate
});

View 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
},
});

View File

@@ -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]) {

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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();
});

View File

@@ -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'),