Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions cms/djangoapps/contentstore/views/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,11 @@ def xblock_view_handler(request, usage_key_string, view_name):

if view_name == STUDIO_VIEW:
try:
fragment = xblock.render(STUDIO_VIEW)
context = {}
if hasattr(xblock, 'studio_view'):
fragment = xblock.render(STUDIO_VIEW, context=context)
else:
fragment = xblock.render_tabbed_editor(context)
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
Expand Down Expand Up @@ -716,7 +720,6 @@ def safe_get_username(user_id):
else:
visibility_state = None
published = modulestore().has_published_version(xblock)

xblock_info = {
"id": unicode(xblock.location),
"display_name": xblock.display_name_with_default,
Expand All @@ -735,7 +738,7 @@ def safe_get_username(user_id):
"due": xblock.fields['due'].to_json(xblock.due),
"format": xblock.format,
"course_graders": json.dumps([grader.get('type') for grader in graders]),
"has_changes": has_changes,
"has_changes": has_changes
}
if data is not None:
xblock_info["data"] = data
Expand Down
11 changes: 9 additions & 2 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from warnings import simplefilter

from lms.lib.xblock.mixin import LmsBlockMixin
from cms.lib.xblock.authoring_mixin import AuthoringMixin
from dealer.git import git
from xmodule.modulestore.edit_info import EditInfoMixin

Expand Down Expand Up @@ -76,7 +77,7 @@

# If set to True, Studio won't restrict the set of advanced components
# to just those pre-approved by edX
'ALLOW_ALL_ADVANCED_COMPONENTS': False,
'ALLOW_ALL_ADVANCED_COMPONENTS': True,

# Turn off account locking if failed login attempts exceeds a limit
'ENABLE_MAX_FAILED_LOGIN_ATTEMPTS': False,
Expand Down Expand Up @@ -255,7 +256,13 @@

# This should be moved into an XBlock Runtime/Application object
# once the responsibility of XBlock creation is moved out of modulestore - cpennington
XBLOCK_MIXINS = (LmsBlockMixin, InheritanceMixin, XModuleMixin, EditInfoMixin)
XBLOCK_MIXINS = (
LmsBlockMixin,
InheritanceMixin,
XModuleMixin,
EditInfoMixin,
AuthoringMixin,
)

