Commit a9560cdf by Calen Pennington

Merge pull request #834 from MITx/feature/cale/cas-draft-mode

CAS Draft Mode
parents 6079c571 9ea3039b
...@@ -26,4 +26,4 @@ class Command(BaseCommand): ...@@ -26,4 +26,4 @@ class Command(BaseCommand):
print "Importing. Data_dir={data}, course_dirs={courses}".format( print "Importing. Data_dir={data}, course_dirs={courses}".format(
data=data_dir, data=data_dir,
courses=course_dirs) 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): ...@@ -141,8 +141,6 @@ class AuthTestCase(ContentStoreTestCase):
"""Make sure pages that do require login work.""" """Make sure pages that do require login work."""
auth_pages = ( auth_pages = (
reverse('index'), reverse('index'),
reverse('edit_item'),
reverse('save_item'),
) )
# These are pages that should just load when the user is logged in # These are pages that should just load when the user is logged in
...@@ -181,6 +179,7 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -181,6 +179,7 @@ class AuthTestCase(ContentStoreTestCase):
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE) TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data') 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) @override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
class EditTestCase(ContentStoreTestCase): class EditTestCase(ContentStoreTestCase):
...@@ -195,17 +194,17 @@ class EditTestCase(ContentStoreTestCase): ...@@ -195,17 +194,17 @@ class EditTestCase(ContentStoreTestCase):
xmodule.modulestore.django._MODULESTORES = {} xmodule.modulestore.django._MODULESTORES = {}
xmodule.modulestore.django.modulestore().collection.drop() 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]) 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 "Checking ", descriptor.location.url()
print descriptor.__class__, descriptor.location 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) self.assertEqual(resp.status_code, 200)
def test_edit_item_toy(self): def test_edit_unit_toy(self):
self.check_edit_item('toy') self.check_edit_unit('toy')
def test_edit_item_full(self): def test_edit_unit_full(self):
self.check_edit_item('full') self.check_edit_unit('full')
from django.conf import settings from django.conf import settings
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore 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): def get_course_location_for_item(location):
''' '''
...@@ -32,16 +35,42 @@ def get_course_location_for_item(location): ...@@ -32,16 +35,42 @@ def get_course_location_for_item(location):
return 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: if settings.LMS_BASE is not None:
lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format( lms_link = "{lms_base}/courses/{course_id}/jump_to/{location}".format(
lms_base=settings.LMS_BASE, 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 # 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, course_id = modulestore().get_containing_courses(location)[0].id,
location=item.location, location=location,
) )
else: else:
lms_link = None lms_link = None
return lms_link 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
from util.json_request import expect_json from util.json_request import expect_json
import exceptions
import json import json
import os
import logging import logging
import sys
import mimetypes import mimetypes
import os
import StringIO import StringIO
import exceptions import sys
import time
from collections import defaultdict from collections import defaultdict
from uuid import uuid4 from uuid import uuid4
...@@ -43,7 +44,7 @@ from cache_toolbox.core import set_cached_content, get_cached_content, del_cache ...@@ -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 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 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 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 from xmodule.templates import all_templates
...@@ -154,7 +155,7 @@ def edit_subsection(request, location): ...@@ -154,7 +155,7 @@ def edit_subsection(request, location):
item = modulestore().get_item(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 # make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential': if item.location.category != 'sequential':
...@@ -168,6 +169,7 @@ def edit_subsection(request, location): ...@@ -168,6 +169,7 @@ def edit_subsection(request, location):
'lms_link': lms_link 'lms_link': lms_link
}) })
@login_required @login_required
def edit_unit(request, location): def edit_unit(request, location):
""" """
...@@ -183,7 +185,8 @@ def edit_unit(request, location): ...@@ -183,7 +185,8 @@ def edit_unit(request, location):
item = modulestore().get_item(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) component_templates = defaultdict(list)
...@@ -210,14 +213,25 @@ def edit_unit(request, location): ...@@ -210,14 +213,25 @@ def edit_unit(request, location):
containing_section_locs = modulestore().get_parent_locations(containing_subsection.location) containing_section_locs = modulestore().get_parent_locations(containing_subsection.location)
containing_section = modulestore().get_item(containing_section_locs[0]) 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', { return render_to_response('unit.html', {
'unit': item, 'unit': item,
'unit_location': location,
'components': components, 'components': components,
'component_templates': component_templates, 'component_templates': component_templates,
'lms_link': lms_link, 'draft_preview_link': lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection, 'subsection': containing_subsection,
'section': containing_section, '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): ...@@ -235,7 +249,6 @@ def preview_component(request, location):
}) })
def user_author_string(user): def user_author_string(user):
'''Get an author string for commits by this user. Format: '''Get an author string for commits by this user. Format:
first last <email@email.com>. first last <email@email.com>.
...@@ -404,6 +417,13 @@ def get_module_previews(request, descriptor): ...@@ -404,6 +417,13 @@ def get_module_previews(request, descriptor):
preview_html.append(module.get_html()) preview_html.append(module.get_html())
return preview_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): def _delete_item(item, recurse=False):
if recurse: if recurse:
children = item.get_children() children = item.get_children()
...@@ -427,8 +447,11 @@ def delete_item(request): ...@@ -427,8 +447,11 @@ def delete_item(request):
item = modulestore().get_item(item_location) 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() return HttpResponse()
...@@ -481,6 +504,51 @@ def save_item(request): ...@@ -481,6 +504,51 @@ def save_item(request):
@login_required @login_required
@expect_json @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): def clone_item(request):
parent_location = Location(request.POST['parent_location']) parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template']) template = Location(request.POST['template'])
...@@ -503,7 +571,12 @@ def clone_item(request): ...@@ -503,7 +571,12 @@ def clone_item(request):
new_item.metadata['display_name'] = display_name new_item.metadata['display_name'] = display_name
modulestore().update_metadata(new_item.location.url(), new_item.own_metadata) 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()})) return HttpResponse(json.dumps({'id': dest_location.url()}))
......
...@@ -14,17 +14,23 @@ LOGGING = get_logger_config(ENV_ROOT / "log", ...@@ -14,17 +14,23 @@ LOGGING = get_logger_config(ENV_ROOT / "log",
tracking_filename="tracking.log", tracking_filename="tracking.log",
debug=True) 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 = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': { 'OPTIONS': 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',
}
} }
} }
......
...@@ -38,17 +38,23 @@ STATICFILES_DIRS += [ ...@@ -38,17 +38,23 @@ STATICFILES_DIRS += [
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir) 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 = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': { 'OPTIONS': modulestore_options
'default_class': 'xmodule.raw_module.RawDescriptor', },
'host': 'localhost', 'direct': {
'db': 'test_xmodule', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'collection': 'modulestore', 'OPTIONS': modulestore_options
'fs_root': GITHUB_REPO_ROOT,
'render_template': 'mitxmako.shortcuts.render_to_string',
}
} }
} }
......
...@@ -35,9 +35,9 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -35,9 +35,9 @@ class CMS.Views.ModuleEdit extends Backbone.View
return _metadata return _metadata
cloneTemplate: (template) -> cloneTemplate: (parent, template) ->
$.post("/clone_item", { $.post("/clone_item", {
parent_location: @$el.parent().data('id') parent_location: parent
template: template template: template
}, (data) => }, (data) =>
@model.set(id: data.id) @model.set(id: data.id)
......
...@@ -5,9 +5,35 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -5,9 +5,35 @@ class CMS.Views.UnitEdit extends Backbone.View
'click .new-component-templates .new-component-template a': 'saveNewComponent' 'click .new-component-templates .new-component-template a': 'saveNewComponent'
'click .new-component-templates .cancel-button': 'closeNewComponent' 'click .new-component-templates .cancel-button': 'closeNewComponent'
'click .new-component-button': 'showNewComponentForm' '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: => 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') @$newComponentItem = @$('.new-component-item')
@$newComponentTypePicker = @$('.new-component') @$newComponentTypePicker = @$('.new-component')
@$newComponentTemplatePickers = @$('.new-component-templates') @$newComponentTemplatePickers = @$('.new-component-templates')
...@@ -15,7 +41,13 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -15,7 +41,13 @@ class CMS.Views.UnitEdit extends Backbone.View
@$('.components').sortable( @$('.components').sortable(
handle: '.drag-handle' 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) => @$('.component').each((idx, element) =>
...@@ -26,10 +58,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -26,10 +58,10 @@ 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
showNewComponentForm: (event) => showNewComponentForm: (event) =>
event.preventDefault() event.preventDefault()
@$newComponentItem.addClass('adding') @$newComponentItem.addClass('adding')
...@@ -56,21 +88,31 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -56,21 +88,31 @@ class CMS.Views.UnitEdit extends Backbone.View
event.preventDefault() event.preventDefault()
editor = new CMS.Views.ModuleEdit( editor = new CMS.Views.ModuleEdit(
onDelete: @deleteComponent
model: new CMS.Models.Module() model: new CMS.Models.Module()
) )
@$newComponentItem.before(editor.$el) @$newComponentItem.before(editor.$el)
editor.cloneTemplate($(event.currentTarget).data('location')) editor.cloneTemplate(
@$el.data('id'),
$(event.currentTarget).data('location')
)
@closeNewComponent(event) @closeNewComponent(event)
components: => @$('.component').map((idx, el) -> $(el).data('id')).get() components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
saveOrder: => wait: (value) =>
@model.save( @$('.unit-body').toggleClass("waiting", value)
children: @components()
) render: =>
if @model.hasChanged('state')
@$el.toggleClass("edit-state-#{@model.previous('state')} edit-state-#{@model.get('state')}")
@wait(false)
saveDraft: =>
@model.save()
deleteComponent: (event) => deleteComponent: (event) =>
$component = $(event.currentTarget).parents('.component') $component = $(event.currentTarget).parents('.component')
...@@ -78,6 +120,94 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -78,6 +120,94 @@ class CMS.Views.UnitEdit extends Backbone.View
id: $component.data('id') id: $component.data('id')
}, => }, =>
$component.remove() $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')
\ No newline at end of file
...@@ -29,6 +29,10 @@ h1 { ...@@ -29,6 +29,10 @@ h1 {
margin: 36px 6px; margin: 36px 6px;
} }
.waiting {
opacity: 0.1;
}
.page-actions { .page-actions {
float: right; float: right;
margin-top: 42px; margin-top: 42px;
......
...@@ -16,6 +16,18 @@ ...@@ -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 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); @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 { &:hover {
@include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15)); @include box-shadow(0 1px 0 rgba(255, 255, 255, .3) inset, 0 1px 1px rgba(0, 0, 0, .15));
} }
...@@ -161,13 +173,33 @@ ...@@ -161,13 +173,33 @@
background: #fffcf1; background: #fffcf1;
} }
.draft-item, .draft-item:after,
.hidden-item, .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 { .private-item {
color: #a4aab7; color: #a4aab7;
} }
.has-new-draft-item { .draft-item {
color: #9f7d10; color: #9f7d10;
} }
} }
......
...@@ -117,9 +117,8 @@ ...@@ -117,9 +117,8 @@
} }
.draft-tag, .draft-tag,
.hidden-tag, .public-tag,
.private-tag, .private-tag {
.has-new-draft-tag {
margin-left: 3px; margin-left: 3px;
font-size: 9px; font-size: 9px;
font-weight: 600; font-weight: 600;
...@@ -127,7 +126,7 @@ ...@@ -127,7 +126,7 @@
color: #a4aab7; color: #a4aab7;
} }
.has-new-draft-tag { .draft-tag {
color: #9f7d10; color: #9f7d10;
} }
......
...@@ -87,7 +87,11 @@ ...@@ -87,7 +87,11 @@
} }
} }
.rendered-component { &.component-placeholder {
border-color: #6696d7;
}
.xmodule_display {
padding: 40px 20px 20px; padding: 40px 20px 20px;
} }
...@@ -394,3 +398,37 @@ ...@@ -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 @@ ...@@ -8,18 +8,27 @@
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.url()}' id: '${unit_location}',
state: '${unit_state}'
}) })
}); });
</script> </script>
</%block> </%block>
<%block name="content"> <%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="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"> <div class="main-column">
<article class="unit-body window"> <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> <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: % for id in components:
<li class="component" data-id="${id}"/> <li class="component" data-id="${id}"/>
% endfor % endfor
...@@ -60,31 +69,26 @@ ...@@ -60,31 +69,26 @@
</article> </article>
</div> </div>
<div class="sidebar wip-box"> <div class="sidebar">
<div class="unit-properties window"> <div class="unit-properties window">
<h4>Unit Properties</h4> <h4>Unit Properties</h4>
<div class="window-contents"> <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"> <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="publish-draft" href="#">This unit has already been published. Click here to release your changes to it</a>
<div class="row status"> <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>
<div class="row unit-actions"> <div class="row unit-actions">
<a href="#" class="save-button">Save</a> <a id="save-draft" href="#" class="save-button">Save Draft</a>
<a href="${lms_link}" target="_blank" class="preview-button">Preview</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> </div>
</div> </div>
...@@ -114,4 +118,4 @@ ...@@ -114,4 +118,4 @@
</div> </div>
</div> </div>
</%block> </%block>
\ No newline at end of file
<%! from django.core.urlresolvers import reverse %> <%! 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 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 ...@@ -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(): % for unit in subsection.get_children():
<li class="leaf unit" data-id="${unit.location}"> <li class="leaf unit" data-id="${unit.location}">
<% <%
unit_state = compute_unit_state(unit)
if unit.location == selected: if unit.location == selected:
selected_class = 'editing' selected_class = 'editing'
else: else:
selected_class = '' selected_class = ''
%> %>
<div class="section-item ${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> <span class="${unit.category}-icon"></span>
${unit.display_name} ${unit.display_name}
<span class="private-tag wip">- private</span>
</a> </a>
% if actions: % if actions:
<div class="item-actions"> <div class="item-actions">
......
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()
...@@ -15,12 +13,15 @@ urlpatterns = ('', ...@@ -15,12 +13,15 @@ urlpatterns = ('',
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
url(r'^delete_item$', 'contentstore.views.delete_item', name='delete_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'^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>[^/]+)$', 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'),
url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$', url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
'contentstore.views.preview_dispatch', name='preview_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'), 'contentstore.views.upload_asset', name='upload_asset'),
url(r'^manage_users/(?P<location>.*?)$', 'contentstore.views.manage_users', name='manage_users'), url(r'^manage_users/(?P<location>.*?)$', 'contentstore.views.manage_users', name='manage_users'),
url(r'^add_user/(?P<location>.*?)$', url(r'^add_user/(?P<location>.*?)$',
...@@ -54,6 +55,6 @@ urlpatterns += ( ...@@ -54,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)
...@@ -2,6 +2,8 @@ class @HTMLEditingDescriptor ...@@ -2,6 +2,8 @@ class @HTMLEditingDescriptor
constructor: (@element) -> constructor: (@element) ->
@edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], { @edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
mode: "text/html" mode: "text/html"
lineNumbers: true
lineWrapping: true
}) })
save: -> save: ->
......
...@@ -2,6 +2,8 @@ class @JSONEditingDescriptor extends XModule.Descriptor ...@@ -2,6 +2,8 @@ class @JSONEditingDescriptor extends XModule.Descriptor
constructor: (@element) -> constructor: (@element) ->
@edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], { @edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
mode: { name: "javascript", json: true } mode: { name: "javascript", json: true }
lineNumbers: true
lineWrapping: true
}) })
save: -> save: ->
......
...@@ -2,6 +2,8 @@ class @XMLEditingDescriptor extends XModule.Descriptor ...@@ -2,6 +2,8 @@ class @XMLEditingDescriptor extends XModule.Descriptor
constructor: (@element) -> constructor: (@element) ->
@edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], { @edit_box = CodeMirror.fromTextArea($(".edit-box", @element)[0], {
mode: "xml" mode: "xml"
lineNumbers: true
lineWrapping: true
}) })
save: -> save: ->
......
...@@ -240,11 +240,15 @@ class ModuleStore(object): ...@@ -240,11 +240,15 @@ class ModuleStore(object):
An abstract interface for a database backend that stores XModuleDescriptor An abstract interface for a database backend that stores XModuleDescriptor
instances instances
""" """
def has_item(self, location):
"""
Returns True if location exists in this ModuleStore.
"""
raise NotImplementedError
def get_item(self, location, depth=0): def get_item(self, location, depth=0):
""" """
Returns an XModuleDescriptor instance for the item at location. 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 If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError xmodule.modulestore.exceptions.InsufficientSpecificationError
...@@ -345,7 +349,8 @@ class ModuleStore(object): ...@@ -345,7 +349,8 @@ class ModuleStore(object):
Returns a list containing the top level XModuleDescriptors of the courses Returns a list containing the top level XModuleDescriptors of the courses
in this modulestore. in this modulestore.
''' '''
raise NotImplementedError course_filter = Location("i4x", category="course")
return self.get_items(course_filter)
def get_parent_locations(self, location): def get_parent_locations(self, location):
'''Find all locations that are the parents of this location. Needed '''Find all locations that are the parents of this location. Needed
......
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 ...@@ -13,6 +13,7 @@ from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location
from .draft import DraftModuleStore
from .exceptions import (ItemNotFoundError, from .exceptions import (ItemNotFoundError,
DuplicateItemError) DuplicateItemError)
...@@ -69,17 +70,21 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -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. 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 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() query = SON()
# Location dict is ordered by specificity, and SON # Location dict is ordered by specificity, and SON
# will preserve that order for queries # will preserve that order for queries
for key, val in Location(location).dict().iteritems(): for key, val in Location(location).dict().iteritems():
if val is not None: if wildcard and val is None:
query['_id.{key}'.format(key=key)] = val continue
query['_id.{key}'.format(key=key)] = val
return query return query
...@@ -202,18 +207,27 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -202,18 +207,27 @@ class MongoModuleStore(ModuleStoreBase):
ItemNotFoundError. ItemNotFoundError.
''' '''
item = self.collection.find_one( item = self.collection.find_one(
location_to_query(location), location_to_query(location, wildcard=False),
sort=[('revision', pymongo.ASCENDING)], sort=[('revision', pymongo.ASCENDING)],
) )
if item is None: if item is None:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
return item 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): def get_item(self, location, depth=0):
""" """
Returns an XModuleDescriptor instance for the item at location. 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 If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError xmodule.modulestore.exceptions.InsufficientSpecificationError
...@@ -321,16 +335,10 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -321,16 +335,10 @@ class MongoModuleStore(ModuleStoreBase):
'''Find all locations that are the parents of this location. Needed '''Find all locations that are the parents of this location. Needed
for path_to_location(). 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 returns an iterable of things that can be passed to Location. This may
be empty if there are no parents. be empty if there are no parents.
''' '''
location = Location.ensure_fully_specified(location) 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()}, items = self.collection.find({'definition.children': location.url()},
{'_id': True}) {'_id': True})
return [i['_id'] for i in items] return [i['_id'] for i in items]
...@@ -341,3 +349,8 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -341,3 +349,8 @@ class MongoModuleStore(ModuleStoreBase):
are loaded on demand, rather than up front are loaded on demand, rather than up front
""" """
return {} 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): ...@@ -60,10 +60,8 @@ def path_to_location(modulestore, course_id, location):
(loc, path) = queue.pop() # Takes from the end (loc, path) = queue.pop() # Takes from the end
loc = Location(loc) loc = Location(loc)
# get_parent_locations should raise ItemNotFoundError if location # Call get_parent_locations first to make sure the location is there
# isn't found so we don't have to do it explicitly. Call this # (even if it's a course, and we would otherwise immediately exit).
# 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) parents = modulestore.get_parent_locations(loc)
# print 'Processing loc={0}, path={1}'.format(loc, path) # print 'Processing loc={0}, path={1}'.format(loc, path)
...@@ -81,6 +79,9 @@ def path_to_location(modulestore, course_id, location): ...@@ -81,6 +79,9 @@ def path_to_location(modulestore, course_id, location):
# If we're here, there is no path # If we're here, there is no path
return None return None
if not modulestore.has_item(location):
raise ItemNotFoundError
path = find_path_to_course() path = find_path_to_course()
if path is None: if path is None:
raise NoPathToItem(location) raise NoPathToItem(location)
......
...@@ -477,11 +477,16 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -477,11 +477,16 @@ class XMLModuleStore(ModuleStoreBase):
except KeyError: except KeyError:
raise ItemNotFoundError(location) 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): def get_item(self, location, depth=0):
""" """
Returns an XModuleDescriptor instance for the item at location. 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 If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError xmodule.modulestore.exceptions.InsufficientSpecificationError
...@@ -545,14 +550,8 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -545,14 +550,8 @@ class XMLModuleStore(ModuleStoreBase):
'''Find all locations that are the parents of this location. Needed '''Find all locations that are the parents of this location. Needed
for path_to_location(). 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 returns an iterable of things that can be passed to Location. This may
be empty if there are no parents. be empty if there are no parents.
''' '''
location = Location.ensure_fully_specified(location) location = Location.ensure_fully_specified(location)
if not self.parent_tracker.is_known(location):
raise ItemNotFoundError(location)
return self.parent_tracker.parents(location) return self.parent_tracker.parents(location)
...@@ -75,6 +75,6 @@ def update_templates(): ...@@ -75,6 +75,6 @@ def update_templates():
), exc_info=True) ), exc_info=True)
continue continue
modulestore().update_item(template_location, template.data) modulestore('direct').update_item(template_location, template.data)
modulestore().update_children(template_location, template.children) modulestore('direct').update_children(template_location, template.children)
modulestore().update_metadata(template_location, template.metadata) modulestore('direct').update_metadata(template_location, template.metadata)
...@@ -409,7 +409,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): ...@@ -409,7 +409,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
# cdodge: this is a list of metadata names which are 'system' metadata # cdodge: this is a list of metadata names which are 'system' metadata
# and should not be edited by an end-user # 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 # A list of descriptor attributes that must be equal for the descriptors to
# be equal # be equal
......
...@@ -57,7 +57,7 @@ def mongo_store_config(data_dir): ...@@ -57,7 +57,7 @@ def mongo_store_config(data_dir):
'OPTIONS': { 'OPTIONS': {
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost', 'host': 'localhost',
'db': 'xmodule', 'db': 'test_xmodule',
'collection': 'modulestore', 'collection': 'modulestore',
'fs_root': data_dir, 'fs_root': data_dir,
'render_template': 'mitxmako.shortcuts.render_to_string', 'render_template': 'mitxmako.shortcuts.render_to_string',
......
...@@ -4,16 +4,18 @@ Settings for the LMS that runs alongside the CMS on AWS ...@@ -4,16 +4,18 @@ Settings for the LMS that runs alongside the CMS on AWS
from ..dev import * 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 = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': { 'OPTIONS': 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',
}
}
} }
"""
Settings for the LMS that runs alongside the CMS on AWS
"""
from .dev import *
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options
},
}
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