Commit 7107e3ee by Chris Dodge

Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into…

Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into feature/cdodge/cas-crud-section-subsection

Conflicts:
	cms/static/js/base.js
parents de303755 6e4fe502
......@@ -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')
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
from util.json_request import expect_json
import exceptions
import json
import os
import logging
import sys
import mimetypes
import os
import StringIO
import exceptions
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
......@@ -157,20 +158,37 @@ 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':
return HttpResponseBadRequest
logging.debug('Start = {0}'.format(item.start))
parent_locs = modulestore().get_parent_locations(location)
# we're for now assuming a single parent
if len(parent_locs) != 1:
logging.error('Multiple (or none) parents have been found for {0}'.format(location))
# this should blow up if we don't find any parents, which would be erroneous
parent = modulestore().get_item(parent_locs[0])
# remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict((key,value) for key, value in item.metadata.iteritems()
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
logging.debug(policy_metadata)
return render_to_response('edit_subsection.html',
{'subsection': item,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'lms_link': lms_link
'lms_link': lms_link,
'parent_item' : parent,
'policy_metadata' : policy_metadata
})
@login_required
def edit_unit(request, location):
"""
......@@ -186,7 +204,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)
......@@ -213,14 +232,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,
})
......@@ -238,7 +268,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>.
......@@ -407,6 +436,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()
......@@ -430,7 +466,10 @@ 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()
......@@ -465,9 +504,12 @@ def save_item(request):
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key in posted_metadata.keys():
# NOTE: We don't want clients to be able to delete 'system metadata' which are not intended to be user
# editable
if posted_metadata[metadata_key] is None and metadata_key not in existing_item.system_metadata_fields:
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in existing_item.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item.metadata:
del existing_item.metadata[metadata_key]
......@@ -484,6 +526,51 @@ def save_item(request):
@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):
parent_location = Location(request.POST['parent_location'])
template = Location(request.POST['template'])
......@@ -506,7 +593,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()}))
......@@ -688,3 +780,9 @@ def asset_index(request, location):
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {})
def static_pages(request, org, course, coursename):
return render_to_response('static-pages.html', {})
def edit_static(request, org, course, coursename):
return render_to_response('edit-static-page.html', {})
......@@ -14,17 +14,23 @@ LOGGING = get_logger_config(ENV_ROOT / "log",
tracking_filename="tracking.log",
debug=True)
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'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',
}
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
}
}
......
......@@ -38,17 +38,23 @@ STATICFILES_DIRS += [
if os.path.isdir(COMMON_TEST_DATA_ROOT / course_dir)
]
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': {
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': 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')
\ No newline at end of file
......@@ -36,6 +36,7 @@ $(document).ready(function() {
$('.set-date').bind('click', showDateSetter);
$('.remove-date').bind('click', removeDateSetter);
<<<<<<< HEAD
// add new/delete section
$('.new-courseware-section-button').bind('click', addNewSection);
$('.delete-section-button').bind('click', deleteSection);
......@@ -43,9 +44,42 @@ $(document).ready(function() {
// add new/delete subsection
$('.new-subsection-item').bind('click', addNewSubsection);
$('.delete-subsection-button').bind('click', deleteSubsection);
=======
// add/remove policy metadata button click handlers
$('.add-policy-data').bind('click', addPolicyMetadata);
$('.remove-policy-data').bind('click', removePolicyMetadata);
$('.sync-date').bind('click', syncReleaseDate);
>>>>>>> 2cf14ae339aed0150be353cbcac25c377956b7ab
});
function syncReleaseDate(e) {
e.preventDefault();
$("#start_date").val("");
$("#start_time").val("");
}
function addPolicyMetadata(e) {
e.preventDefault();
var template =$('#add-new-policy-element-template > li');
var newNode = template.clone();
var _parent_el = $(this).parent('ol:.policy-list');
newNode.insertBefore('.add-policy-data');
$('.remove-policy-data').bind('click', removePolicyMetadata);
}
function removePolicyMetadata(e) {
e.preventDefault();
policy_name = $(this).data('policy-name');
var _parent_el = $(this).parent('li:.policy-list-element');
if ($(_parent_el).hasClass("new-policy-list-element"))
_parent_el.remove();
else
_parent_el.appendTo("#policy-to-delete");
}
// This method only changes the ordering of the child objects in a subsection
function onUnitReordered() {
var subsection_id = $(this).data('subsection-id');
......@@ -94,7 +128,7 @@ function saveSubsection(e) {
var id = $(this).data('id');
// pull all metadata editable fields on page
// pull all 'normalized' metadata editable fields on page
var metadata_fields = $('input[data-metadata-name]');
metadata = {};
......@@ -103,6 +137,20 @@ function saveSubsection(e) {
metadata[$(el).data("metadata-name")] = el.value;
}
// now add 'free-formed' metadata which are presented to the user as dual input fields (name/value)
$('ol.policy-list > li.policy-list-element').each( function(i, element) {
name = $(element).children('.policy-list-name').val();
val = $(element).children('.policy-list-value').val();
metadata[name] = val;
});
// now add any 'removed' policy metadata which is stored in a separate hidden div
// 'null' presented to the server means 'remove'
$("#policy-to-delete > li.policy-list-element").each(function(i, element) {
name = $(element).children('.policy-list-name').val();
if (name != "")
metadata[name] = null;
});
// Piece back together the date/time UI elements into one date/time string
// NOTE: our various "date/time" metadata elements don't always utilize the same formatting string
......
......@@ -29,6 +29,10 @@ h1 {
margin: 36px 6px;
}
.waiting {
opacity: 0.1;
}
.page-actions {
float: right;
margin-top: 42px;
......@@ -128,13 +132,15 @@ label {
}
.new-unit-item,
.new-subsection-item {
.new-subsection-item,
.new-policy-item {
@include grey-button;
margin: 5px 8px;
padding: 3px 10px 4px 10px;
font-size: 10px;
.new-folder-icon,
.new-policy-icon,
.new-unit-icon {
position: relative;
top: 2px;
......
......@@ -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;
}
......@@ -171,6 +170,14 @@
background: url(../img/new-unit-icon.png) right no-repeat;
}
.new-policy-icon {
display: inline-block;
width: 23px;
height: 12px;
margin-right: 8px;
background: url(../img/new-unit-icon.png) right no-repeat;
}
.textbook-icon {
display: inline-block;
width: 32px;
......
......@@ -13,6 +13,10 @@ body.no-header {
color: #fff;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.2), 0 -1px 0px rgba(255, 255, 255, 0.05) inset);
.left {
width: 700px;
}
.drop-icon {
margin-left: 5px;
font-size: 11px;
......
.static-pages {
.new-static-page-button {
@include grey-button;
display: block;
text-align: center;
padding: 12px 0;
}
.static-page-item {
position: relative;
margin: 10px 0;
padding: 22px 20px;
border: 1px solid $darkGrey;
border-radius: 3px;
background: #fff;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1));
.page-name {
font-size: 19px;
font-weight: 700;
}
.item-actions {
margin-top: 19px;
margin-right: 12px;
}
}
}
.edit-static-page {
.main-wrapper {
margin-top: 40px;
}
.static-page-details {
@extend .window;
padding: 32px 40px;
.row {
border: none;
}
}
.page-display-name-input {
width: 100%;
font-size: 20px;
}
.page-contents {
@include box-sizing(border-box);
width: 100%;
height: 360px;
padding: 15px;
border: 1px solid #b0b6c2;
border-radius: 2px;
@include linear-gradient(top, rgba(255, 255, 255, 0), rgba(255, 255, 255, .3));
background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
font-family: Monaco, monospace;
font-size: 13px;
color: #3c3c3c;
outline: 0;
}
}
\ No newline at end of file
......@@ -75,19 +75,17 @@
&.editing {
border-color: #6696d7;
&:hover {
.drag-handle,
.component-actions a {
background-color: $blue;
}
.drag-handle {
border-color: $blue;
.component-actions {
display: none;
}
}
&.component-placeholder {
border-color: #6696d7;
}
.rendered-component {
.xmodule_display {
padding: 40px 20px 20px;
}
......@@ -230,14 +228,24 @@
@include edit-box;
display: none;
padding: 20px;
border-radius: 0 0 3px 3px;
border-radius: 2px 2px 0 0;
@include linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .1));
background-color: $blue;
color: #fff;
@include box-shadow(none);
.metadata_edit {
margin-bottom: 20px;
font-size: 13px;
li {
margin-bottom: 10px;
}
label {
display: inline-block;
margin-right: 10px;
}
}
.CodeMirror {
......@@ -245,6 +253,7 @@
}
h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: 700;
}
......@@ -394,3 +403,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;
}
}
......@@ -14,6 +14,7 @@
@import "subsection";
@import "unit";
@import "assets";
@import "static-pages";
@import "course-info";
@import "landing";
@import "graphics";
......
${preview}
<div class="component-actions">
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
</div>
<a href="#" class="drag-handle"></a>
<div class="component-editor">
<div class="module-editor">
${editor}
......@@ -11,4 +5,9 @@ ${preview}
<a href="#" class="save-button">Save</a>
<a href="#" class="cancel-button">Cancel</a>
</div>
<div class="component-actions">
<a href="#" class="edit-button"><span class="edit-icon white"></span>Edit</a>
<a href="#" class="delete-button"><span class="delete-icon white"></span>Delete</a>
</div>
<a href="#" class="drag-handle"></a>
${preview}
\ No newline at end of file
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Edit Static Page</%block>
<%block name="bodyclass">edit-static-page</%block>
<%block name="content">
<div class="main-wrapper">
<div class="inner-wrapper">
<div class="main-column">
<article class="static-page-details">
<div class="row">
<label>Display Name:</label>
<input type="text" value="Syllabus" class="page-display-name-input" data-metadata-name="display_name"/>
</div>
<div class="row">
<label>Page Content:</label>
<textarea class="page-contents"></textarea>
</div>
</article>
</div>
<div class="sidebar">
<div class="unit-properties window">
<h4>Page Settings</h4>
<div class="window-contents">
<div class="row visibility">
<label class="inline-label">Visibility:</label>
<select>
<option>Public</option>
<option>Private</option>
</select>
</div>
<div class="row unit-actions">
<a href="#" class="save-button">Save</a>
<a href="#" target="_blank" class="preview-button">Preview</a>
</div>
</div>
</div>
</div>
</div>
</div>
</%block>
\ No newline at end of file
......@@ -25,19 +25,36 @@
</div>
<div>
<label>Format:</label>
<input type="text" value="${subsection.metadata['format'] if 'format' in subsection.metadata else ''}" class="unit-subtitle" data-metadata-name="subtitle"/>
<input type="text" value="${subsection.metadata['format'] if 'format' in subsection.metadata else ''}" class="unit-subtitle" data-metadata-name="format"/>
</div>
<div class="unit-list">
<label>Units:</label>
${units.enum_units(subsection)}
</div>
<div class='wip-box'>
<div>
<label>Policy:</label>
<textarea class="text-editor">Policy blah, blah, blah…</textarea>
<ol class='policy-list'>
% for policy_name in policy_metadata.keys():
<li class="policy-list-element">
<input type="text" class="policy-list-name" name="${policy_name}" value="${policy_name}" disabled size="15"/>:&nbsp;<input type="text" class="policy-list-value" name="${policy_metadata[policy_name]}" value="${policy_metadata[policy_name]}" size="40"/><a href="#" class="delete-icon remove-policy-data"></a>
</li>
% endfor
<a href="#" class="new-policy-item add-policy-data" >
<span class="new-policy-icon"></span>New Policy Data
</a>
</ol>
</div>
</article>
</div>
<div id="policy-to-delete" style="display:none">
</div>
<div id="add-new-policy-element-template" style="display:none">
<li class="policy-list-element new-policy-list-element"><input type="text" class="policy-list-name" autocomplete="off" size="15"/>:&nbsp;<input type="text" class="policy-list-value" size=40 autocomplete="off"/><a href="#" class="delete-icon remove-policy-data"></a></li>
</div>
<div class="sidebar">
<div class="unit-properties window">
<h4>Subsection Settings</h4>
......@@ -46,12 +63,15 @@
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
<div class="datepair" data-language="javascript">
<%
start_time = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
parent_start_date = datetime.fromtimestamp(mktime(parent_item.start)) if parent_item.start is not None else None
%>
<input type="text" id="start_date" value="${start_time.strftime('%m/%d/%Y') if start_time is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15'/>
<input type="text" id="start_time" value="${start_time.strftime('%H:%M') if start_time is not None else ''}" placeholder="HH:MM" class="time" size='10'/>
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
<p class="notice wip-box">The date above differs from the release date of Week 1 – 10/10/2012 at 12:00 am. <a href="#" class="sync-date">Sync to Week 1.</a></p>
% if subsection.start != parent_item.start and subsection.start:
<p class="notice">The date above differs from the release date of ${parent_item.display_name} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}. <a href="#" class="sync-date">Sync to ${parent_item.display_name}.</a></p>
% endif
</div>
<div class="due-date-input row">
<label>Due date:</label>
......@@ -62,8 +82,8 @@
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None
%>
<input type="text" id="due_date" value="${due_date.strftime('%Y-%m-%d') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' />
<input type="text" id="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' />
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<a href="#" class="remove-date">Remove due date</a>
</p>
</div>
......@@ -85,6 +105,7 @@
<script src="${static.url('js/vendor/date.js')}"></script>
<script type="text/javascript">
$(document).ready(function() {
// expand the due-date area if the values are set
if ($('#due_date').val() != '') {
var $block = $('.set-date').closest('.due-date-input');
$('.set-date').hide();
......
<%inherit file="base.html" />
<%! from django.core.urlresolvers import reverse %>
<%block name="title">Static Pages</%block>
<%block name="bodyclass">static-pages</%block>
<%block name="content">
<div class="main-wrapper">
<div class="inner-wrapper">
<h1>Static Pages</h1>
<div class="page-actions">
</div>
<article class="static-page-overview">
<a href="#" class="new-static-page-button wip-box"><span class="plus-icon"></span> New Static Page</a>
<ul class="static-page-list">
<li class="static-page-item">
<a href="#" class="page-name">Course Info</a>
<div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a>
</div>
</li>
<li class="static-page-item">
<a href="#" class="page-name">Textbook</a>
<div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a>
</div>
</li>
<li class="static-page-item">
<a href="#" class="page-name">Syllabus</a>
<div class="item-actions">
<a href="#" class="edit-button wip"><span class="delete-icon"></span></a>
<a href="#" class="drag-handle wip"></a>
</div>
</li>
</ul>
</article>
</div>
</div>
</%block>
\ No newline at end of file
......@@ -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>
......
......@@ -7,8 +7,9 @@
<a href="#" class="class-name wip-box">6.002x Circuits and Electronics <span class="drop-icon"></span></a>
<ul class="class-nav">
<li><a href="overview-full.html" class="active">Courseware</a></li>
<li><a href="updates.html" class="wip-box">Course Info</a></li>
<li><a href="textbook.html" class="wip-box">Textbook</a></li>
<li><a href="#" class="wip-box">Pages</a></li>
<li><a href="#" class="wip-box">Assets</a></li>
<li><a href="#" class="wip-box">Users</a></li>
</ul>
</div>
<div class="right">
......@@ -18,7 +19,6 @@
% else:
<a href="${reverse('login')}">Log in</a>
% endif
</ul>
</nav>
</header>
......@@ -3,7 +3,7 @@
<h3>Metadata</h3>
<ul>
% for keyname in editable_metadata_fields:
<li>${keyname}: <input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' /></li>
<li><label>${keyname}:</label> <input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' /></li>
% endfor
</ul>
</section>
......
<%! 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">
......
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,6 +13,9 @@ 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'),
......@@ -30,6 +31,8 @@ urlpatterns = ('',
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
'contentstore.views.remove_user', name='remove_user'),
url(r'^assets/(?P<location>.*?)$', 'contentstore.views.asset_index', name='asset_index'),
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages', name='static_pages'),
url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'),
# temporary landing page for a course
url(r'^landing/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.landing', name='landing')
......@@ -54,6 +57,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
......
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,16 +70,20 @@ 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:
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 = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'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',
}
}
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
},
}
"""
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