# Allow any XBlock in Studio
# You should also enable the ALLOW_ALL_ADVANCED_COMPONENTS feature flag, so that
Expand Down
131 changes: 131 additions & 0 deletions cms/lib/xblock/authoring_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
TODO: something smart here
"""
import copy
import logging
from django.utils.translation import ugettext as _
import defusedxml.ElementTree as safe_etree
import pkg_resources

from xblock.core import XBlock

from xblock.fields import XBlockMixin
from xblock.fragment import Fragment
from lxml import etree
import json

logger = logging.getLogger(__name__)

# Uck, this needs to go into a shared location.
from xmodule.x_module import MetadataEditingMixin
from mako.template import Template

XML_EDITOR_HTML = u'<div id="xml-edit"><textarea class="xml-editor">{xml}</textarea></div>'


@XBlock.needs("i18n")
class AuthoringMixin(MetadataEditingMixin, XBlockMixin):
"""
TODO:
"""
_services_requested = {
"i18n": "need",
}

@property
def editor_tabs(self):
return [
# TODO: internationalize
{"display_name": "Settings", "id": "settings"},
]

def settings_tab_view(self, context=None):
"""
TODO:
"""
settings_template = Template("""
<%!
import json
# Uck, don't really want this import.
from xmodule.modulestore import EdxJSONEncoder
%>
<div class="wrapper-comp-settings metadata_edit is-active" id="settings-tab" data-metadata='${json.dumps(metadata_fields, cls=EdxJSONEncoder) | h}'/>
""")
fragment = Fragment(settings_template.render(metadata_fields=self.editable_metadata_fields))
fragment.add_javascript(pkg_resources.resource_string(__name__, "static/js/src/authoring.js"))
fragment.initialize_js('SettingsTabViewInit')
return fragment

def xml_tab_view(self, context=None):
"""
Render the XBlock for editing in XML in Studio.
Args:
context: Not actively used for this view.
Returns:
(Fragment): An HTML fragment for editing the configuration of this XBlock.
"""
root = etree.Element('root')
self.add_xml_to_node(root)
xml = etree.tostring(root, pretty_print=True)
frag = Fragment(XML_EDITOR_HTML.format(xml=xml))
frag.add_javascript(pkg_resources.resource_string(__name__, "static/js/src/authoring.js"))
frag.initialize_js('XBlockXMLEditor')
return frag

def update_from_xml(self, xml):
"""
Update the XBlock's XML.
Args:
xml (str): XML String representation used to update the XBlock's field.
"""
root = safe_etree.fromstring(xml.encode('utf-8'))
for key in root.attrib.keys():
if key in self.fields:
setattr(self, key, root.attrib[key])

def render_tabbed_editor(self, context):
"""
Renders the Studio preview by rendering each tab desired by the xblock and then rendering
a view for each one. The client will ensure that only one view is visible at a time.
"""
fragment = Fragment()
if self.editor_tabs and len(self.editor_tabs) > 0:
tabs = copy.deepcopy(self.editor_tabs)
current_tab = tabs[0]
for tab in tabs:
view_name = tab['id'] + '_tab_view'
rendered_child = self.render(view_name, context)
fragment.add_frag_resources(rendered_child)
tab['rendered_view'] = rendered_child.content

fragment.add_content(self.system.render_template('studio_xblock_tabbed_editor.html', {
'xblock': self,
'tabs': tabs,
'current_tab_id': current_tab['id']
}))
else:
raise Exception("Unable to render tabs for xblock")

return fragment

@XBlock.json_handler
def save_tab_data(self, data, suffix=''):

# TODO: try/catch in appropriate place
# for tab in self.editor_tabs:
for tab in data:
tab_data = data[tab]
if 'fields' in tab_data:
for key, value in tab_data["fields"].iteritems():
if key in self.fields:
setattr(self, key, value)
else:
logger.error(
"Field {field} not a valid field for XBlock.".format(field=key)
)
elif 'xml' in tab_data:
self.update_from_xml(tab_data["xml"])

self.save()
# TODO: return validation messages.
return {'success': True, 'msg': _('Successfully saved XBlock')}
73 changes: 73 additions & 0 deletions cms/lib/xblock/static/js/src/authoring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* JavaScript for Studio editing view of XBlock XML */


/* Namespace for Studio XBlock Editing */
if (typeof XBlockAuthoring == "undefined" || !XBlockAuthoring) {
XBlockAuthoring = {};
}


/**
Interface for editing view in Studio.
The constructor initializes the DOM for editing.
Args:
runtime (Runtime): an XBlock runtime instance.
element (DOM element): The DOM element representing this XBlock.
Returns:
XBlockAuthoring.StudioView
**/
XBlockAuthoring.StudioView = function(runtime, element) {
this.runtime = runtime;

// Initialize the code box
this.codeBox = CodeMirror.fromTextArea(
$(element).find('.xml-editor').first().get(0),
{mode: "xml", lineNumbers: true, lineWrapping: true}
);
};


XBlockAuthoring.StudioView.prototype = {
/**
Collect the XML configuration.
**/
collectXmlData: function() {
return this.codeBox.getValue();
}
};

/* XBlock entry point for Studio view */
function XBlockXMLEditor(runtime, element) {

/**
Initialize the editing interface on page load.
**/
return new XBlockAuthoring.StudioView(runtime, element);
}

function SettingsTabViewInit(runtime, element) {
var view = new SettingsTabView(runtime, element);
return view;
}

function SettingsTabView(runtime, element) {
this.runtime = runtime;
this.element = element;
var metadataEditor = $(element).find('.metadata_edit');
var models = [];
var metadataData = metadataEditor.data('metadata');
for (var key in metadataData) {
if (metadataData.hasOwnProperty(key)) {
models.push(metadataData[key]);
}
}
this.metadataView = new window.MetadataView.Editor({
el: metadataEditor,
collection: new window.MetadataCollection(models)
});
this.metadataView.render();
}
SettingsTabView.prototype.collectFieldData = function collectFieldData() {
var data = this.metadataView.getModifiedMetadataValues();
return data;
};
6 changes: 4 additions & 2 deletions cms/static/js/factories/container.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
define([
'jquery', 'js/models/xblock_info', 'js/views/pages/container',
'js/collections/component_template', 'xmodule', 'coffee/src/main',
'js/collections/component_template', 'xmodule', "js/views/metadata", "js/collections/metadata", 'coffee/src/main',
'xblock/cms.runtime.v1'
],
function($, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
function($, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader, MetadataView, MetadataCollection) {
'use strict';
return function (componentTemplates, XBlockInfoJson, action, isUnitPage) {
var templates = new ComponentTemplates(componentTemplates, {parse: true}),
mainXBlockInfo = new XBlockInfo(XBlockInfoJson, {parse: true});

window.MetadataView = MetadataView;
window.MetadataCollection = MetadataCollection;
xmoduleLoader.done(function () {
var view = new ContainerPage({
el: $('#content'),
Expand Down
25 changes: 20 additions & 5 deletions cms/static/js/views/modals/edit_xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
// If the xblock is not using custom buttons then choose which buttons to show
if (!editorView.hasCustomButtons()) {
// If the xblock does not support save then disable the save button
if (!editorView.xblock.save) {
if (!this.usingTabbedEditor() && (editorView.xblock && !editorView.xblock.save)) {
this.disableSave();
}
}
this.getActionBar().show();
}

Expand Down Expand Up @@ -136,19 +136,34 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
buttonSelector;
editorView.selectMode(mode);
this.$('.editor-modes a').removeClass('is-set');
this.$('.action-modes a').removeClass('is-set');
if (mode) {
buttonSelector = '.' + mode + '-button';
this.$(buttonSelector).addClass('is-set');
}
},

usingTabbedEditor: function () {
return this.$('.xblock-tabbed-editor').length > 0;
},

save: function(event) {
var self = this,
editorView = this.editorView,
xblockInfo = this.xblockInfo,
data = editorView.getXModuleData();
editorView = this.editorView;

event.preventDefault();
if (data) {
if (self.usingTabbedEditor()) {
ViewUtils.runOperationShowingMessage(gettext('Saving&hellip;'),
function() {
return editorView.saveEditorTabs();
}).done(function() {
self.onSave();
});
}
else {
var data = editorView.getXModuleData();

ViewUtils.runOperationShowingMessage(gettext('Saving&hellip;'),
function() {
return xblockInfo.save(data);
Expand Down
41 changes: 29 additions & 12 deletions cms/static/js/views/xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,47 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"],
handleXBlockFragment: function(fragment, options) {
var self = this,
wrapper = this.$el,
xblockElement,
successCallback = options ? options.success || options.done : null,
errorCallback = options ? options.error || options.done : null,
xblock,
fragmentsRendered;

fragmentsRendered = this.renderXBlockFragment(fragment, wrapper);
fragmentsRendered.always(function() {
xblockElement = self.$('.xblock').first();
try {
xblock = XBlock.initializeBlock(xblockElement);
self.xblock = xblock;
self.xblockReady(xblock);

// With tabbed xblock editor support, there will be multiple "xblock" elements.
var xblockElements = self.$('.xblock');
var continueInitializing = true;

self.xblockElements = [];

xblockElements.each(function (index, xblockElement) {
if (continueInitializing) {
try {
xblock = XBlock.initializeBlock($(xblockElement));
self.xblockElements.push(xblock);
} catch (e) {
console.error(e.stack);
continueInitializing = false;
// Add 'xblock-initialization-failed' class to every xblock??
self.$('.xblock').addClass('xblock-initialization-failed');
}
}
});

if (continueInitializing) {
// Some code (but I think only xmodule code) assumes self.xblock will exist.
// Assign to the first xblockElements item for now. TODO: make more elegant.
self.xblock = self.xblockElements[0];
self.xblockReady(self.xblock);
if (successCallback) {
successCallback(xblock);
}
} catch (e) {
console.error(e.stack);
// Add 'xblock-initialization-failed' class to every xblock
self.$('.xblock').addClass('xblock-initialization-failed');

}
else {
// If the xblock was rendered but failed then still call xblockReady to allow
// drag-and-drop to be initialized.
if (xblockElement) {
if (xblockElements.length > 0) {
self.xblockReady(null);
}
if (errorCallback) {
Expand Down
Loading