Commit 25544132 by Calen Pennington

Hook up link to create modules from templates

parent 912d6cd6
from xmodule.templates import update_templates
update_templates()
...@@ -20,6 +20,8 @@ from xmodule.modulestore.django import modulestore ...@@ -20,6 +20,8 @@ from xmodule.modulestore.django import modulestore
from xmodule_modifiers import replace_static_urls, wrap_xmodule from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from functools import partial from functools import partial
from itertools import groupby
from operator import attrgetter
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -128,6 +130,33 @@ def edit_item(request): ...@@ -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): 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>.
...@@ -262,7 +291,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_ ...@@ -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 = descriptor.xmodule_constructor(system)(instance_state, shared_state)
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
wrap_xmodule(module.get_html, module, "xmodule_display.html"), 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(), save_preview_state(request, preview_id, descriptor.location.url(),
module.get_instance_state(), module.get_shared_state()) module.get_instance_state(), module.get_shared_state())
...@@ -326,3 +355,24 @@ def save_item(request): ...@@ -326,3 +355,24 @@ def save_item(request):
preview_html = get_module_previews(request, descriptor) preview_html = get_module_previews(request, descriptor)
return HttpResponse(json.dumps(preview_html)) 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): ...@@ -210,7 +210,7 @@ for dir_ in (js_file_dir, css_file_dir):
js_fragments = set() js_fragments = set()
css_fragments = defaultdict(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() descriptor_js = descriptor.get_javascript()
module_js = descriptor.module_class.get_javascript() module_js = descriptor.module_class.get_javascript()
......
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 ...@@ -7,7 +7,8 @@ class CMS.Views.Module extends Backbone.View
previewType = @$el.data('preview-type') previewType = @$el.data('preview-type')
moduleType = @$el.data('type') moduleType = @$el.data('type')
CMS.replaceView new CMS.Views.ModuleEdit CMS.replaceView new CMS.Views.ModuleEdit
model: new CMS.Models.Module model: new CMS.Models.Module
id: @$el.data('id') id: @$el.data('id')
type: if moduleType == 'None' then null else moduleType type: if moduleType == 'None' then null else moduleType
previewType: if previewType == 'None' then null else previewType previewType: if previewType == 'None' then null else previewType
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 ...@@ -39,7 +39,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
) )
XModule.loadModules('display') XModule.loadModules('display')
).fail(-> ).fail( ->
alert("There was an error saving your changes. Please try again.") alert("There was an error saving your changes. Please try again.")
) )
......
class CMS.Views.Week extends Backbone.View class CMS.Views.Week extends Backbone.View
events: events:
'click .week-edit': 'edit' 'click .week-edit': 'edit'
'click .new-module': 'new'
initialize: -> initialize: ->
CMS.on('content.show', @resetHeight) CMS.on('content.show', @resetHeight)
...@@ -23,3 +24,9 @@ class CMS.Views.Week extends Backbone.View ...@@ -23,3 +24,9 @@ class CMS.Views.Week extends Backbone.View
resetHeight: => resetHeight: =>
@$el.height('') @$el.height('')
new: (event) =>
event.preventDefault()
CMS.replaceView new CMS.Views.ModuleAdd
model: new CMS.Models.NewModule
parent_location: @$el.data('id')
<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 @@ ...@@ -7,7 +7,7 @@
<form> <form>
<ul> <ul>
<li> <li>
<input type="text" name="" id="" placeholder="Moldule title" /> <input type="text" name="" id="" placeholder="Module title" />
</li> </li>
<li> <li>
<select> <select>
......
...@@ -9,8 +9,10 @@ import django.contrib.auth.views ...@@ -9,8 +9,10 @@ import django.contrib.auth.views
urlpatterns = ('', urlpatterns = ('',
url(r'^$', 'contentstore.views.index', name='index'), 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'^edit_item$', 'contentstore.views.edit_item', name='edit_item'),
url(r'^save_item$', 'contentstore.views.save_item', name='save_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>[^/]+)$', 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'),
......
...@@ -6,7 +6,7 @@ import sys ...@@ -6,7 +6,7 @@ import sys
from lxml import etree from lxml import etree
from path import path from path import path
from .x_module import XModule from .x_module import XModule, Template
from .xml_module import XmlDescriptor, name_to_pathname from .xml_module import XmlDescriptor, name_to_pathname
from .editing_module import EditingDescriptor from .editing_module import EditingDescriptor
from .stringify import stringify_children from .stringify import stringify_children
...@@ -34,6 +34,10 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -34,6 +34,10 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
module_class = HtmlModule module_class = HtmlModule
filename_extension = "xml" filename_extension = "xml"
templates = [
Template('Empty', '', [])
]
# VS[compat] TODO (cpennington): Delete this method once all fall 2012 course # VS[compat] TODO (cpennington): Delete this method once all fall 2012 course
# are being edited in the cms # are being edited in the cms
@classmethod @classmethod
......
...@@ -297,8 +297,11 @@ class ModuleStore(object): ...@@ -297,8 +297,11 @@ class ModuleStore(object):
""" """
raise NotImplementedError raise NotImplementedError
# TODO (cpennington): Replace with clone_item def clone_item(self, source, location):
def create_item(self, location, editor): """
Clone a new item that is a copy of the item at the location `source`
and writes it to `location`
"""
raise NotImplementedError raise NotImplementedError
def update_item(self, location, data): def update_item(self, location, data):
......
...@@ -237,20 +237,16 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -237,20 +237,16 @@ class MongoModuleStore(ModuleStoreBase):
return self._load_items(list(items), depth) return self._load_items(list(items), depth)
# TODO (cpennington): This needs to be replaced by clone_item as soon as we allow def clone_item(self, source, location):
# creation of items from the cms
def create_item(self, location):
""" """
Create an empty item at the specified location. Clone a new item that is a copy of the item at the location `source`
and writes it to `location`
If that location already exists, raises a DuplicateItemError
location: Something that can be passed to Location
""" """
try: try:
self.collection.insert({ source_item = self.collection.find_one(location_to_query(source))
'_id': Location(location).dict(), source_item['_id'] = Location(location).dict()
}) self.collection.insert(source_item)
return self._load_items([source_item])[0]
except pymongo.errors.DuplicateKeyError: except pymongo.errors.DuplicateKeyError:
raise DuplicateItemError(location) raise DuplicateItemError(location)
......
...@@ -471,10 +471,6 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -471,10 +471,6 @@ class XMLModuleStore(ModuleStoreBase):
""" """
return dict( (k, self.errored_courses[k].errors) for k in self.errored_courses) 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): def update_item(self, location, data):
""" """
Set the data in the item specified by the location to 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, ...@@ -24,13 +24,6 @@ def import_from_xml(store, data_dir, course_dirs=None,
for course_id in module_store.modules.keys(): for course_id in module_store.modules.keys():
for module in module_store.modules[course_id].itervalues(): 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: if 'data' in module.definition:
store.update_item(module.location, module.definition['data']) store.update_item(module.location, module.definition['data'])
if 'children' in module.definition: if 'children' in module.definition:
......
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 ...@@ -7,6 +7,7 @@ from functools import partial
from lxml import etree from lxml import etree
from lxml.etree import XMLSyntaxError from lxml.etree import XMLSyntaxError
from pprint import pprint from pprint import pprint
from collections import namedtuple
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -71,7 +72,11 @@ class Plugin(object): ...@@ -71,7 +72,11 @@ class Plugin(object):
@classmethod @classmethod
def load_classes(cls): 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_ for class_
in pkg_resources.iter_entry_points(cls.entry_point)] in pkg_resources.iter_entry_points(cls.entry_point)]
...@@ -321,6 +326,9 @@ def policy_key(location): ...@@ -321,6 +326,9 @@ def policy_key(location):
return '{cat}/{name}'.format(cat=location.category, name=location.name) return '{cat}/{name}'.format(cat=location.category, name=location.name)
Template = namedtuple("Template", "name data children")
class XModuleDescriptor(Plugin, HTMLSnippet): class XModuleDescriptor(Plugin, HTMLSnippet):
""" """
An XModuleDescriptor is a specification for an element of a course. This An XModuleDescriptor is a specification for an element of a course. This
...@@ -361,6 +369,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -361,6 +369,11 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
equality_attributes = ('definition', 'metadata', 'location', equality_attributes = ('definition', 'metadata', 'location',
'shared_state_key', '_inherited_metadata') '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 =================== # ============================= STRUCTURAL MANIPULATION ===================
def __init__(self, def __init__(self,
system, system,
......
...@@ -468,7 +468,7 @@ for dir_ in (js_file_dir, css_file_dir): ...@@ -468,7 +468,7 @@ for dir_ in (js_file_dir, css_file_dir):
js_fragments = set() js_fragments = set()
css_fragments = defaultdict(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() module_js = descriptor.module_class.get_javascript()
for filetype in ('coffee', 'js'): for filetype in ('coffee', 'js'):
for idx, fragment in enumerate(module_js.get(filetype, [])): for idx, fragment in enumerate(module_js.get(filetype, [])):
......
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