Commit 310f26a6 by Calen Pennington

Basic implementation of draft, public and private modes

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