Commit 1a8532d8 by Calen Pennington

Make it possible to create, edit, and publish a draft

parent 8da37234
from django.conf import settings from django.conf import settings
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.draft import DRAFT
from xmodule.modulestore.exceptions import ItemNotFoundError
def get_course_location_for_item(location): def get_course_location_for_item(location):
''' '''
...@@ -45,3 +48,22 @@ def get_lms_link_for_item(item): ...@@ -45,3 +48,22 @@ def get_lms_link_for_item(item):
return lms_link return lms_link
def compute_unit_state(unit):
"""
Returns whether this unit is 'draft', 'public', or 'private'.
'draft' content is in the process of being edited, but still has a previous
version visible in the LMS
'public' content is locked and visible in the LMS
'private' content is editabled and not visible in the LMS
"""
if unit.location.revision == DRAFT:
try:
modulestore('direct').get_item(unit.location._replace(revision=None))
return 'draft'
except ItemNotFoundError:
return 'private'
else:
return 'public'
...@@ -43,7 +43,7 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache ...@@ -43,7 +43,7 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME
from .utils import get_course_location_for_item, get_lms_link_for_item from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state
from xmodule.templates import all_templates from xmodule.templates import all_templates
...@@ -210,6 +210,8 @@ def edit_unit(request, location): ...@@ -210,6 +210,8 @@ def edit_unit(request, location):
containing_section_locs = modulestore().get_parent_locations(containing_subsection.location) containing_section_locs = modulestore().get_parent_locations(containing_subsection.location)
containing_section = modulestore().get_item(containing_section_locs[0]) containing_section = modulestore().get_item(containing_section_locs[0])
unit_state = compute_unit_state(item)
return render_to_response('unit.html', { return render_to_response('unit.html', {
'unit': item, 'unit': item,
'components': components, 'components': components,
...@@ -217,7 +219,9 @@ def edit_unit(request, location): ...@@ -217,7 +219,9 @@ def edit_unit(request, location):
'lms_link': lms_link, 'lms_link': lms_link,
'subsection': containing_subsection, 'subsection': containing_subsection,
'section': containing_section, 'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty') 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
'release_date': None,
}) })
...@@ -235,7 +239,6 @@ def preview_component(request, location): ...@@ -235,7 +239,6 @@ def preview_component(request, location):
}) })
def user_author_string(user): def user_author_string(user):
'''Get an author string for commits by this user. Format: '''Get an author string for commits by this user. Format:
first last <email@email.com>. first last <email@email.com>.
...@@ -428,7 +431,7 @@ def delete_item(request): ...@@ -428,7 +431,7 @@ def delete_item(request):
item = modulestore().get_item(item_location) item = modulestore().get_item(item_location)
_delete_item(item, delete_children) _delete_item(item, delete_children)
return HttpResponse() return HttpResponse()
...@@ -481,6 +484,34 @@ def save_item(request): ...@@ -481,6 +484,34 @@ def save_item(request):
@login_required @login_required
@expect_json @expect_json
def create_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
# This clones the existing item location to a draft location (the draft is implicit,
# because modulestore is a Draft modulestore)
modulestore().clone_item(location, location)
return HttpResponse()
@login_required
@expect_json
def publish_draft(request):
location = request.POST['id']
# check permissions for this user within this course
if not has_access(request.user, location):
raise PermissionDenied()
modulestore().publish(location)
return HttpResponse()
@login_required
@expect_json
def clone_item(request): def clone_item(request):
parent_location = Location(request.POST['parent_location']) parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template']) template = Location(request.POST['template'])
......
...@@ -35,9 +35,9 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -35,9 +35,9 @@ class CMS.Views.ModuleEdit extends Backbone.View
return _metadata return _metadata
cloneTemplate: (template) -> cloneTemplate: (parent, template) ->
$.post("/clone_item", { $.post("/clone_item", {
parent_location: @$el.parent().data('id') parent_location: parent
template: template template: template
}, (data) => }, (data) =>
@model.set(id: data.id) @model.set(id: data.id)
......
...@@ -5,7 +5,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -5,7 +5,10 @@ class CMS.Views.UnitEdit extends Backbone.View
'click .new-component-templates .new-component-template a': 'saveNewComponent' 'click .new-component-templates .new-component-template a': 'saveNewComponent'
'click .new-component-templates .cancel-button': 'closeNewComponent' 'click .new-component-templates .cancel-button': 'closeNewComponent'
'click .new-component-button': 'showNewComponentForm' 'click .new-component-button': 'showNewComponentForm'
'click .unit-actions .save-button': 'save' 'click #save-draft': 'saveDraft'
'click #delete-draft': 'deleteDraft'
'click #create-draft': 'createDraft'
'click #publish-draft': 'publishDraft'
initialize: => initialize: =>
@$newComponentItem = @$('.new-component-item') @$newComponentItem = @$('.new-component-item')
...@@ -15,7 +18,6 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -15,7 +18,6 @@ class CMS.Views.UnitEdit extends Backbone.View
@$('.components').sortable( @$('.components').sortable(
handle: '.drag-handle' handle: '.drag-handle'
update: (event, ui) => @saveOrder()
) )
@$('.component').each((idx, element) => @$('.component').each((idx, element) =>
...@@ -30,6 +32,7 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -30,6 +32,7 @@ class CMS.Views.UnitEdit extends Backbone.View
@model.components = @components() @model.components = @components()
# New component creation
showNewComponentForm: (event) => showNewComponentForm: (event) =>
event.preventDefault() event.preventDefault()
@$newComponentItem.addClass('adding') @$newComponentItem.addClass('adding')
...@@ -61,13 +64,16 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -61,13 +64,16 @@ class CMS.Views.UnitEdit extends Backbone.View
@$newComponentItem.before(editor.$el) @$newComponentItem.before(editor.$el)
editor.cloneTemplate($(event.currentTarget).data('location')) editor.cloneTemplate(
@$el.data('id'),
$(event.currentTarget).data('location')
)
@closeNewComponent(event) @closeNewComponent(event)
components: => @$('.component').map((idx, el) -> $(el).data('id')).get() components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
saveOrder: => saveDraft: =>
@model.save( @model.save(
children: @components() children: @components()
) )
...@@ -81,3 +87,24 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -81,3 +87,24 @@ class CMS.Views.UnitEdit extends Backbone.View
@saveOrder() @saveOrder()
) )
deleteDraft: (event) ->
$.post('/delete_item', {
id: @$el.data('id')
delete_children: true
}, =>
window.location.reload()
)
createDraft: (event) ->
$.post('/create_draft', {
id: @$el.data('id')
}, =>
@$el.toggleClass('edit-state-public edit-state-draft')
)
publishDraft: (event) ->
$.post('/publish_draft', {
id: @$el.data('id')
}, =>
@$el.toggleClass('edit-state-public edit-state-draft')
)
\ No newline at end of file
...@@ -394,3 +394,36 @@ ...@@ -394,3 +394,36 @@
} }
} }
} }
.edit-state-draft {
.visibility {
display: none;
}
#create-draft {
display: none;
}
}
.edit-state-public {
#save-draft,
#delete-draft,
#publish-draft,
.component-actions,
.new-component-item {
display: none;
}
.drag-handle {
display: none !important;
}
}
.edit-state-private {
#save-draft,
#delete-draft,
#publish-draft,
#create-draft, {
display: none;
}
}
...@@ -14,12 +14,12 @@ ...@@ -14,12 +14,12 @@
</script> </script>
</%block> </%block>
<%block name="content"> <%block name="content">
<div class="main-wrapper"> <div class="main-wrapper edit-state-${unit_state}" data-id="${unit.location.url()}">
<div class="inner-wrapper"> <div class="inner-wrapper">
<div class="main-column"> <div class="main-column">
<article class="unit-body window"> <article class="unit-body window">
<p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.display_name}" class="unit-display-name-input" /></p> <p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.display_name}" class="unit-display-name-input" /></p>
<ol class="components" data-id="${unit.location.url()}"> <ol class="components">
% for id in components: % for id in components:
<li class="component" data-id="${id}"/> <li class="component" data-id="${id}"/>
% endfor % endfor
...@@ -64,14 +64,6 @@ ...@@ -64,14 +64,6 @@
<div class="unit-properties window"> <div class="unit-properties window">
<h4>Unit Properties</h4> <h4>Unit Properties</h4>
<div class="window-contents"> <div class="window-contents">
<div class="due-date-input row">
<label>Due date:</label>
<a href="#" class="set-date">Set a due date</a>
<div class="date-setter">
<p class="date-description"><input type="text" value="10/20/2012" class="date-input" /> <input type="text" value="6:00 am" class="time-input" />
<a href="#" class="remove-date">Remove due date</a>
</div>
</div>
<div class="row visibility"> <div class="row visibility">
<label class="inline-label">Visibility:</label> <label class="inline-label">Visibility:</label>
<select> <select>
...@@ -79,11 +71,14 @@ ...@@ -79,11 +71,14 @@
<option>Private</option> <option>Private</option>
</select> </select>
</div> </div>
<a id="create-draft" href="#">This unit has been published. Click here to edit it.</a>
<a id="publish-draft" href="#">This unit has already been published. Click here to release your changes to it</a>
<div class="row status"> <div class="row status">
<p>This unit is scheduled to be released to <strong>students</strong> on <strong>10/12/2012</strong> with the subsection <a href="#">"Administrivia and Circuit Elements."</a></p> <p>This unit is scheduled to be released to <strong>students</strong> on <strong>${release_date}</strong> with the subsection <a href="#">""</a></p>
</div> </div>
<div class="row unit-actions"> <div class="row unit-actions">
<a href="#" class="save-button">Save</a> <a id="save-draft" href="#" class="save-button">Save Draft</a>
<a id="delete-draft" href="#" class="save-button">Delete Draft</a>
<a href="${lms_link}" target="_blank" class="preview-button">Preview</a> <a href="${lms_link}" target="_blank" class="preview-button">Preview</a>
</div> </div>
</div> </div>
...@@ -114,4 +109,4 @@ ...@@ -114,4 +109,4 @@
</div> </div>
</div> </div>
</%block> </%block>
\ No newline at end of file
...@@ -15,6 +15,8 @@ urlpatterns = ('', ...@@ -15,6 +15,8 @@ urlpatterns = ('',
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
url(r'^delete_item$', 'contentstore.views.delete_item', name='delete_item'), url(r'^delete_item$', 'contentstore.views.delete_item', name='delete_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'^create_draft$', 'contentstore.views.create_draft', name='create_draft'),
url(r'^publish_draft$', 'contentstore.views.publish_draft', name='publish_draft'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
'contentstore.views.course_index', name='course_index'), 'contentstore.views.course_index', name='course_index'),
url(r'^github_service_hook$', 'github_sync.views.github_post_receive'), url(r'^github_service_hook$', 'github_sync.views.github_post_receive'),
......
...@@ -136,10 +136,12 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -136,10 +136,12 @@ class DraftModuleStore(ModuleStoreBase):
""" """
return super(DraftModuleStore, self).delete_item(Location(location)._replace(revision=DRAFT)) return super(DraftModuleStore, self).delete_item(Location(location)._replace(revision=DRAFT))
def get_parent_locations(self, location): def publish(self, location):
'''Find all locations that are the parents of this location. Needed """
for path_to_location(). Save a current draft to the underlying modulestore
"""
returns an iterable of things that can be passed to Location. draft = self.get_item(location)
''' super(DraftModuleStore, self).update_item(location, draft.definition.get('data', {}))
return super(DraftModuleStore, self).get_parent_locations(Location(location)._replace(revision=DRAFT)) super(DraftModuleStore, self).update_children(location, draft.definition.get('children', []))
super(DraftModuleStore, self).update_metadata(location, draft.metadata)
self.delete_item(location)
...@@ -4,16 +4,22 @@ Settings for the LMS that runs alongside the CMS on AWS ...@@ -4,16 +4,22 @@ Settings for the LMS that runs alongside the CMS on AWS
from ..dev import * from ..dev import *
modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'xmodule',
'collection': 'modulestore',
'fs_root': DATA_DIR,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
MODULESTORE = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': { 'OPTIONS': modulestore_options
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'xmodule',
'collection': 'modulestore',
'fs_root': DATA_DIR,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
} }
} }
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