Hook up link to create modules from templates
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from xmodule.templates import update_templates
|
||||
|
||||
update_templates()
|
||||
|
||||
@@ -20,6 +20,8 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule_modifiers import replace_static_urls, wrap_xmodule
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from functools import partial
|
||||
from itertools import groupby
|
||||
from operator import attrgetter
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -128,6 +130,33 @@ def edit_item(request):
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def new_item(request):
|
||||
"""
|
||||
Display a page where the user can create a new item from a template
|
||||
|
||||
Expects a GET request with the parameter 'parent_location', which is the element to add
|
||||
the newly created item to as a child.
|
||||
|
||||
parent_location: A Location URL
|
||||
"""
|
||||
|
||||
parent_location = request.GET['parent_location']
|
||||
if not has_access(request.user, parent_location):
|
||||
raise Http404
|
||||
|
||||
parent = modulestore().get_item(parent_location)
|
||||
templates = modulestore().get_items(Location('i4x', 'edx', 'templates'))
|
||||
|
||||
templates.sort(key=attrgetter('location.category', 'display_name'))
|
||||
|
||||
return render_to_response('new_item.html', {
|
||||
'parent_name': parent.display_name,
|
||||
'parent_location': parent.location.url(),
|
||||
'templates': groupby(templates, attrgetter('location.category')),
|
||||
})
|
||||
|
||||
|
||||
def user_author_string(user):
|
||||
'''Get an author string for commits by this user. Format:
|
||||
first last <email@email.com>.
|
||||
@@ -262,7 +291,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
|
||||
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
|
||||
module.get_html = replace_static_urls(
|
||||
wrap_xmodule(module.get_html, module, "xmodule_display.html"),
|
||||
module.metadata['data_dir']
|
||||
module.metadata.get('data_dir', module.location.course)
|
||||
)
|
||||
save_preview_state(request, preview_id, descriptor.location.url(),
|
||||
module.get_instance_state(), module.get_shared_state())
|
||||
@@ -326,3 +355,24 @@ def save_item(request):
|
||||
preview_html = get_module_previews(request, descriptor)
|
||||
|
||||
return HttpResponse(json.dumps(preview_html))
|
||||
|
||||
|
||||
@login_required
|
||||
@expect_json
|
||||
def clone_item(request):
|
||||
parent_location = Location(request.POST['parent_location'])
|
||||
template = Location(request.POST['template'])
|
||||
display_name = request.POST['name']
|
||||
|
||||
if not has_access(request.user, parent_location):
|
||||
raise Http404 # TODO (vshnayder): better error
|
||||
|
||||
parent = modulestore().get_item(parent_location)
|
||||
dest_location = parent_location._replace(category=template.category, name=Location.clean_for_url_name(display_name))
|
||||
|
||||
new_item = modulestore().clone_item(template, dest_location)
|
||||
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()])
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
@@ -210,7 +210,7 @@ for dir_ in (js_file_dir, css_file_dir):
|
||||
|
||||
js_fragments = set()
|
||||
css_fragments = defaultdict(set)
|
||||
for descriptor in XModuleDescriptor.load_classes() + [RawDescriptor]:
|
||||
for _, descriptor in XModuleDescriptor.load_classes() + [(None, RawDescriptor)]:
|
||||
descriptor_js = descriptor.get_javascript()
|
||||
module_js = descriptor.module_class.get_javascript()
|
||||
|
||||
|
||||
5
cms/static/coffee/src/models/new_module.coffee
Normal file
5
cms/static/coffee/src/models/new_module.coffee
Normal file
@@ -0,0 +1,5 @@
|
||||
class CMS.Models.NewModule extends Backbone.Model
|
||||
url: '/clone_item'
|
||||
|
||||
newUrl: ->
|
||||
"/new_item?#{$.param(parent_location: @get('parent_location'))}"
|
||||
@@ -7,7 +7,8 @@ class CMS.Views.Module extends Backbone.View
|
||||
previewType = @$el.data('preview-type')
|
||||
moduleType = @$el.data('type')
|
||||
CMS.replaceView new CMS.Views.ModuleEdit
|
||||
model: new CMS.Models.Module
|
||||
id: @$el.data('id')
|
||||
type: if moduleType == 'None' then null else moduleType
|
||||
previewType: if previewType == 'None' then null else previewType
|
||||
model: new CMS.Models.Module
|
||||
id: @$el.data('id')
|
||||
type: if moduleType == 'None' then null else moduleType
|
||||
previewType: if previewType == 'None' then null else previewType
|
||||
|
||||
|
||||
26
cms/static/coffee/src/views/module_add.coffee
Normal file
26
cms/static/coffee/src/views/module_add.coffee
Normal file
@@ -0,0 +1,26 @@
|
||||
class CMS.Views.ModuleAdd extends Backbone.View
|
||||
tagName: 'section'
|
||||
className: 'add-pane'
|
||||
|
||||
events:
|
||||
'click .cancel': 'cancel'
|
||||
'click .save': 'save'
|
||||
|
||||
initialize: ->
|
||||
@$el.load @model.newUrl()
|
||||
|
||||
save: (event) ->
|
||||
event.preventDefault()
|
||||
@model.save({
|
||||
name: @$el.find('.name').val()
|
||||
template: $(event.target).data('template-id')
|
||||
}, {
|
||||
success: -> CMS.popView()
|
||||
error: -> alert('Create failed')
|
||||
})
|
||||
|
||||
cancel: (event) ->
|
||||
event.preventDefault()
|
||||
CMS.popView()
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
)
|
||||
|
||||
XModule.loadModules('display')
|
||||
).fail(->
|
||||
).fail( ->
|
||||
alert("There was an error saving your changes. Please try again.")
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
class CMS.Views.Week extends Backbone.View
|
||||
events:
|
||||
'click .week-edit': 'edit'
|
||||
'click .new-module': 'new'
|
||||
|
||||
initialize: ->
|
||||
CMS.on('content.show', @resetHeight)
|
||||
@@ -23,3 +24,9 @@ class CMS.Views.Week extends Backbone.View
|
||||
|
||||
resetHeight: =>
|
||||
@$el.height('')
|
||||
|
||||
new: (event) =>
|
||||
event.preventDefault()
|
||||
CMS.replaceView new CMS.Views.ModuleAdd
|
||||
model: new CMS.Models.NewModule
|
||||
parent_location: @$el.data('id')
|
||||
|
||||
19
cms/templates/new_item.html
Normal file
19
cms/templates/new_item.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<section>
|
||||
<div>${parent_name}</div>
|
||||
<div>${parent_location}</div>
|
||||
<input type="text" class="name"/>
|
||||
<div>
|
||||
% for module_type, module_templates in templates:
|
||||
<div>
|
||||
<div>${module_type}</div>
|
||||
<div>
|
||||
% for template in module_templates:
|
||||
<a class="save" data-template-id="${template.location.url()}">${template.display_name}</a>
|
||||
% endfor
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
<a class='cancel'>Cancel</a>
|
||||
</section>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<form>
|
||||
<ul>
|
||||
<li>
|
||||
<input type="text" name="" id="" placeholder="Moldule title" />
|
||||
<input type="text" name="" id="" placeholder="Module title" />
|
||||
</li>
|
||||
<li>
|
||||
<select>
|
||||
|
||||
@@ -9,8 +9,10 @@ import django.contrib.auth.views
|
||||
|
||||
urlpatterns = ('',
|
||||
url(r'^$', 'contentstore.views.index', name='index'),
|
||||
url(r'^new_item$', 'contentstore.views.new_item', name='new_item'),
|
||||
url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'),
|
||||
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
|
||||
url(r'^clone_item$', 'contentstore.views.clone_item', name='clone_item'),
|
||||
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'),
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
from lxml import etree
|
||||
from path import path
|
||||
|
||||
from .x_module import XModule
|
||||
from .x_module import XModule, Template
|
||||
from .xml_module import XmlDescriptor, name_to_pathname
|
||||
from .editing_module import EditingDescriptor
|
||||
from .stringify import stringify_children
|
||||
@@ -34,6 +34,10 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
module_class = HtmlModule
|
||||
filename_extension = "xml"
|
||||
|
||||
templates = [
|
||||
Template('Empty', '', [])
|
||||
]
|
||||
|
||||
# VS[compat] TODO (cpennington): Delete this method once all fall 2012 course
|
||||
# are being edited in the cms
|
||||
@classmethod
|
||||
|
||||
@@ -297,8 +297,11 @@ class ModuleStore(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# TODO (cpennington): Replace with clone_item
|
||||
def create_item(self, location, editor):
|
||||
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`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_item(self, location, data):
|
||||
|
||||
@@ -237,20 +237,16 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
|
||||
return self._load_items(list(items), depth)
|
||||
|
||||
# TODO (cpennington): This needs to be replaced by clone_item as soon as we allow
|
||||
# creation of items from the cms
|
||||
def create_item(self, location):
|
||||
def clone_item(self, source, location):
|
||||
"""
|
||||
Create an empty item at the specified location.
|
||||
|
||||
If that location already exists, raises a DuplicateItemError
|
||||
|
||||
location: Something that can be passed to Location
|
||||
Clone a new item that is a copy of the item at the location `source`
|
||||
and writes it to `location`
|
||||
"""
|
||||
try:
|
||||
self.collection.insert({
|
||||
'_id': Location(location).dict(),
|
||||
})
|
||||
source_item = self.collection.find_one(location_to_query(source))
|
||||
source_item['_id'] = Location(location).dict()
|
||||
self.collection.insert(source_item)
|
||||
return self._load_items([source_item])[0]
|
||||
except pymongo.errors.DuplicateKeyError:
|
||||
raise DuplicateItemError(location)
|
||||
|
||||
|
||||
@@ -471,10 +471,6 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
return dict( (k, self.errored_courses[k].errors) for k in self.errored_courses)
|
||||
|
||||
def create_item(self, location):
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
|
||||
def update_item(self, location, data):
|
||||
"""
|
||||
Set the data in the item specified by the location to
|
||||
|
||||
@@ -24,13 +24,6 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
for course_id in module_store.modules.keys():
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
|
||||
# TODO (cpennington): This forces import to overrite the same items.
|
||||
# This should in the future create new revisions of the items on import
|
||||
try:
|
||||
store.create_item(module.location)
|
||||
except DuplicateItemError:
|
||||
log.exception('Item already exists at %s' % module.location.url())
|
||||
pass
|
||||
if 'data' in module.definition:
|
||||
store.update_item(module.location, module.definition['data'])
|
||||
if 'children' in module.definition:
|
||||
|
||||
30
common/lib/xmodule/xmodule/templates.py
Normal file
30
common/lib/xmodule/xmodule/templates.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from collections import defaultdict
|
||||
from .x_module import XModuleDescriptor
|
||||
from .modulestore import Location
|
||||
from .modulestore.django import modulestore
|
||||
|
||||
|
||||
def all_templates():
|
||||
"""
|
||||
Returns all templates for enabled modules, grouped by descriptor type
|
||||
"""
|
||||
|
||||
templates = defaultdict(list)
|
||||
for category, descriptor in XModuleDescriptor.load_classes():
|
||||
templates[category] = descriptor.templates
|
||||
|
||||
return templates
|
||||
|
||||
|
||||
def update_templates():
|
||||
"""
|
||||
Updates the set of templates in the modulestore with all templates currently
|
||||
available from the installed plugins
|
||||
"""
|
||||
|
||||
for category, templates in all_templates().items():
|
||||
for template in templates:
|
||||
template_location = Location('i4x', 'edx', 'templates', category, Location.clean_for_url_name(template.name))
|
||||
modulestore().update_item(template_location, template.data)
|
||||
modulestore().update_children(template_location, template.children)
|
||||
modulestore().update_metadata(template_location, {'display_name': template.name})
|
||||
@@ -7,6 +7,7 @@ from functools import partial
|
||||
from lxml import etree
|
||||
from lxml.etree import XMLSyntaxError
|
||||
from pprint import pprint
|
||||
from collections import namedtuple
|
||||
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.modulestore import Location
|
||||
@@ -71,7 +72,11 @@ class Plugin(object):
|
||||
|
||||
@classmethod
|
||||
def load_classes(cls):
|
||||
return [class_.load()
|
||||
"""
|
||||
Returns a list of containing the identifiers and their corresponding classes for all
|
||||
of the available instances of this plugin
|
||||
"""
|
||||
return [(class_.name, class_.load())
|
||||
for class_
|
||||
in pkg_resources.iter_entry_points(cls.entry_point)]
|
||||
|
||||
@@ -321,6 +326,9 @@ def policy_key(location):
|
||||
return '{cat}/{name}'.format(cat=location.category, name=location.name)
|
||||
|
||||
|
||||
Template = namedtuple("Template", "name data children")
|
||||
|
||||
|
||||
class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
"""
|
||||
An XModuleDescriptor is a specification for an element of a course. This
|
||||
@@ -361,6 +369,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
equality_attributes = ('definition', 'metadata', 'location',
|
||||
'shared_state_key', '_inherited_metadata')
|
||||
|
||||
# A list of Template objects that describe possible templates that can be used
|
||||
# to create a module of this type.
|
||||
# If no templates are provided, there will be no way to create a module of this type
|
||||
templates = []
|
||||
|
||||
# ============================= STRUCTURAL MANIPULATION ===================
|
||||
def __init__(self,
|
||||
system,
|
||||
|
||||
@@ -468,7 +468,7 @@ for dir_ in (js_file_dir, css_file_dir):
|
||||
|
||||
js_fragments = set()
|
||||
css_fragments = defaultdict(set)
|
||||
for descriptor in XModuleDescriptor.load_classes() + [HiddenDescriptor]:
|
||||
for _, descriptor in XModuleDescriptor.load_classes() + [(None, HiddenDescriptor)]:
|
||||
module_js = descriptor.module_class.get_javascript()
|
||||
for filetype in ('coffee', 'js'):
|
||||
for idx, fragment in enumerate(module_js.get(filetype, [])):
|
||||
|
||||
Reference in New Issue
Block a user