diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 4000f011ba..f103c74a8d 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1,6 +1,9 @@ from django.conf import settings from xmodule.modulestore import Location 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): ''' @@ -45,3 +48,22 @@ def get_lms_link_for_item(item): 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' diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 190b463383..cf60de7937 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -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 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 .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 @@ -210,6 +210,8 @@ def edit_unit(request, location): containing_section_locs = modulestore().get_parent_locations(containing_subsection.location) containing_section = modulestore().get_item(containing_section_locs[0]) + unit_state = compute_unit_state(item) + return render_to_response('unit.html', { 'unit': item, 'components': components, @@ -217,7 +219,9 @@ def edit_unit(request, location): 'lms_link': lms_link, 'subsection': containing_subsection, '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): }) - def user_author_string(user): '''Get an author string for commits by this user. Format: first last . @@ -428,7 +431,7 @@ def delete_item(request): item = modulestore().get_item(item_location) _delete_item(item, delete_children) - + return HttpResponse() @@ -479,6 +482,34 @@ def save_item(request): return HttpResponse() +@login_required +@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): diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee index 2326756dc8..85c099ab9a 100644 --- a/cms/static/coffee/src/views/module_edit.coffee +++ b/cms/static/coffee/src/views/module_edit.coffee @@ -35,9 +35,9 @@ class CMS.Views.ModuleEdit extends Backbone.View return _metadata - cloneTemplate: (template) -> + cloneTemplate: (parent, template) -> $.post("/clone_item", { - parent_location: @$el.parent().data('id') + parent_location: parent template: template }, (data) => @model.set(id: data.id) diff --git a/cms/static/coffee/src/views/unit.coffee b/cms/static/coffee/src/views/unit.coffee index 1180b17471..3953e5ab92 100644 --- a/cms/static/coffee/src/views/unit.coffee +++ b/cms/static/coffee/src/views/unit.coffee @@ -5,7 +5,10 @@ class CMS.Views.UnitEdit extends Backbone.View 'click .new-component-templates .new-component-template a': 'saveNewComponent' 'click .new-component-templates .cancel-button': 'closeNewComponent' '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: => @$newComponentItem = @$('.new-component-item') @@ -15,7 +18,6 @@ class CMS.Views.UnitEdit extends Backbone.View @$('.components').sortable( handle: '.drag-handle' - update: (event, ui) => @saveOrder() ) @$('.component').each((idx, element) => @@ -30,6 +32,7 @@ class CMS.Views.UnitEdit extends Backbone.View @model.components = @components() + # New component creation showNewComponentForm: (event) => event.preventDefault() @$newComponentItem.addClass('adding') @@ -61,13 +64,16 @@ class CMS.Views.UnitEdit extends Backbone.View @$newComponentItem.before(editor.$el) - editor.cloneTemplate($(event.currentTarget).data('location')) + editor.cloneTemplate( + @$el.data('id'), + $(event.currentTarget).data('location') + ) @closeNewComponent(event) components: => @$('.component').map((idx, el) -> $(el).data('id')).get() - saveOrder: => + saveDraft: => @model.save( children: @components() ) @@ -81,3 +87,24 @@ class CMS.Views.UnitEdit extends Backbone.View @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 diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index 7ed7e3ee7e..a030168cff 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -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; + } +} diff --git a/cms/templates/unit.html b/cms/templates/unit.html index 5434c11845..581ec0bef3 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -14,12 +14,12 @@ <%block name="content"> -
+

-
    +
      % for id in components:
    1. % endfor @@ -64,14 +64,6 @@

      Unit Properties

      -
      - - Set a due date - -
      + This unit has been published. Click here to edit it. + This unit has already been published. Click here to release your changes to it
      -

      This unit is scheduled to be released to students on 10/12/2012 with the subsection "Administrivia and Circuit Elements."

      +

      This unit is scheduled to be released to students on ${release_date} with the subsection ""

      @@ -114,4 +109,4 @@
- \ No newline at end of file + diff --git a/cms/urls.py b/cms/urls.py index 890e9e2671..fa5377c277 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -15,6 +15,8 @@ urlpatterns = ('', url(r'^save_item$', 'contentstore.views.save_item', name='save_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'^create_draft$', 'contentstore.views.create_draft', name='create_draft'), + url(r'^publish_draft$', 'contentstore.views.publish_draft', name='publish_draft'), url(r'^(?P[^/]+)/(?P[^/]+)/course/(?P[^/]+)$', 'contentstore.views.course_index', name='course_index'), url(r'^github_service_hook$', 'github_sync.views.github_post_receive'), diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py index 12173ad13d..2b94f892a4 100644 --- a/common/lib/xmodule/xmodule/modulestore/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -136,10 +136,12 @@ class DraftModuleStore(ModuleStoreBase): """ return super(DraftModuleStore, self).delete_item(Location(location)._replace(revision=DRAFT)) - def get_parent_locations(self, location): - '''Find all locations that are the parents of this location. Needed - for path_to_location(). - - returns an iterable of things that can be passed to Location. - ''' - return super(DraftModuleStore, self).get_parent_locations(Location(location)._replace(revision=DRAFT)) + def publish(self, location): + """ + Save a current draft to the underlying modulestore + """ + draft = self.get_item(location) + super(DraftModuleStore, self).update_item(location, draft.definition.get('data', {})) + super(DraftModuleStore, self).update_children(location, draft.definition.get('children', [])) + super(DraftModuleStore, self).update_metadata(location, draft.metadata) + self.delete_item(location) diff --git a/lms/envs/cms/dev.py b/lms/envs/cms/dev.py index 6e4697cccb..b192f12c93 100644 --- a/lms/envs/cms/dev.py +++ b/lms/envs/cms/dev.py @@ -4,16 +4,22 @@ Settings for the LMS that runs alongside the CMS on AWS 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 = { 'default': { + 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore', + 'OPTIONS': modulestore_options + }, + 'direct': { 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', - 'OPTIONS': { - 'default_class': 'xmodule.raw_module.RawDescriptor', - 'host': 'localhost', - 'db': 'xmodule', - 'collection': 'modulestore', - 'fs_root': DATA_DIR, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } + 'OPTIONS': modulestore_options } }