From 75f8b7c98d17d542c007da56114a6d984faec42b Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 4 Oct 2012 10:44:18 -0400 Subject: [PATCH 01/29] Add Draft module store that is used whenever any item is update in the CAS (but not during import, and not for templates) --- .../management/commands/import.py | 2 +- cms/envs/dev.py | 22 +-- .../xmodule/xmodule/modulestore/__init__.py | 4 +- .../lib/xmodule/xmodule/modulestore/draft.py | 128 ++++++++++++++++++ .../lib/xmodule/xmodule/modulestore/mongo.py | 6 + common/lib/xmodule/xmodule/templates.py | 6 +- 6 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 common/lib/xmodule/xmodule/modulestore/draft.py diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 1d15f1e7df..a8ec2c2685 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -26,4 +26,4 @@ class Command(BaseCommand): print "Importing. Data_dir={data}, course_dirs={courses}".format( data=data_dir, courses=course_dirs) - import_from_xml(modulestore(), data_dir, course_dirs, load_error_modules=False) + import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False) diff --git a/cms/envs/dev.py b/cms/envs/dev.py index 8401fa5c15..e5548df2d4 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -14,17 +14,23 @@ LOGGING = get_logger_config(ENV_ROOT / "log", tracking_filename="tracking.log", debug=True) +modulestore_options = { + 'default_class': 'xmodule.raw_module.RawDescriptor', + 'host': 'localhost', + 'db': 'xmodule', + 'collection': 'modulestore', + 'fs_root': GITHUB_REPO_ROOT, + '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': GITHUB_REPO_ROOT, - 'render_template': 'mitxmako.shortcuts.render_to_string', - } + 'OPTIONS': modulestore_options } } diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index 880159d8ed..3ee83449f9 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -345,7 +345,9 @@ class ModuleStore(object): Returns a list containing the top level XModuleDescriptors of the courses in this modulestore. ''' - raise NotImplementedError + # TODO (vshnayder): Why do I have to specify i4x here? + course_filter = Location("i4x", category="course") + return self.get_items(course_filter) def get_parent_locations(self, location): '''Find all locations that are the parents of this location. Needed diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py new file mode 100644 index 0000000000..6293063ffa --- /dev/null +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -0,0 +1,128 @@ + +from . import ModuleStoreBase, Location +from .exceptions import ItemNotFoundError + + +class DraftModuleStore(ModuleStoreBase): + """ + This mixin modifies a modulestore to give it draft semantics. + That is, edits made to units are stored to locations that have the revision 'draft', + and when reads are made, they first read with revision 'draft', and then fall back + to the baseline revision only if 'draft' doesn't exist. + + This module also includes functionality to promote 'draft' modules (and optionally + their children) to published modules. + """ + + def get_item(self, location, depth=0): + """ + Returns an XModuleDescriptor instance for the item at location. + If location.revision is None, returns the item with the most + recent revision + + If any segment of the location is None except revision, raises + xmodule.modulestore.exceptions.InsufficientSpecificationError + + If no object is found at that location, raises + xmodule.modulestore.exceptions.ItemNotFoundError + + location: Something that can be passed to Location + + depth (int): An argument that some module stores may use to prefetch + descendents of the queried modules for more efficient results later + in the request. The depth is counted in the number of calls to + get_children() to cache. None indicates to cache all descendents + """ + try: + return super(DraftModuleStore, self).get_item(Location(location)._replace(revision='draft'), depth) + except ItemNotFoundError: + return super(DraftModuleStore, self).get_item(location, depth) + + def get_instance(self, course_id, location): + """ + Get an instance of this location, with policy for course_id applied. + 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')) + except ItemNotFoundError: + return super(DraftModuleStore, self).get_instance(course_id, location) + + def get_items(self, location, depth=0): + """ + Returns a list of XModuleDescriptor instances for the items + that match location. Any element of location that is None is treated + as a wildcard that matches any value + + location: Something that can be passed to Location + + depth: An argument that some module stores may use to prefetch + descendents of the queried modules for more efficient results later + 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_items = super(DraftModuleStore, self).get_items(draft_loc, depth) + items = super(DraftModuleStore, self).get_items(location, depth) + + draft_locs_found = set(item.location._replace(revision=None) for item in draft_items) + non_draft_items = [ + item + for item in items + if (item.location.revision != 'draft' + and item.location._replace(revision=None) not in draft_locs_found) + ] + return 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')) + + def update_item(self, location, data): + """ + Set the data in the item specified by the location to + data + + location: Something that can be passed to Location + data: A nested dictionary of problem data + """ + return super(DraftModuleStore, self).update_item(Location(location)._replace(revision='draft'), data) + + def update_children(self, location, children): + """ + Set the children for the item specified by the location to + children + + location: Something that can be passed to Location + children: A list of child item identifiers + """ + return super(DraftModuleStore, self).update_children(Location(location)._replace(revision='draft'), children) + + def update_metadata(self, location, metadata): + """ + Set the metadata for the item specified by the location to + metadata + + location: Something that can be passed to Location + metadata: A nested dictionary of module metadata + """ + return super(DraftModuleStore, self).update_metadata(Location(location)._replace(revision='draft'), metadata) + + def delete_item(self, location): + """ + Delete an item from this modulestore + + location: Something that can be passed to Location + """ + 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')) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 21e28e9d67..1e203c6a78 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -13,6 +13,7 @@ from xmodule.mako_module import MakoDescriptorSystem from xmodule.error_module import ErrorDescriptor from . import ModuleStoreBase, Location +from .draft import DraftModuleStore from .exceptions import (ItemNotFoundError, DuplicateItemError) @@ -341,3 +342,8 @@ class MongoModuleStore(ModuleStoreBase): are loaded on demand, rather than up front """ return {} + + +# DraftModuleStore is first, because it needs to intercept calls to MongoModuleStore +class DraftMongoModuleStore(DraftModuleStore, MongoModuleStore): + pass diff --git a/common/lib/xmodule/xmodule/templates.py b/common/lib/xmodule/xmodule/templates.py index 2937cddea6..41b1523709 100644 --- a/common/lib/xmodule/xmodule/templates.py +++ b/common/lib/xmodule/xmodule/templates.py @@ -75,6 +75,6 @@ def update_templates(): ), exc_info=True) continue - modulestore().update_item(template_location, template.data) - modulestore().update_children(template_location, template.children) - modulestore().update_metadata(template_location, template.metadata) + modulestore('direct').update_item(template_location, template.data) + modulestore('direct').update_children(template_location, template.children) + modulestore('direct').update_metadata(template_location, template.metadata) From 8da372344542af5a484a3b6f2843dbc4093c13a0 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 4 Oct 2012 11:22:52 -0400 Subject: [PATCH 02/29] Clone the full item as a draft if it doesn't already exist when doing a draft editing operation --- .../lib/xmodule/xmodule/modulestore/draft.py | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py index 6293063ffa..12173ad13d 100644 --- a/common/lib/xmodule/xmodule/modulestore/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -2,15 +2,17 @@ from . import ModuleStoreBase, Location from .exceptions import ItemNotFoundError +DRAFT = 'draft' + class DraftModuleStore(ModuleStoreBase): """ This mixin modifies a modulestore to give it draft semantics. - That is, edits made to units are stored to locations that have the revision 'draft', - and when reads are made, they first read with revision 'draft', and then fall back - to the baseline revision only if 'draft' doesn't exist. + That is, edits made to units are stored to locations that have the revision DRAFT, + and when reads are made, they first read with revision DRAFT, and then fall back + to the baseline revision only if DRAFT doesn't exist. - This module also includes functionality to promote 'draft' modules (and optionally + This module also includes functionality to promote DRAFT modules (and optionally their children) to published modules. """ @@ -34,7 +36,7 @@ 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 super(DraftModuleStore, self).get_item(Location(location)._replace(revision=DRAFT), depth) except ItemNotFoundError: return super(DraftModuleStore, self).get_item(location, depth) @@ -44,7 +46,7 @@ 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 super(DraftModuleStore, self).get_instance(course_id, Location(location)._replace(revision=DRAFT)) except ItemNotFoundError: return super(DraftModuleStore, self).get_instance(course_id, location) @@ -61,7 +63,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 = Location(location)._replace(revision=DRAFT) draft_items = super(DraftModuleStore, self).get_items(draft_loc, depth) items = super(DraftModuleStore, self).get_items(location, depth) @@ -69,7 +71,7 @@ class DraftModuleStore(ModuleStoreBase): non_draft_items = [ item for item in items - if (item.location.revision != 'draft' + if (item.location.revision != DRAFT and item.location._replace(revision=None) not in draft_locs_found) ] return draft_items + non_draft_items @@ -79,7 +81,7 @@ class DraftModuleStore(ModuleStoreBase): 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, Location(location)._replace(revision=DRAFT)) def update_item(self, location, data): """ @@ -89,7 +91,12 @@ class DraftModuleStore(ModuleStoreBase): location: Something that can be passed to Location data: A nested dictionary of problem data """ - return super(DraftModuleStore, self).update_item(Location(location)._replace(revision='draft'), data) + draft_loc = Location(location)._replace(revision=DRAFT) + draft_item = self.get_item(location) + if draft_item.location.revision != DRAFT: + self.clone_item(location, draft_loc) + + return super(DraftModuleStore, self).update_item(draft_loc, data) def update_children(self, location, children): """ @@ -99,7 +106,12 @@ class DraftModuleStore(ModuleStoreBase): location: Something that can be passed to Location children: A list of child item identifiers """ - return super(DraftModuleStore, self).update_children(Location(location)._replace(revision='draft'), children) + draft_loc = Location(location)._replace(revision=DRAFT) + draft_item = self.get_item(location) + if draft_item.location.revision != DRAFT: + self.clone_item(location, draft_loc) + + return super(DraftModuleStore, self).update_children(draft_loc, children) def update_metadata(self, location, metadata): """ @@ -109,7 +121,12 @@ class DraftModuleStore(ModuleStoreBase): location: Something that can be passed to Location metadata: A nested dictionary of module metadata """ - return super(DraftModuleStore, self).update_metadata(Location(location)._replace(revision='draft'), metadata) + draft_loc = Location(location)._replace(revision=DRAFT) + draft_item = self.get_item(location) + if draft_item.location.revision != DRAFT: + self.clone_item(location, draft_loc) + + return super(DraftModuleStore, self).update_metadata(draft_loc, metadata) def delete_item(self, location): """ @@ -117,7 +134,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(Location(location)._replace(revision=DRAFT)) def get_parent_locations(self, location): '''Find all locations that are the parents of this location. Needed @@ -125,4 +142,4 @@ class DraftModuleStore(ModuleStoreBase): returns an iterable of things that can be passed to Location. ''' - return super(DraftModuleStore, self).get_parent_locations(Location(location)._replace(revision='draft')) + return super(DraftModuleStore, self).get_parent_locations(Location(location)._replace(revision=DRAFT)) From 1a8532d8ad595a4d8ce6f4a9950fcba621eb574c Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 4 Oct 2012 14:29:24 -0400 Subject: [PATCH 03/29] Make it possible to create, edit, and publish a draft --- cms/djangoapps/contentstore/utils.py | 22 +++++++++++ cms/djangoapps/contentstore/views.py | 39 +++++++++++++++++-- .../coffee/src/views/module_edit.coffee | 4 +- cms/static/coffee/src/views/unit.coffee | 35 +++++++++++++++-- cms/static/sass/_unit.scss | 33 ++++++++++++++++ cms/templates/unit.html | 21 ++++------ cms/urls.py | 2 + .../lib/xmodule/xmodule/modulestore/draft.py | 16 ++++---- lms/envs/cms/dev.py | 22 +++++++---- 9 files changed, 156 insertions(+), 38 deletions(-) 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 } } From 1328fc5ac0ceeb124e27d36afbe26e0760b07a46 Mon Sep 17 00:00:00 2001 From: Calen Pennington Date: Thu, 4 Oct 2012 15:27:02 -0400 Subject: [PATCH 04/29] Store published date in module metadata, and display it on draft pages --- cms/djangoapps/contentstore/utils.py | 6 ++-- cms/djangoapps/contentstore/views.py | 28 +++++++++++++------ cms/static/sass/_unit.scss | 6 ++-- cms/templates/unit.html | 16 ++++++++--- .../lib/xmodule/xmodule/modulestore/draft.py | 6 +++- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index f103c74a8d..687df11f62 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -35,13 +35,13 @@ def get_course_location_for_item(location): return location -def get_lms_link_for_item(item): +def get_lms_link_for_item(location): if settings.LMS_BASE is not None: lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format( lms_base=settings.LMS_BASE, # TODO: These will need to be changed to point to the particular instance of this problem in the particular course - course_id = modulestore().get_containing_courses(item.location)[0].id, - location=item.location, + course_id = modulestore().get_containing_courses(location)[0].id, + location=location, ) else: lms_link = None diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index cf60de7937..43ec799a66 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -1,11 +1,12 @@ from util.json_request import expect_json -import json -import os -import logging -import sys -import mimetypes -import StringIO import exceptions +import json +import logging +import mimetypes +import os +import StringIO +import sys +import time from collections import defaultdict from uuid import uuid4 @@ -154,7 +155,7 @@ def edit_subsection(request, location): item = modulestore().get_item(location) - lms_link = get_lms_link_for_item(item) + lms_link = get_lms_link_for_item(location) # make sure that location references a 'sequential', otherwise return BadRequest if item.location.category != 'sequential': @@ -183,7 +184,8 @@ def edit_unit(request, location): item = modulestore().get_item(location) - lms_link = get_lms_link_for_item(item) + # The non-draft location + lms_link = get_lms_link_for_item(item.location._replace(revision=None)) component_templates = defaultdict(list) @@ -212,16 +214,24 @@ def edit_unit(request, location): unit_state = compute_unit_state(item) + try: + published_date = time.strftime('%B %d, %Y', item.metadata.get('published_date')) + except TypeError: + published_date = None + return render_to_response('unit.html', { 'unit': item, + 'unit_location': published_location, 'components': components, 'component_templates': component_templates, - 'lms_link': lms_link, + 'draft_preview_link': lms_link, + 'published_preview_link': lms_link, 'subsection': containing_subsection, 'section': containing_section, 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'unit_state': unit_state, 'release_date': None, + 'published_date': published_date, }) diff --git a/cms/static/sass/_unit.scss b/cms/static/sass/_unit.scss index a030168cff..fcd2f9a64d 100644 --- a/cms/static/sass/_unit.scss +++ b/cms/static/sass/_unit.scss @@ -87,7 +87,7 @@ } } - .rendered-component { + .xmodule_display { padding: 40px 20px 20px; } @@ -410,7 +410,8 @@ #delete-draft, #publish-draft, .component-actions, - .new-component-item { + .new-component-item, + #published-alert { display: none; } @@ -423,6 +424,7 @@ #save-draft, #delete-draft, #publish-draft, + #published-alert, #create-draft, { display: none; } diff --git a/cms/templates/unit.html b/cms/templates/unit.html index 581ec0bef3..7f300d717c 100644 --- a/cms/templates/unit.html +++ b/cms/templates/unit.html @@ -8,14 +8,22 @@ new CMS.Views.UnitEdit({ el: $('.main-wrapper'), model: new CMS.Models.Module({ - id: '${unit.location.url()}' + id: '${unit_location}' }) }); <%block name="content"> -
+
+
+

You are editing a draft. + % if published_date: + This unit was originally published on ${published_date}. + % endif +

+ Preview the published version +

@@ -60,7 +68,7 @@
-