Separate unit page from vertical implementation for editing
This commit is contained in:
@@ -43,6 +43,9 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
|
||||
# ==== Public views ==================================================
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -142,11 +145,49 @@ def edit_unit(request, location):
|
||||
else:
|
||||
lms_link = None
|
||||
|
||||
<<<<<<< HEAD
|
||||
|
||||
return render_to_response('unit.html', {
|
||||
'module': item,
|
||||
'editable_preview': get_module_previews(request, item)[0],
|
||||
})
|
||||
=======
|
||||
component_templates = defaultdict(list)
|
||||
>>>>>>> Separate unit page from vertical implementation for editing
|
||||
|
||||
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
|
||||
for template in templates:
|
||||
if template.location.category in COMPONENT_TYPES:
|
||||
component_templates[template.location.category].append((
|
||||
template.display_name,
|
||||
template.location.url(),
|
||||
))
|
||||
|
||||
components = [
|
||||
component.location.url()
|
||||
for component
|
||||
in item.get_children()
|
||||
]
|
||||
|
||||
return render_to_response('unit.html', {
|
||||
'unit_name': item.display_name,
|
||||
'components': components,
|
||||
'component_templates': component_templates,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def preview_component(request, location):
|
||||
# TODO (vshnayder): change name from id to location in coffee+html as well.
|
||||
if not has_access(request.user, location):
|
||||
raise Http404 # TODO (vshnayder): better error
|
||||
|
||||
component = modulestore().get_item(location)
|
||||
|
||||
return render_to_response('component.html', {
|
||||
'preview': get_module_previews(request, component)[0],
|
||||
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -154,33 +195,6 @@ def delete_unit(request, location):
|
||||
pass
|
||||
|
||||
|
||||
@login_required
|
||||
def new_item(request):
|
||||
"""
|
||||
Display a page where the user can create a new item from a template
|
||||
|
||||
Expects a GET request with the parameter 'parent_location', which is the element to add
|
||||
the newly created item to as a child.
|
||||
|
||||
parent_location: A Location URL
|
||||
"""
|
||||
|
||||
parent_location = request.GET['parent_location']
|
||||
if not has_access(request.user, parent_location):
|
||||
raise Http404
|
||||
|
||||
parent = modulestore().get_item(parent_location)
|
||||
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
|
||||
|
||||
templates.sort(key=attrgetter('location.category', 'display_name'))
|
||||
|
||||
return render_to_response('new_item.html', {
|
||||
'parent_name': parent.display_name,
|
||||
'parent_location': parent.location.url(),
|
||||
'templates': groupby(templates, attrgetter('location.category')),
|
||||
})
|
||||
|
||||
|
||||
def user_author_string(user):
|
||||
'''Get an author string for commits by this user. Format:
|
||||
first last <email@email.com>.
|
||||
@@ -321,20 +335,10 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
|
||||
error_msg=exc_info_to_str(sys.exc_info())
|
||||
).xmodule_constructor(system)(None, None)
|
||||
|
||||
|
||||
module.get_html = wrap_xmodule(
|
||||
module.get_html,
|
||||
module,
|
||||
"xmodule_edit.html",
|
||||
{
|
||||
'location': descriptor.location.url(),
|
||||
'editor_content': descriptor.get_html(),
|
||||
'editor_type': descriptor.js_module_name,
|
||||
'editor_class': descriptor.__class__.__name__,
|
||||
# TODO (cpennington): Make descriptors know if they have data that can be editng
|
||||
'editable_data': descriptor.definition.get('data'),
|
||||
'editable_class': 'editable' if descriptor.definition.get('data') else '',
|
||||
}
|
||||
"xmodule_display.html",
|
||||
)
|
||||
module.get_html = replace_static_urls(
|
||||
module.get_html,
|
||||
|
||||
@@ -4,9 +4,3 @@ class CMS.Models.Module extends Backbone.Model
|
||||
data: ''
|
||||
children: ''
|
||||
metadata: {}
|
||||
|
||||
initialize: (attributes) ->
|
||||
@module = attributes.module
|
||||
@unset('module')
|
||||
delete attributes.module
|
||||
super(attributes)
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
class CMS.Views.ModuleEdit extends Backbone.View
|
||||
tagName: 'div'
|
||||
className: 'xmodule_edit'
|
||||
tagName: 'li'
|
||||
className: 'component'
|
||||
|
||||
events:
|
||||
"click .component-editor .cancel-button": 'clickCancelButton'
|
||||
"click .component-editor .save-button": 'clickSaveButton'
|
||||
"click .component-actions .edit-button": 'clickEditButton'
|
||||
|
||||
|
||||
initialize: ->
|
||||
@module = @options.module
|
||||
@module.onUpdate(@save)
|
||||
@render()
|
||||
|
||||
@setEvents()
|
||||
$component_editor: => @$el.find('.component-editor')
|
||||
|
||||
$component_editor: -> @$el.find('.component-editor')
|
||||
|
||||
setEvents: ->
|
||||
id = @$el.data('id')
|
||||
|
||||
@events = {}
|
||||
@events["click .component-editor[data-id=#{ id }] .cancel-button"] = 'clickCancelButton'
|
||||
@events["click .component-editor[data-id=#{ id }] .save-button"] = 'clickSaveButton'
|
||||
@events["click .component-actions[data-id=#{ id }] .edit-button"] = 'clickEditButton'
|
||||
|
||||
@delegateEvents()
|
||||
loadModules: ->
|
||||
@module = XModule.loadModule(@$el.find('.xmodule_edit'))
|
||||
XModule.loadModule(@$el.find('.xmodule_display'))
|
||||
|
||||
metadata: ->
|
||||
# cdodge: package up metadata which is separated into a number of input fields
|
||||
@@ -32,30 +30,26 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
# build up a object to pass back to the server on the subsequent POST
|
||||
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', $metadata)
|
||||
|
||||
_metadata
|
||||
return _metadata
|
||||
|
||||
save: (data) =>
|
||||
@model.unset('preview')
|
||||
@model.set(data)
|
||||
@model.save().done( (resp) =>
|
||||
alert("Your changes have been saved.")
|
||||
|
||||
$preview = $(resp.preview)
|
||||
@$el.replaceWith($preview)
|
||||
@setElement($preview)
|
||||
@module.constructor(@$el)
|
||||
XModule.loadModules(@$el)
|
||||
|
||||
).fail( ->
|
||||
alert("There was an error saving your changes. Please try again.")
|
||||
render: ->
|
||||
@$el.load("/preview_component/#{@model.id}", =>
|
||||
@loadModules()
|
||||
@delegateEvents()
|
||||
)
|
||||
|
||||
clickSaveButton: (event) =>
|
||||
event.preventDefault()
|
||||
data = @module.save()
|
||||
data.metadata = @metadata()
|
||||
@model.save(data).done( =>
|
||||
alert("Your changes have been saved.")
|
||||
|
||||
@save(data)
|
||||
@render()
|
||||
@$el.removeClass('editing')
|
||||
).fail( ->
|
||||
alert("There was an error saving your changes. Please try again.")
|
||||
)
|
||||
|
||||
clickCancelButton: (event) ->
|
||||
event.preventDefault()
|
||||
|
||||
@@ -10,32 +10,23 @@ $(document).ready(function() {
|
||||
$modal = $('.history-modal');
|
||||
$modalCover = $('.modal-cover');
|
||||
$newComponentItem = $('.new-component-item');
|
||||
$newComponentStep1 = $('.new-component-step-1');
|
||||
$newComponentStep2 = $('.new-component-step-2');
|
||||
$newComponentChooser = $('.new-component');
|
||||
$newComponentButton = $('.new-component-button');
|
||||
|
||||
$(document).bind('XModule.loaded.edit', function(e, element, module) {
|
||||
var previewType = $(element).data('preview-type');
|
||||
var moduleType = $(element).data('type');
|
||||
$('li.component').each(function(idx, element) {
|
||||
new CMS.Views.ModuleEdit({
|
||||
el: element,
|
||||
module: module,
|
||||
model: new CMS.Models.Module({
|
||||
id: $(element).data('id'),
|
||||
type: moduleType == 'None' ? null : moduleType,
|
||||
previewType: previewType == 'None' ? null : previewType,
|
||||
})
|
||||
});
|
||||
});
|
||||
XModule.loadModules()
|
||||
|
||||
$('.expand-collapse-icon').bind('click', toggleSubmodules);
|
||||
$('.visibility-options').bind('change', setVisibility);
|
||||
|
||||
$newComponentButton.bind('click', showNewComponentForm);
|
||||
$newComponentStep1.find('.new-component-type a').bind('click', showNewComponentProperties);
|
||||
$newComponentStep2.find('.save-button').bind('click', saveNewComponent);
|
||||
$newComponentStep2.find('.cancel-button').bind('click', cancelNewComponent);
|
||||
$newComponentChooser.find('.new-component-type a').bind('click', showComponentTemplates);
|
||||
|
||||
$('.unit-history ol a').bind('click', showHistoryModal);
|
||||
$modal.bind('click', hideHistoryModal);
|
||||
@@ -64,70 +55,18 @@ function closeComponentEditor(e) {
|
||||
}
|
||||
|
||||
function showNewComponentForm(e) {
|
||||
e.preventDefault();
|
||||
e.preventDefault();
|
||||
$newComponentItem.addClass('adding');
|
||||
$(this).slideUp(150);
|
||||
$newComponentStep1.slideDown(150);
|
||||
$newComponentChooser.slideDown(150);
|
||||
}
|
||||
|
||||
function showNewComponentProperties(e) {
|
||||
function showComponentTemplates(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var displayName;
|
||||
var componentSource;
|
||||
var selectionRange;
|
||||
var $renderedComponent;
|
||||
|
||||
switch($(this).attr('data-type')) {
|
||||
case 'video':
|
||||
displayName = 'Video';
|
||||
componentSource = '<video youtube="1.50:___,1.25:___,1.0:___,0.75:___"/>';
|
||||
selectionRange = [21, 24];
|
||||
$renderedComponent = $('<div class="rendered-component"><div class="video-unit"><img src="images/video-module.png"></div></div>');
|
||||
break;
|
||||
case 'textbook':
|
||||
displayName = 'Textbook';
|
||||
componentSource = '<customtag page="___"><impl>book</impl></customtag>';
|
||||
selectionRange = [17, 20];
|
||||
$renderedComponent = $('<div class="rendered-component"><p><span class="textbook-icon"></span>More information given in the text.</p></div>');
|
||||
break;
|
||||
case 'slide':
|
||||
displayName = 'Slide';
|
||||
componentSource = '<customtag page="___"><customtag lecnum="___"><impl>slides</impl></customtag>';
|
||||
selectionRange = [17, 20];
|
||||
$renderedComponent = $('<div class="rendered-component"><p><span class="slides-icon"></span>Lecture Slides Handout [Clean] [Annotated]</p></div>');
|
||||
break;
|
||||
case 'discussion':
|
||||
displayName = 'Discussion';
|
||||
componentSource = '<discussion for="___" id="___" discussion_category="___"/>';
|
||||
selectionRange = [17, 20];
|
||||
$renderedComponent = $('<div class="rendered-component"><div class="discussion-unit"><img src="images/discussion-module.png"></div></div>');
|
||||
break;
|
||||
case 'problem':
|
||||
displayName = 'Problem';
|
||||
componentSource = '<problem>___</problem>';
|
||||
selectionRange = [9, 12];
|
||||
$renderedComponent = $('<div class="rendered-component"></div>');
|
||||
break;
|
||||
case 'freeform':
|
||||
displayName = 'Freeform HTML';
|
||||
componentSource = '';
|
||||
selectionRange = [0, 0];
|
||||
$renderedComponent = $('<div class="rendered-component"></div>');
|
||||
break;
|
||||
}
|
||||
|
||||
$newComponentItem.prepend($renderedComponent);
|
||||
$renderedComponent.slideDown(250);
|
||||
|
||||
$newComponentStep2.find('h5').html('Edit ' + displayName + ' Component');
|
||||
$newComponentStep2.find('textarea').html(componentSource);
|
||||
setTimeout(function() {
|
||||
$newComponentStep2.find('textarea').focus().get(0).setSelectionRange(selectionRange[0], selectionRange[1]);
|
||||
}, 10);
|
||||
|
||||
$newComponentStep1.slideUp(250);
|
||||
$newComponentStep2.slideDown(250);
|
||||
var type = $(this).data('type');
|
||||
$newComponentChooser.slideUp(250);
|
||||
$('.new-component-'+type).slideDown(250);
|
||||
}
|
||||
|
||||
function cancelNewComponent(e) {
|
||||
|
||||
@@ -268,7 +268,7 @@ input.courseware-unit-search-input {
|
||||
header {
|
||||
height: 67px;
|
||||
|
||||
.item-actions {
|
||||
.item-actions {
|
||||
margin-top: 11px;
|
||||
margin-right: 12px;
|
||||
|
||||
@@ -359,7 +359,7 @@ input.courseware-unit-search-input {
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
.component-actions {
|
||||
float: right;
|
||||
|
||||
.edit-button,
|
||||
@@ -499,8 +499,8 @@ input.courseware-unit-search-input {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.edit-pane {
|
||||
.xmodule_edit.editable {
|
||||
.components {
|
||||
.component {
|
||||
position: relative;
|
||||
border: 1px solid transparent;
|
||||
z-index: 10;
|
||||
@@ -577,82 +577,76 @@ input.courseware-unit-search-input {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity .15s;
|
||||
}
|
||||
}
|
||||
.new-component-item {
|
||||
padding: 0;
|
||||
border: 1px solid #8891a1;
|
||||
border-radius: 3px;
|
||||
background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)) #d1dae3;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset;
|
||||
-webkit-transition: background-color .15s, border-color .15s;
|
||||
|
||||
&.new-component-item {
|
||||
padding: 0;
|
||||
border: 1px solid #8891a1;
|
||||
border-radius: 3px;
|
||||
background: -webkit-linear-gradient(top, rgba(255, 255, 255, .3), rgba(255, 255, 255, 0)) #d1dae3;
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset;
|
||||
-webkit-transition: background-color .15s, border-color .15s;
|
||||
&.adding {
|
||||
background-color: $blue;
|
||||
border-color: #437fbf;
|
||||
}
|
||||
|
||||
&.adding {
|
||||
background-color: $blue;
|
||||
border-color: #437fbf;
|
||||
}
|
||||
.new-component-button {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #6d788b;
|
||||
}
|
||||
|
||||
.new-component-button {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #6d788b;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
.rendered-component {
|
||||
display: none;
|
||||
background: #fff;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.new-component-type {
|
||||
@include clearfix;
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 14px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, .4), 0 1px 0 rgba(255, 255, 255, .4) inset;
|
||||
-webkit-transition: background-color .15s;
|
||||
|
||||
.rendered-component {
|
||||
display: none;
|
||||
background: #fff;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, .2);
|
||||
}
|
||||
|
||||
.new-component-type {
|
||||
@include clearfix;
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 14px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, .4), 0 1px 0 rgba(255, 255, 255, .4) inset;
|
||||
-webkit-transition: background-color .15s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, .2);
|
||||
}
|
||||
|
||||
.name {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.name {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.new-component-step-1,
|
||||
.new-component-step-2 {
|
||||
display: none;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-unit img,
|
||||
.discussion-unit img {
|
||||
width: 100%;
|
||||
.new-component,
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
cms/templates/component.html
Normal file
12
cms/templates/component.html
Normal file
@@ -0,0 +1,12 @@
|
||||
${preview}
|
||||
<div class="component-actions">
|
||||
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
|
||||
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
|
||||
</div>
|
||||
<a href="#" class="drag-handle"></a>
|
||||
<div class="component-editor">
|
||||
${editor}
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
|
||||
@@ -9,19 +9,52 @@
|
||||
<ul>
|
||||
<li><a href="#">Week 2</a></li>
|
||||
<li><a href="#">Linearity and Superposition</a></li>
|
||||
<li><span class="current-page">S3V2: Properties of Linearity</span></li>
|
||||
<li><span class="current-page">${unit_name}</span></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section class='edit-pane'>
|
||||
${editable_preview}
|
||||
</section>
|
||||
<ol class="components">
|
||||
% for id in components:
|
||||
<li class="component" data-id="${id}"/>
|
||||
% endfor
|
||||
<li class="new-component-item">
|
||||
<a href="#" class="new-component-button">
|
||||
<span class="plus-icon"></span>New Component
|
||||
</a>
|
||||
<div class="new-component">
|
||||
<h5>Select Component Type</h5>
|
||||
<ul class="new-component-type">
|
||||
% for type in sorted(component_templates.keys()):
|
||||
<li>
|
||||
<a href="#" data-type="${type}">
|
||||
<span class="large-template-icon large-${type}-icon"></span>
|
||||
<span class="name">${type}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
% for type, templates in sorted(component_templates.items()):
|
||||
<div class="new-component-templates new-component-${type}">
|
||||
<ul class="new-component-template">
|
||||
% for name, location in templates:
|
||||
<li>
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name">${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
% endfor
|
||||
</li>
|
||||
</ol>
|
||||
</article>
|
||||
|
||||
<div class="sidebar">
|
||||
<div class="unit-properties window">
|
||||
<h4>Properties</h4>
|
||||
<div class="window-contents">
|
||||
<div class="row"><label>Display Name:</label><input type="text" value="${module.display_name}" /></div>
|
||||
<div class="row"><label>Display Name:</label><input type="text" value="${unit_name}" /></div>
|
||||
<div class="row">
|
||||
<label>State:</label>
|
||||
<div class="visibility-options">
|
||||
|
||||
@@ -9,9 +9,9 @@ import django.contrib.auth.views
|
||||
|
||||
urlpatterns = ('',
|
||||
url(r'^$', 'contentstore.views.index', name='index'),
|
||||
url(r'^new_item$', 'contentstore.views.new_item', name='new_item'),
|
||||
url(r'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'),
|
||||
url(r'^delete/(?P<location>.*?)$', 'contentstore.views.delete_unit', name='delete_unit'),
|
||||
url(r'^preview_component/(?P<location>.*?)$', 'contentstore.views.preview_component', name='preview_component'),
|
||||
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
|
||||
url(r'^clone_item$', 'contentstore.views.clone_item', name='clone_item'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
if $(element).hasClass('xmodule_display')
|
||||
$(document).trigger('XModule.loaded.display', [element, module])
|
||||
|
||||
return module
|
||||
|
||||
catch error
|
||||
console.error "Unable to load #{moduleType}: #{error.message}" if console
|
||||
|
||||
@@ -33,7 +35,7 @@
|
||||
else
|
||||
modules = $(selector)
|
||||
|
||||
modules.each (idx, element) -> XModule.loadModule element
|
||||
modules.each((idx, element) -> XModule.loadModule element)
|
||||
|
||||
class @XModule.Descriptor
|
||||
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
<div class="xmodule_edit xmodule_${editor_class} ${editable_class}" data-type="${editor_type}" data-id="${location}">
|
||||
<%include file="xmodule_display.html"/>
|
||||
% if editable_data:
|
||||
<div class="component-actions" data-id="${location}">
|
||||
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
|
||||
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
|
||||
</div>
|
||||
<div class="component-editor" data-id="${location}">
|
||||
${editor_content}
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
<section class="xmodule_edit xmodule_${class_}" data-type="${module_name}">
|
||||
${content}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user