Basic implementation of draft, public and private modes
This commit is contained in:
@@ -59,9 +59,9 @@ def compute_unit_state(unit):
|
||||
'private' content is editabled and not visible in the LMS
|
||||
"""
|
||||
|
||||
if unit.location.revision == DRAFT:
|
||||
if unit.metadata.get('is_draft', False):
|
||||
try:
|
||||
modulestore('direct').get_item(unit.location._replace(revision=None))
|
||||
modulestore('direct').get_item(unit.location)
|
||||
return 'draft'
|
||||
except ItemNotFoundError:
|
||||
return 'private'
|
||||
|
||||
@@ -521,6 +521,21 @@ def publish_draft(request):
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
def unpublish_unit(request):
|
||||
location = request.POST['id']
|
||||
|
||||
# check permissions for this user within this course
|
||||
if not has_access(request.user, location):
|
||||
raise PermissionDenied()
|
||||
|
||||
modulestore().unpublish(location)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
def clone_item(request):
|
||||
|
||||
@@ -9,8 +9,15 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
'click #delete-draft': 'deleteDraft'
|
||||
'click #create-draft': 'createDraft'
|
||||
'click #publish-draft': 'publishDraft'
|
||||
'change #visibility': 'setVisibility'
|
||||
|
||||
initialize: =>
|
||||
@visibility_view = new CMS.Views.UnitEdit.Visibility(
|
||||
el: @$('#visibility')
|
||||
model: @model
|
||||
)
|
||||
@visibility_view.render()
|
||||
|
||||
@$newComponentItem = @$('.new-component-item')
|
||||
@$newComponentTypePicker = @$('.new-component')
|
||||
@$newComponentTemplatePickers = @$('.new-component-templates')
|
||||
@@ -18,6 +25,7 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: (event, ui) => @model.set('children', @components())
|
||||
)
|
||||
|
||||
@$('.component').each((idx, element) =>
|
||||
@@ -28,10 +36,9 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
id: $(element).data('id'),
|
||||
)
|
||||
)
|
||||
update: (event, ui) => @model.set('children', @components())
|
||||
)
|
||||
|
||||
@model.components = @components()
|
||||
|
||||
# New component creation
|
||||
showNewComponentForm: (event) =>
|
||||
event.preventDefault()
|
||||
@@ -75,9 +82,7 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
|
||||
|
||||
saveDraft: =>
|
||||
@model.save(
|
||||
children: @components()
|
||||
)
|
||||
@model.save()
|
||||
|
||||
deleteComponent: (event) =>
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
@@ -101,6 +106,7 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
@$el.toggleClass('edit-state-public edit-state-draft')
|
||||
@model.set('state', 'draft')
|
||||
)
|
||||
|
||||
publishDraft: (event) ->
|
||||
@@ -108,4 +114,26 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
@$el.toggleClass('edit-state-public edit-state-draft')
|
||||
)
|
||||
@model.set('state', 'public')
|
||||
)
|
||||
|
||||
setVisibility: (event) ->
|
||||
if @$('#visibility').val() == 'private'
|
||||
target_url = '/unpublish_unit'
|
||||
else
|
||||
target_url = '/publish_draft'
|
||||
|
||||
$.post(target_url, {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
@$el.toggleClass('edit-state-public edit-state-private')
|
||||
@model.set('state', @$('#visibility').val())
|
||||
)
|
||||
|
||||
class CMS.Views.UnitEdit.Visibility extends Backbone.View
|
||||
initialize: =>
|
||||
@model.on('change:state', @render)
|
||||
|
||||
render: =>
|
||||
@$el.val(@model.get('state'))
|
||||
|
||||
|
||||
@@ -421,7 +421,6 @@
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
#save-draft,
|
||||
#delete-draft,
|
||||
#publish-draft,
|
||||
#published-alert,
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
new CMS.Views.UnitEdit({
|
||||
el: $('.main-wrapper'),
|
||||
model: new CMS.Models.Module({
|
||||
id: '${unit_location}'
|
||||
id: '${unit_location}',
|
||||
state: '${unit_state}'
|
||||
})
|
||||
});
|
||||
</script>
|
||||
@@ -74,9 +75,9 @@
|
||||
<div class="window-contents">
|
||||
<div class="row visibility">
|
||||
<label class="inline-label">Visibility:</label>
|
||||
<select>
|
||||
<option>Public</option>
|
||||
<option>Private</option>
|
||||
<select id='visibility'>
|
||||
<option value="public">Public</option>
|
||||
<option value="private">Private</option>
|
||||
</select>
|
||||
</div>
|
||||
<a id="create-draft" href="#">This unit has been published. Click here to edit it.</a>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import patterns, include, url
|
||||
|
||||
import django.contrib.auth.views
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
# from django.contrib import admin
|
||||
# admin.autodiscover()
|
||||
@@ -17,12 +15,13 @@ urlpatterns = ('',
|
||||
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'^unpublish_unit$', 'contentstore.views.unpublish_unit', name='unpublish_unit'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
|
||||
'contentstore.views.course_index', name='course_index'),
|
||||
url(r'^github_service_hook$', 'github_sync.views.github_post_receive'),
|
||||
url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
|
||||
'contentstore.views.preview_dispatch', name='preview_dispatch'),
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$',
|
||||
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$',
|
||||
'contentstore.views.upload_asset', name='upload_asset'),
|
||||
url(r'^manage_users/(?P<location>.*?)$', 'contentstore.views.manage_users', name='manage_users'),
|
||||
url(r'^add_user/(?P<location>.*?)$',
|
||||
@@ -56,6 +55,6 @@ urlpatterns += (
|
||||
|
||||
if settings.DEBUG:
|
||||
## Jasmine
|
||||
urlpatterns=urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
|
||||
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
|
||||
|
||||
urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
@@ -6,6 +6,24 @@ from .exceptions import ItemNotFoundError
|
||||
DRAFT = 'draft'
|
||||
|
||||
|
||||
def as_draft(location):
|
||||
"""
|
||||
Returns the Location that is the draft for `location`
|
||||
"""
|
||||
return Location(location)._replace(revision=DRAFT)
|
||||
|
||||
|
||||
def wrap_draft(item):
|
||||
"""
|
||||
Sets `item.metadata['is_draft']` to `True` if the item is a
|
||||
draft, and false otherwise. Sets the item's location to the
|
||||
non-draft location in either case
|
||||
"""
|
||||
item.metadata['is_draft'] = item.location.revision == DRAFT
|
||||
item.location = item.location._replace(revision=None)
|
||||
return item
|
||||
|
||||
|
||||
class DraftModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
This mixin modifies a modulestore to give it draft semantics.
|
||||
@@ -37,9 +55,9 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
get_children() to cache. None indicates to cache all descendents
|
||||
"""
|
||||
try:
|
||||
return super(DraftModuleStore, self).get_item(Location(location)._replace(revision=DRAFT), depth)
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item(as_draft(location), depth))
|
||||
except ItemNotFoundError:
|
||||
return super(DraftModuleStore, self).get_item(location, depth)
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item(location, depth))
|
||||
|
||||
def get_instance(self, course_id, location):
|
||||
"""
|
||||
@@ -47,9 +65,9 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
TODO (vshnayder): this may want to live outside the modulestore eventually
|
||||
"""
|
||||
try:
|
||||
return super(DraftModuleStore, self).get_instance(course_id, Location(location)._replace(revision=DRAFT))
|
||||
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, as_draft(location)))
|
||||
except ItemNotFoundError:
|
||||
return super(DraftModuleStore, self).get_instance(course_id, location)
|
||||
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location))
|
||||
|
||||
def get_items(self, location, depth=0):
|
||||
"""
|
||||
@@ -64,7 +82,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
in the request. The depth is counted in the number of calls to
|
||||
get_children() to cache. None indicates to cache all descendents
|
||||
"""
|
||||
draft_loc = Location(location)._replace(revision=DRAFT)
|
||||
draft_loc = as_draft(location)
|
||||
draft_items = super(DraftModuleStore, self).get_items(draft_loc, depth)
|
||||
items = super(DraftModuleStore, self).get_items(location, depth)
|
||||
|
||||
@@ -75,14 +93,14 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
if (item.location.revision != DRAFT
|
||||
and item.location._replace(revision=None) not in draft_locs_found)
|
||||
]
|
||||
return draft_items + non_draft_items
|
||||
return [wrap_draft(item) for item in draft_items + non_draft_items]
|
||||
|
||||
def clone_item(self, source, location):
|
||||
"""
|
||||
Clone a new item that is a copy of the item at the location `source`
|
||||
and writes it to `location`
|
||||
"""
|
||||
return super(DraftModuleStore, self).clone_item(source, Location(location)._replace(revision=DRAFT))
|
||||
return super(DraftModuleStore, self).clone_item(source, as_draft(location))
|
||||
|
||||
def update_item(self, location, data):
|
||||
"""
|
||||
@@ -92,7 +110,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
location: Something that can be passed to Location
|
||||
data: A nested dictionary of problem data
|
||||
"""
|
||||
draft_loc = Location(location)._replace(revision=DRAFT)
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
if draft_item.location.revision != DRAFT:
|
||||
self.clone_item(location, draft_loc)
|
||||
@@ -107,9 +125,9 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
location: Something that can be passed to Location
|
||||
children: A list of child item identifiers
|
||||
"""
|
||||
draft_loc = Location(location)._replace(revision=DRAFT)
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
if draft_item.location.revision != DRAFT:
|
||||
if not draft_item.metadata['is_draft']:
|
||||
self.clone_item(location, draft_loc)
|
||||
|
||||
return super(DraftModuleStore, self).update_children(draft_loc, children)
|
||||
@@ -122,8 +140,12 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
location: Something that can be passed to Location
|
||||
metadata: A nested dictionary of module metadata
|
||||
"""
|
||||
draft_loc = Location(location)._replace(revision=DRAFT)
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
|
||||
if 'is_draft' in metadata:
|
||||
del metadata['is_draft']
|
||||
|
||||
if draft_item.location.revision != DRAFT:
|
||||
self.clone_item(location, draft_loc)
|
||||
|
||||
@@ -135,7 +157,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
return super(DraftModuleStore, self).delete_item(Location(location)._replace(revision=DRAFT))
|
||||
return super(DraftModuleStore, self).delete_item(as_draft(location))
|
||||
|
||||
def publish(self, location):
|
||||
"""
|
||||
@@ -149,3 +171,10 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
super(DraftModuleStore, self).update_children(location, draft.definition.get('children', []))
|
||||
super(DraftModuleStore, self).update_metadata(location, metadata)
|
||||
self.delete_item(location)
|
||||
|
||||
def unpublish(self, location):
|
||||
"""
|
||||
Turn the published version into a draft, removing the published version
|
||||
"""
|
||||
super(DraftModuleStore, self).clone_item(location, as_draft(location))
|
||||
super(DraftModuleStore, self).delete_item(location)
|
||||
|
||||
Reference in New Issue
Block a user