Merge pull request #834 from MITx/feature/cale/cas-draft-mode
CAS Draft Mode
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -141,8 +141,6 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
"""Make sure pages that do require login work."""
|
||||
auth_pages = (
|
||||
reverse('index'),
|
||||
reverse('edit_item'),
|
||||
reverse('save_item'),
|
||||
)
|
||||
|
||||
# These are pages that should just load when the user is logged in
|
||||
@@ -181,6 +179,7 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
|
||||
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
class EditTestCase(ContentStoreTestCase):
|
||||
@@ -195,17 +194,17 @@ class EditTestCase(ContentStoreTestCase):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
|
||||
def check_edit_item(self, test_course_name):
|
||||
def check_edit_unit(self, test_course_name):
|
||||
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
|
||||
|
||||
for descriptor in modulestore().get_items(Location(None, None, None, None, None)):
|
||||
for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)):
|
||||
print "Checking ", descriptor.location.url()
|
||||
print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('edit_item'), {'id': descriptor.location.url()})
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_edit_item_toy(self):
|
||||
self.check_edit_item('toy')
|
||||
def test_edit_unit_toy(self):
|
||||
self.check_edit_unit('toy')
|
||||
|
||||
def test_edit_item_full(self):
|
||||
self.check_edit_item('full')
|
||||
def test_edit_unit_full(self):
|
||||
self.check_edit_unit('full')
|
||||
|
||||
@@ -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):
|
||||
'''
|
||||
@@ -32,16 +35,42 @@ def get_course_location_for_item(location):
|
||||
return location
|
||||
|
||||
|
||||
def get_lms_link_for_item(item):
|
||||
def get_lms_link_for_item(location):
|
||||
location = Location(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
|
||||
|
||||
return lms_link
|
||||
|
||||
|
||||
class UnitState(object):
|
||||
draft = 'draft'
|
||||
private = 'private'
|
||||
public = 'public'
|
||||
|
||||
|
||||
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.metadata.get('is_draft', False):
|
||||
try:
|
||||
modulestore('direct').get_item(unit.location)
|
||||
return UnitState.draft
|
||||
except ItemNotFoundError:
|
||||
return UnitState.private
|
||||
else:
|
||||
return UnitState.public
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,7 +44,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
|
||||
|
||||
@@ -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':
|
||||
@@ -168,6 +169,7 @@ def edit_subsection(request, location):
|
||||
'lms_link': lms_link
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_unit(request, location):
|
||||
"""
|
||||
@@ -183,7 +185,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)
|
||||
|
||||
@@ -210,14 +213,25 @@ 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)
|
||||
|
||||
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': 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')
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'unit_state': unit_state,
|
||||
'published_date': published_date,
|
||||
})
|
||||
|
||||
|
||||
@@ -235,7 +249,6 @@ def preview_component(request, location):
|
||||
})
|
||||
|
||||
|
||||
|
||||
def user_author_string(user):
|
||||
'''Get an author string for commits by this user. Format:
|
||||
first last <email@email.com>.
|
||||
@@ -404,6 +417,13 @@ def get_module_previews(request, descriptor):
|
||||
preview_html.append(module.get_html())
|
||||
return preview_html
|
||||
|
||||
|
||||
def _xmodule_recurse(item, action):
|
||||
for child in item.get_children():
|
||||
_xmodule_recurse(child, action)
|
||||
|
||||
action(item)
|
||||
|
||||
def _delete_item(item, recurse=False):
|
||||
if recurse:
|
||||
children = item.get_children()
|
||||
@@ -427,8 +447,11 @@ def delete_item(request):
|
||||
|
||||
item = modulestore().get_item(item_location)
|
||||
|
||||
_delete_item(item, delete_children)
|
||||
|
||||
if delete_children:
|
||||
_xmodule_recurse(item, lambda i: modulestore().delete_item(i.location))
|
||||
else:
|
||||
modulestore().delete_item(item.location)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@@ -479,6 +502,51 @@ 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()
|
||||
|
||||
item = modulestore().get_item(location)
|
||||
_xmodule_recurse(item, lambda i: modulestore().publish(i.location, request.user.id))
|
||||
|
||||
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()
|
||||
|
||||
item = modulestore().get_item(location)
|
||||
_xmodule_recurse(item, lambda i: modulestore().unpublish(i.location))
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
def clone_item(request):
|
||||
@@ -503,7 +571,12 @@ def clone_item(request):
|
||||
new_item.metadata['display_name'] = display_name
|
||||
|
||||
modulestore().update_metadata(new_item.location.url(), new_item.own_metadata)
|
||||
modulestore().update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
|
||||
|
||||
if parent_location.category not in ('vertical',):
|
||||
parent_update_modulestore = modulestore('direct')
|
||||
else:
|
||||
parent_update_modulestore = modulestore()
|
||||
parent_update_modulestore.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
|
||||
|
||||
return HttpResponse(json.dumps({'id': dest_location.url()}))
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,17 +38,23 @@ STATICFILES_DIRS += [
|
||||
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir)
|
||||
]
|
||||
|
||||
modulestore_options = {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': GITHUB_REPO_ROOT,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
|
||||
MODULESTORE = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': GITHUB_REPO_ROOT,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
}
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
'direct': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,9 +5,35 @@ 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'
|
||||
'change #visibility': 'setVisibility'
|
||||
|
||||
initialize: =>
|
||||
@visibilityView = new CMS.Views.UnitEdit.Visibility(
|
||||
el: @$('#visibility')
|
||||
model: @model
|
||||
)
|
||||
|
||||
@saveView = new CMS.Views.UnitEdit.SaveDraftButton(
|
||||
el: @$('#save-draft')
|
||||
model: @model
|
||||
)
|
||||
|
||||
@locationView = new CMS.Views.UnitEdit.LocationState(
|
||||
el: @$('.section-item.editing a')
|
||||
model: @model
|
||||
)
|
||||
|
||||
@nameView = new CMS.Views.UnitEdit.NameEdit(
|
||||
el: @$('.unit-name-input')
|
||||
model: @model
|
||||
)
|
||||
|
||||
@model.on('change:state', @render)
|
||||
|
||||
@$newComponentItem = @$('.new-component-item')
|
||||
@$newComponentTypePicker = @$('.new-component')
|
||||
@$newComponentTemplatePickers = @$('.new-component-templates')
|
||||
@@ -15,7 +41,13 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
|
||||
@$('.components').sortable(
|
||||
handle: '.drag-handle'
|
||||
update: (event, ui) => @saveOrder()
|
||||
update: (event, ui) => @model.set('children', @components())
|
||||
helper: 'clone'
|
||||
opacity: '0.5'
|
||||
placeholder: 'component-placeholder'
|
||||
forcePlaceholderSize: true
|
||||
axis: 'y'
|
||||
items: '> .component'
|
||||
)
|
||||
|
||||
@$('.component').each((idx, element) =>
|
||||
@@ -26,10 +58,10 @@ 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()
|
||||
@$newComponentItem.addClass('adding')
|
||||
@@ -56,21 +88,31 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
event.preventDefault()
|
||||
|
||||
editor = new CMS.Views.ModuleEdit(
|
||||
onDelete: @deleteComponent
|
||||
model: new CMS.Models.Module()
|
||||
)
|
||||
|
||||
@$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: =>
|
||||
@model.save(
|
||||
children: @components()
|
||||
)
|
||||
wait: (value) =>
|
||||
@$('.unit-body').toggleClass("waiting", value)
|
||||
|
||||
render: =>
|
||||
if @model.hasChanged('state')
|
||||
@$el.toggleClass("edit-state-#{@model.previous('state')} edit-state-#{@model.get('state')}")
|
||||
@wait(false)
|
||||
|
||||
saveDraft: =>
|
||||
@model.save()
|
||||
|
||||
deleteComponent: (event) =>
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
@@ -78,6 +120,94 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
$component.remove()
|
||||
@saveOrder()
|
||||
@model.set('children', @components())
|
||||
)
|
||||
|
||||
deleteDraft: (event) ->
|
||||
@wait(true)
|
||||
|
||||
$.post('/delete_item', {
|
||||
id: @$el.data('id')
|
||||
delete_children: true
|
||||
}, =>
|
||||
window.location.reload()
|
||||
)
|
||||
|
||||
createDraft: (event) ->
|
||||
@wait(true)
|
||||
|
||||
$.post('/create_draft', {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
@model.set('state', 'draft')
|
||||
)
|
||||
|
||||
publishDraft: (event) ->
|
||||
@wait(true)
|
||||
@saveDraft()
|
||||
|
||||
$.post('/publish_draft', {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
@model.set('state', 'public')
|
||||
)
|
||||
|
||||
setVisibility: (event) ->
|
||||
if @$('#visibility').val() == 'private'
|
||||
target_url = '/unpublish_unit'
|
||||
else
|
||||
target_url = '/publish_draft'
|
||||
|
||||
@wait(true)
|
||||
|
||||
$.post(target_url, {
|
||||
id: @$el.data('id')
|
||||
}, =>
|
||||
@model.set('state', @$('#visibility').val())
|
||||
)
|
||||
|
||||
class CMS.Views.UnitEdit.NameEdit extends Backbone.View
|
||||
events:
|
||||
"keyup .unit-display-name-input": "saveName"
|
||||
|
||||
initialize: =>
|
||||
@model.on('change:metadata', @render)
|
||||
@saveName
|
||||
|
||||
render: =>
|
||||
@$('.unit-display-name-input').val(@model.get('metadata').display_name)
|
||||
|
||||
saveName: =>
|
||||
# Treat the metadata dictionary as immutable
|
||||
metadata = $.extend({}, @model.get('metadata'))
|
||||
metadata.display_name = @$('.unit-display-name-input').val()
|
||||
@model.set('metadata', metadata)
|
||||
|
||||
class CMS.Views.UnitEdit.LocationState extends Backbone.View
|
||||
initialize: =>
|
||||
@model.on('change:state', @render)
|
||||
|
||||
render: =>
|
||||
@$el.toggleClass("#{@model.previous('state')}-item #{@model.get('state')}-item")
|
||||
|
||||
class CMS.Views.UnitEdit.Visibility extends Backbone.View
|
||||
initialize: =>
|
||||
@model.on('change:state', @render)
|
||||
@render()
|
||||
|
||||
render: =>
|
||||
@$el.val(@model.get('state'))
|
||||
|
||||
class CMS.Views.UnitEdit.SaveDraftButton extends Backbone.View
|
||||
initialize: =>
|
||||
@model.on('change:children', @enable)
|
||||
@model.on('change:metadata', @enable)
|
||||
@model.on('sync', @disable)
|
||||
|
||||
@disable()
|
||||
|
||||
disable: =>
|
||||
@$el.addClass('disabled')
|
||||
|
||||
enable: =>
|
||||
@$el.removeClass('disabled')
|
||||
@@ -29,6 +29,10 @@ h1 {
|
||||
margin: 36px 6px;
|
||||
}
|
||||
|
||||
.waiting {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
float: right;
|
||||
margin-top: 42px;
|
||||
|
||||
@@ -16,6 +16,18 @@
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 0 0 rgba(0, 0, 0, 0));
|
||||
@include transition(background-color .15s, box-shadow .15s);
|
||||
|
||||
&.disabled {
|
||||
border: 1px solid $lightGrey !important;
|
||||
border-radius: 3px !important;
|
||||
background: $lightGrey !important;
|
||||
color: $darkGrey !important;
|
||||
pointer-events: none;
|
||||
cursor: none;
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15));
|
||||
}
|
||||
@@ -161,13 +173,33 @@
|
||||
background: #fffcf1;
|
||||
}
|
||||
|
||||
.draft-item,
|
||||
.hidden-item,
|
||||
.draft-item:after,
|
||||
.public-item:after,
|
||||
.private-item:after {
|
||||
margin-left: 3px;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.draft-item:after {
|
||||
content: "- draft";
|
||||
}
|
||||
|
||||
.public-item:after {
|
||||
content: "- public";
|
||||
}
|
||||
|
||||
.private-item:after {
|
||||
content: "- private";
|
||||
}
|
||||
|
||||
.public-item,
|
||||
.private-item {
|
||||
color: #a4aab7;
|
||||
}
|
||||
|
||||
.has-new-draft-item {
|
||||
.draft-item {
|
||||
color: #9f7d10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +117,8 @@
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
.public-tag,
|
||||
.private-tag {
|
||||
margin-left: 3px;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
@@ -127,7 +126,7 @@
|
||||
color: #a4aab7;
|
||||
}
|
||||
|
||||
.has-new-draft-tag {
|
||||
.draft-tag {
|
||||
color: #9f7d10;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.rendered-component {
|
||||
&.component-placeholder {
|
||||
border-color: #6696d7;
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 40px 20px 20px;
|
||||
}
|
||||
|
||||
@@ -394,3 +398,37 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#create-draft {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
#save-draft,
|
||||
#delete-draft,
|
||||
#publish-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
#published-alert {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
#delete-draft,
|
||||
#publish-draft,
|
||||
#published-alert,
|
||||
#create-draft, {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,27 @@
|
||||
new CMS.Views.UnitEdit({
|
||||
el: $('.main-wrapper'),
|
||||
model: new CMS.Models.Module({
|
||||
id: '${unit.location.url()}'
|
||||
id: '${unit_location}',
|
||||
state: '${unit_state}'
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
<%block name="content">
|
||||
<div class="main-wrapper">
|
||||
<div class="main-wrapper edit-state-${unit_state}" data-id="${unit_location}">
|
||||
<div class="inner-wrapper">
|
||||
<div class="alert" id="published-alert">
|
||||
<p class="alert-message"><strong>You are editing a draft.</strong>
|
||||
% if published_date:
|
||||
This unit was originally published on ${published_date}.
|
||||
% endif
|
||||
</p>
|
||||
<a href="${published_preview_link}" target="_blank" class="alert-action secondary">Preview the published version</a>
|
||||
</div>
|
||||
<div class="main-column">
|
||||
<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>
|
||||
<ol class="components" data-id="${unit.location.url()}">
|
||||
<ol class="components">
|
||||
% for id in components:
|
||||
<li class="component" data-id="${id}"/>
|
||||
% endfor
|
||||
@@ -60,31 +69,26 @@
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="sidebar wip-box">
|
||||
<div class="sidebar">
|
||||
<div class="unit-properties window">
|
||||
<h4>Unit Properties</h4>
|
||||
<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">
|
||||
<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>
|
||||
<a id="publish-draft" href="#">This unit has already been published. Click here to release your changes to it</a>
|
||||
<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>${subsection.start}</strong> with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p>
|
||||
</div>
|
||||
<div class="row unit-actions">
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="${lms_link}" target="_blank" class="preview-button">Preview</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="${draft_preview_link}" target="_blank" class="preview-button">Preview</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,4 +118,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</%block>
|
||||
</%block>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%! from contentstore.utils import compute_unit_state %>
|
||||
|
||||
<!--
|
||||
This def will enumerate through a passed in subsection and list all of the units
|
||||
@@ -8,16 +9,16 @@ This def will enumerate through a passed in subsection and list all of the units
|
||||
% for unit in subsection.get_children():
|
||||
<li class="leaf unit" data-id="${unit.location}">
|
||||
<%
|
||||
unit_state = compute_unit_state(unit)
|
||||
if unit.location == selected:
|
||||
selected_class = 'editing'
|
||||
else:
|
||||
selected_class = ''
|
||||
%>
|
||||
<div class="section-item ${selected_class}">
|
||||
<a href="${reverse('edit_unit', args=[unit.location])}" class="private-item">
|
||||
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
|
||||
<span class="${unit.category}-icon"></span>
|
||||
${unit.display_name}
|
||||
<span class="private-tag wip">- private</span>
|
||||
</a>
|
||||
% if actions:
|
||||
<div class="item-actions">
|
||||
|
||||
@@ -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()
|
||||
@@ -15,12 +13,15 @@ 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'^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>.*?)$',
|
||||
@@ -54,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)
|
||||
|
||||
@@ -2,6 +2,8 @@ class @HTMLEditingDescriptor
|
||||
constructor: (@element) ->
|
||||
@edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
|
||||
mode: "text/html"
|
||||
lineNumbers: true
|
||||
lineWrapping: true
|
||||
})
|
||||
|
||||
save: ->
|
||||
|
||||
@@ -2,6 +2,8 @@ class @JSONEditingDescriptor extends XModule.Descriptor
|
||||
constructor: (@element) ->
|
||||
@edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
|
||||
mode: { name: "javascript", json: true }
|
||||
lineNumbers: true
|
||||
lineWrapping: true
|
||||
})
|
||||
|
||||
save: ->
|
||||
|
||||
@@ -2,6 +2,8 @@ class @XMLEditingDescriptor extends XModule.Descriptor
|
||||
constructor: (@element) ->
|
||||
@edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
|
||||
mode: "xml"
|
||||
lineNumbers: true
|
||||
lineWrapping: true
|
||||
})
|
||||
|
||||
save: ->
|
||||
|
||||
@@ -240,11 +240,15 @@ class ModuleStore(object):
|
||||
An abstract interface for a database backend that stores XModuleDescriptor
|
||||
instances
|
||||
"""
|
||||
def has_item(self, location):
|
||||
"""
|
||||
Returns True if location exists in this ModuleStore.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
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
|
||||
@@ -345,7 +349,8 @@ class ModuleStore(object):
|
||||
Returns a list containing the top level XModuleDescriptors of the courses
|
||||
in this modulestore.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
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
|
||||
|
||||
189
common/lib/xmodule/xmodule/modulestore/draft.py
Normal file
189
common/lib/xmodule/xmodule/modulestore/draft.py
Normal file
@@ -0,0 +1,189 @@
|
||||
from datetime import datetime
|
||||
|
||||
from . import ModuleStoreBase, Location
|
||||
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.
|
||||
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 wrap_draft(super(DraftModuleStore, self).get_item(as_draft(location), depth))
|
||||
except ItemNotFoundError:
|
||||
return wrap_draft(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 wrap_draft(super(DraftModuleStore, self).get_instance(course_id, as_draft(location)))
|
||||
except ItemNotFoundError:
|
||||
return wrap_draft(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 = as_draft(location)
|
||||
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 [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 wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location)))
|
||||
|
||||
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
|
||||
"""
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
if not draft_item.metadata['is_draft']:
|
||||
self.clone_item(location, draft_loc)
|
||||
|
||||
return super(DraftModuleStore, self).update_item(draft_loc, 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
|
||||
"""
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
if not draft_item.metadata['is_draft']:
|
||||
self.clone_item(location, draft_loc)
|
||||
|
||||
return super(DraftModuleStore, self).update_children(draft_loc, 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
|
||||
"""
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
|
||||
if not draft_item.metadata['is_draft']:
|
||||
self.clone_item(location, draft_loc)
|
||||
|
||||
if 'is_draft' in metadata:
|
||||
del metadata['is_draft']
|
||||
|
||||
return super(DraftModuleStore, self).update_metadata(draft_loc, 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(as_draft(location))
|
||||
|
||||
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)
|
||||
|
||||
def publish(self, location, published_by_id):
|
||||
"""
|
||||
Save a current draft to the underlying modulestore
|
||||
"""
|
||||
draft = self.get_item(location)
|
||||
metadata = {}
|
||||
metadata.update(draft.metadata)
|
||||
metadata['published_date'] = tuple(datetime.utcnow().timetuple())
|
||||
metadata['published_by'] = published_by_id
|
||||
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, 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -69,17 +70,21 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
|
||||
)
|
||||
|
||||
|
||||
def location_to_query(location):
|
||||
def location_to_query(location, wildcard=True):
|
||||
"""
|
||||
Takes a Location and returns a SON object that will query for that location.
|
||||
Fields in location that are None are ignored in the query
|
||||
|
||||
If `wildcard` is True, then a None in a location is treated as a wildcard
|
||||
query. Otherwise, it is searched for literally
|
||||
"""
|
||||
query = SON()
|
||||
# Location dict is ordered by specificity, and SON
|
||||
# will preserve that order for queries
|
||||
for key, val in Location(location).dict().iteritems():
|
||||
if val is not None:
|
||||
query['_id.{key}'.format(key=key)] = val
|
||||
if wildcard and val is None:
|
||||
continue
|
||||
query['_id.{key}'.format(key=key)] = val
|
||||
|
||||
return query
|
||||
|
||||
@@ -202,18 +207,27 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
ItemNotFoundError.
|
||||
'''
|
||||
item = self.collection.find_one(
|
||||
location_to_query(location),
|
||||
location_to_query(location, wildcard=False),
|
||||
sort=[('revision', pymongo.ASCENDING)],
|
||||
)
|
||||
if item is None:
|
||||
raise ItemNotFoundError(location)
|
||||
return item
|
||||
|
||||
def has_item(self, location):
|
||||
"""
|
||||
Returns True if location exists in this ModuleStore.
|
||||
"""
|
||||
location = Location.ensure_fully_specified(location)
|
||||
try:
|
||||
self._find_one(location)
|
||||
return True
|
||||
except ItemNotFoundError:
|
||||
return False
|
||||
|
||||
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
|
||||
@@ -321,16 +335,10 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
|
||||
If there is no data at location in this modulestore, raise
|
||||
ItemNotFoundError.
|
||||
|
||||
returns an iterable of things that can be passed to Location. This may
|
||||
be empty if there are no parents.
|
||||
'''
|
||||
location = Location.ensure_fully_specified(location)
|
||||
# Check that it's actually in this modulestore.
|
||||
self._find_one(location)
|
||||
# now get the parents
|
||||
items = self.collection.find({'definition.children': location.url()},
|
||||
{'_id': True})
|
||||
return [i['_id'] for i in items]
|
||||
@@ -341,3 +349,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
|
||||
|
||||
@@ -60,10 +60,8 @@ def path_to_location(modulestore, course_id, location):
|
||||
(loc, path) = queue.pop() # Takes from the end
|
||||
loc = Location(loc)
|
||||
|
||||
# get_parent_locations should raise ItemNotFoundError if location
|
||||
# isn't found so we don't have to do it explicitly. Call this
|
||||
# first to make sure the location is there (even if it's a course, and
|
||||
# we would otherwise immediately exit).
|
||||
# Call get_parent_locations first to make sure the location is there
|
||||
# (even if it's a course, and we would otherwise immediately exit).
|
||||
parents = modulestore.get_parent_locations(loc)
|
||||
|
||||
# print 'Processing loc={0}, path={1}'.format(loc, path)
|
||||
@@ -81,6 +79,9 @@ def path_to_location(modulestore, course_id, location):
|
||||
# If we're here, there is no path
|
||||
return None
|
||||
|
||||
if not modulestore.has_item(location):
|
||||
raise ItemNotFoundError
|
||||
|
||||
path = find_path_to_course()
|
||||
if path is None:
|
||||
raise NoPathToItem(location)
|
||||
|
||||
@@ -477,11 +477,16 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
except KeyError:
|
||||
raise ItemNotFoundError(location)
|
||||
|
||||
def has_item(self, location):
|
||||
"""
|
||||
Returns True if location exists in this ModuleStore.
|
||||
"""
|
||||
location = Location(location)
|
||||
return any(location in course_modules for course_modules in self.modules.values())
|
||||
|
||||
def get_item(self, location, depth=0):
|
||||
"""
|
||||
Returns an XModuleDescriptor instance for the item at location.
|
||||
If location.revision is None, returns the most item with the most
|
||||
recent revision
|
||||
|
||||
If any segment of the location is None except revision, raises
|
||||
xmodule.modulestore.exceptions.InsufficientSpecificationError
|
||||
@@ -545,14 +550,8 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
|
||||
If there is no data at location in this modulestore, raise
|
||||
ItemNotFoundError.
|
||||
|
||||
returns an iterable of things that can be passed to Location. This may
|
||||
be empty if there are no parents.
|
||||
'''
|
||||
location = Location.ensure_fully_specified(location)
|
||||
if not self.parent_tracker.is_known(location):
|
||||
raise ItemNotFoundError(location)
|
||||
|
||||
return self.parent_tracker.parents(location)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -409,7 +409,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
|
||||
|
||||
# cdodge: this is a list of metadata names which are 'system' metadata
|
||||
# and should not be edited by an end-user
|
||||
system_metadata_fields = [ 'data_dir' ]
|
||||
system_metadata_fields = ['data_dir', 'published_date', 'published_by']
|
||||
|
||||
# A list of descriptor attributes that must be equal for the descriptors to
|
||||
# be equal
|
||||
|
||||
@@ -57,7 +57,7 @@ def mongo_store_config(data_dir):
|
||||
'OPTIONS': {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'xmodule',
|
||||
'db': 'test_xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': data_dir,
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string',
|
||||
|
||||
@@ -4,16 +4,18 @@ 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.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
|
||||
},
|
||||
}
|
||||
|
||||
12
lms/envs/cms/preview_dev.py
Normal file
12
lms/envs/cms/preview_dev.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Settings for the LMS that runs alongside the CMS on AWS
|
||||
"""
|
||||
|
||||
from .dev import *
|
||||
|
||||
MODULESTORE = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user