Commit 8ca10a83 by Calen Pennington

Separate unit page from vertical implementation for editing

parent 82a28f06
...@@ -43,6 +43,9 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache ...@@ -43,6 +43,9 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
# ==== Public views ================================================== # ==== Public views ==================================================
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -142,43 +145,54 @@ def edit_unit(request, location): ...@@ -142,43 +145,54 @@ def edit_unit(request, location):
else: else:
lms_link = None lms_link = None
<<<<<<< HEAD
return render_to_response('unit.html', { return render_to_response('unit.html', {
'module': item, 'module': item,
'editable_preview': get_module_previews(request, item)[0], '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()
]
@login_required return render_to_response('unit.html', {
def delete_unit(request, location): 'unit_name': item.display_name,
pass 'components': components,
'component_templates': component_templates,
})
@login_required @login_required
def new_item(request): def preview_component(request, location):
""" # TODO (vshnayder): change name from id to location in coffee+html as well.
Display a page where the user can create a new item from a template if not has_access(request.user, location):
raise Http404 # TODO (vshnayder): better error
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'] component = modulestore().get_item(location)
if not has_access(request.user, parent_location):
raise Http404
parent = modulestore().get_item(parent_location) return render_to_response('component.html', {
templates = modulestore().get_items(Location('i4x', 'edx', 'templates')) 'preview': get_module_previews(request, component)[0],
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
})
templates.sort(key=attrgetter('location.category', 'display_name'))
return render_to_response('new_item.html', { @login_required
'parent_name': parent.display_name, def delete_unit(request, location):
'parent_location': parent.location.url(), pass
'templates': groupby(templates, attrgetter('location.category')),
})
def user_author_string(user): def user_author_string(user):
...@@ -321,20 +335,10 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_ ...@@ -321,20 +335,10 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
error_msg=exc_info_to_str(sys.exc_info()) error_msg=exc_info_to_str(sys.exc_info())
).xmodule_constructor(system)(None, None) ).xmodule_constructor(system)(None, None)
module.get_html = wrap_xmodule( module.get_html = wrap_xmodule(
module.get_html, module.get_html,
module, module,
"xmodule_edit.html", "xmodule_display.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 '',
}
) )
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
module.get_html, module.get_html,
......
...@@ -4,9 +4,3 @@ class CMS.Models.Module extends Backbone.Model ...@@ -4,9 +4,3 @@ class CMS.Models.Module extends Backbone.Model
data: '' data: ''
children: '' children: ''
metadata: {} metadata: {}
initialize: (attributes) ->
@module = attributes.module
@unset('module')
delete attributes.module
super(attributes)
class CMS.Views.ModuleEdit extends Backbone.View class CMS.Views.ModuleEdit extends Backbone.View
tagName: 'div' tagName: 'li'
className: 'xmodule_edit' className: 'component'
initialize: -> events:
@module = @options.module "click .component-editor .cancel-button": 'clickCancelButton'
@module.onUpdate(@save) "click .component-editor .save-button": 'clickSaveButton'
"click .component-actions .edit-button": 'clickEditButton'
@setEvents()
$component_editor: -> @$el.find('.component-editor') initialize: ->
@module = @options.module
setEvents: -> @render()
id = @$el.data('id')
@events = {} $component_editor: => @$el.find('.component-editor')
@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: -> metadata: ->
# cdodge: package up metadata which is separated into a number of input fields # cdodge: package up metadata which is separated into a number of input fields
...@@ -32,30 +30,26 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -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 # 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[$(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( -> render: ->
alert("There was an error saving your changes. Please try again.") @$el.load("/preview_component/#{@model.id}", =>
@loadModules()
@delegateEvents()
) )
clickSaveButton: (event) => clickSaveButton: (event) =>
event.preventDefault() event.preventDefault()
data = @module.save() data = @module.save()
data.metadata = @metadata() 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) -> clickCancelButton: (event) ->
event.preventDefault() event.preventDefault()
......
...@@ -10,32 +10,23 @@ $(document).ready(function() { ...@@ -10,32 +10,23 @@ $(document).ready(function() {
$modal = $('.history-modal'); $modal = $('.history-modal');
$modalCover = $('.modal-cover'); $modalCover = $('.modal-cover');
$newComponentItem = $('.new-component-item'); $newComponentItem = $('.new-component-item');
$newComponentStep1 = $('.new-component-step-1'); $newComponentChooser = $('.new-component');
$newComponentStep2 = $('.new-component-step-2');
$newComponentButton = $('.new-component-button'); $newComponentButton = $('.new-component-button');
$(document).bind('XModule.loaded.edit', function(e, element, module) { $('li.component').each(function(idx, element) {
var previewType = $(element).data('preview-type');
var moduleType = $(element).data('type');
new CMS.Views.ModuleEdit({ new CMS.Views.ModuleEdit({
el: element, el: element,
module: module,
model: new CMS.Models.Module({ model: new CMS.Models.Module({
id: $(element).data('id'), id: $(element).data('id'),
type: moduleType == 'None' ? null : moduleType,
previewType: previewType == 'None' ? null : previewType,
}) })
}); });
}); });
XModule.loadModules()
$('.expand-collapse-icon').bind('click', toggleSubmodules); $('.expand-collapse-icon').bind('click', toggleSubmodules);
$('.visibility-options').bind('change', setVisibility); $('.visibility-options').bind('change', setVisibility);
$newComponentButton.bind('click', showNewComponentForm); $newComponentButton.bind('click', showNewComponentForm);
$newComponentStep1.find('.new-component-type a').bind('click', showNewComponentProperties); $newComponentChooser.find('.new-component-type a').bind('click', showComponentTemplates);
$newComponentStep2.find('.save-button').bind('click', saveNewComponent);
$newComponentStep2.find('.cancel-button').bind('click', cancelNewComponent);
$('.unit-history ol a').bind('click', showHistoryModal); $('.unit-history ol a').bind('click', showHistoryModal);
$modal.bind('click', hideHistoryModal); $modal.bind('click', hideHistoryModal);
...@@ -64,70 +55,18 @@ function closeComponentEditor(e) { ...@@ -64,70 +55,18 @@ function closeComponentEditor(e) {
} }
function showNewComponentForm(e) { function showNewComponentForm(e) {
e.preventDefault(); e.preventDefault();
$newComponentItem.addClass('adding'); $newComponentItem.addClass('adding');
$(this).slideUp(150); $(this).slideUp(150);
$newComponentStep1.slideDown(150); $newComponentChooser.slideDown(150);
} }
function showNewComponentProperties(e) { function showComponentTemplates(e) {
e.preventDefault(); e.preventDefault();
var displayName; var type = $(this).data('type');
var componentSource; $newComponentChooser.slideUp(250);
var selectionRange; $('.new-component-'+type).slideDown(250);
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);
} }
function cancelNewComponent(e) { function cancelNewComponent(e) {
......
...@@ -268,7 +268,7 @@ input.courseware-unit-search-input { ...@@ -268,7 +268,7 @@ input.courseware-unit-search-input {
header { header {
height: 67px; height: 67px;
.item-actions { .item-actions {
margin-top: 11px; margin-top: 11px;
margin-right: 12px; margin-right: 12px;
...@@ -359,7 +359,7 @@ input.courseware-unit-search-input { ...@@ -359,7 +359,7 @@ input.courseware-unit-search-input {
} }
} }
.item-actions { .component-actions {
float: right; float: right;
.edit-button, .edit-button,
...@@ -499,8 +499,8 @@ input.courseware-unit-search-input { ...@@ -499,8 +499,8 @@ input.courseware-unit-search-input {
text-transform: uppercase; text-transform: uppercase;
} }
.edit-pane { .components {
.xmodule_edit.editable { .component {
position: relative; position: relative;
border: 1px solid transparent; border: 1px solid transparent;
z-index: 10; z-index: 10;
...@@ -577,82 +577,76 @@ input.courseware-unit-search-input { ...@@ -577,82 +577,76 @@ input.courseware-unit-search-input {
opacity: 0; opacity: 0;
-webkit-transition: opacity .15s; -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 { &.adding {
padding: 0; background-color: $blue;
border: 1px solid #8891a1; border-color: #437fbf;
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 { .new-component-button {
background-color: $blue; display: block;
border-color: #437fbf; padding: 20px;
} text-align: center;
color: #6d788b;
}
.new-component-button { h5 {
display: block; margin-bottom: 8px;
padding: 20px; color: #fff;
text-align: center; font-weight: 700;
color: #6d788b; }
}
h5 { .rendered-component {
margin-bottom: 8px; display: none;
color: #fff; background: #fff;
font-weight: 700; border-radius: 3px 3px 0 0;
} }
.rendered-component { .new-component-type {
display: none; @include clearfix;
background: #fff;
border-radius: 3px 3px 0 0; 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;
.new-component-type { &:hover {
@include clearfix; background-color: rgba(255, 255, 255, .2);
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;
}
} }
}
.new-component-step-1, .name {
.new-component-step-2 { position: absolute;
display: none; bottom: 5px;
padding: 20px; left: 0;
width: 100%;
padding: 10px;
box-sizing: border-box;
}
} }
} }
}
.video-unit img, .new-component,
.discussion-unit img { .new-component-templates {
width: 100%; display: none;
padding: 20px;
}
} }
} }
......
${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 @@ ...@@ -9,19 +9,52 @@
<ul> <ul>
<li><a href="#">Week 2</a></li> <li><a href="#">Week 2</a></li>
<li><a href="#">Linearity and Superposition</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> </ul>
</nav> </nav>
<section class='edit-pane'> <ol class="components">
${editable_preview} % for id in components:
</section> <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> </article>
<div class="sidebar"> <div class="sidebar">
<div class="unit-properties window"> <div class="unit-properties window">
<h4>Properties</h4> <h4>Properties</h4>
<div class="window-contents"> <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"> <div class="row">
<label>State:</label> <label>State:</label>
<div class="visibility-options"> <div class="visibility-options">
......
...@@ -9,9 +9,9 @@ import django.contrib.auth.views ...@@ -9,9 +9,9 @@ import django.contrib.auth.views
urlpatterns = ('', urlpatterns = ('',
url(r'^$', 'contentstore.views.index', name='index'), 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'^edit/(?P<location>.*?)$', 'contentstore.views.edit_unit', name='edit_unit'),
url(r'^delete/(?P<location>.*?)$', 'contentstore.views.delete_unit', name='delete_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'^save_item$', 'contentstore.views.save_item', name='save_item'),
url(r'^clone_item$', 'contentstore.views.clone_item', name='clone_item'), url(r'^clone_item$', 'contentstore.views.clone_item', name='clone_item'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
......
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
if $(element).hasClass('xmodule_display') if $(element).hasClass('xmodule_display')
$(document).trigger('XModule.loaded.display', [element, module]) $(document).trigger('XModule.loaded.display', [element, module])
return module
catch error catch error
console.error "Unable to load #{moduleType}: #{error.message}" if console console.error "Unable to load #{moduleType}: #{error.message}" if console
...@@ -33,7 +35,7 @@ ...@@ -33,7 +35,7 @@
else else
modules = $(selector) modules = $(selector)
modules.each (idx, element) -> XModule.loadModule element modules.each((idx, element) -> XModule.loadModule element)
class @XModule.Descriptor class @XModule.Descriptor
......
<div class="xmodule_edit xmodule_${editor_class} ${editable_class}" data-type="${editor_type}" data-id="${location}"> <section class="xmodule_edit xmodule_${class_}" data-type="${module_name}">
<%include file="xmodule_display.html"/> ${content}
% if editable_data: </section>
<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>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment