From 0eed43baa42880f9290cbed940dae043d489f0d0 Mon Sep 17 00:00:00 2001 From: stv Date: Thu, 20 Nov 2014 11:06:11 -0500 Subject: [PATCH] Implement default Studio View editor --- cms/djangoapps/contentstore/views/item.py | 9 +- cms/envs/common.py | 11 +- cms/lib/xblock/authoring_mixin.py | 131 +++++++++++++ cms/lib/xblock/static/js/src/authoring.js | 73 ++++++++ cms/static/js/factories/container.js | 6 +- cms/static/js/views/modals/edit_xblock.js | 25 ++- cms/static/js/views/xblock.js | 41 ++-- cms/static/js/views/xblock_editor.js | 46 ++++- cms/static/sass/_developer.scss | 89 +++++++++ cms/templates/container.html | 9 +- .../studio_xblock_tabbed_editor.html | 23 +++ common/lib/xmodule/xmodule/x_module.py | 177 +++++++++--------- requirements/edx/github.txt | 5 +- 13 files changed, 525 insertions(+), 120 deletions(-) create mode 100644 cms/lib/xblock/authoring_mixin.py create mode 100644 cms/lib/xblock/static/js/src/authoring.js create mode 100644 cms/templates/studio_xblock_tabbed_editor.html diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index e09db7a4e499..f9d41c4e38a1 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -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. @@ -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, @@ -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 diff --git a/cms/envs/common.py b/cms/envs/common.py index c542b373b702..d1bc9c1d0b1e 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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 @@ -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, @@ -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 diff --git a/cms/lib/xblock/authoring_mixin.py b/cms/lib/xblock/authoring_mixin.py new file mode 100644 index 000000000000..484cd820af19 --- /dev/null +++ b/cms/lib/xblock/authoring_mixin.py @@ -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'
' + + +@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 + %> +
+ """) + 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')} diff --git a/cms/lib/xblock/static/js/src/authoring.js b/cms/lib/xblock/static/js/src/authoring.js new file mode 100644 index 000000000000..f86841995d84 --- /dev/null +++ b/cms/lib/xblock/static/js/src/authoring.js @@ -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; +}; diff --git a/cms/static/js/factories/container.js b/cms/static/js/factories/container.js index 93cdeb8fd991..9065b80e2fec 100644 --- a/cms/static/js/factories/container.js +++ b/cms/static/js/factories/container.js @@ -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'), diff --git a/cms/static/js/views/modals/edit_xblock.js b/cms/static/js/views/modals/edit_xblock.js index 67e9de6f88e1..4ac18f8ceab1 100644 --- a/cms/static/js/views/modals/edit_xblock.js +++ b/cms/static/js/views/modals/edit_xblock.js @@ -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(); } @@ -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…'), + function() { + return editorView.saveEditorTabs(); + }).done(function() { + self.onSave(); + }); + } + else { + var data = editorView.getXModuleData(); + ViewUtils.runOperationShowingMessage(gettext('Saving…'), function() { return xblockInfo.save(data); diff --git a/cms/static/js/views/xblock.js b/cms/static/js/views/xblock.js index 797cc8cb45f5..19bed73f08df 100644 --- a/cms/static/js/views/xblock.js +++ b/cms/static/js/views/xblock.js @@ -32,7 +32,6 @@ 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, @@ -40,22 +39,40 @@ define(["jquery", "underscore", "js/views/baseview", "xblock/runtime.v1"], 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) { diff --git a/cms/static/js/views/xblock_editor.js b/cms/static/js/views/xblock_editor.js index 7cdf2de34a75..b502bfa14677 100644 --- a/cms/static/js/views/xblock_editor.js +++ b/cms/static/js/views/xblock_editor.js @@ -26,9 +26,12 @@ define(["jquery", "underscore", "gettext", "js/views/xblock", "js/views/metadata initializeEditors: function() { var metadataEditor, defaultMode = 'editor'; - metadataEditor = this.createMetadataEditor(); - this.metadataEditor = metadataEditor; + if (!this.hasCustomTabs()) { + // TODO does this cause any problems with the video player? + // Don't want to go into createMetadataEditor because tabbed editor + // will be using a metadata editor, and there will be conflicts. + this.metadataEditor = this.createMetadataEditor(); if (this.getDataEditor()) { defaultMode = 'editor'; } else if (metadataEditor) { @@ -79,6 +82,22 @@ define(["jquery", "underscore", "gettext", "js/views/xblock", "js/views/metadata return metadataView; }, + saveEditorTabs: function () { + var payload = {}; + _.each(this.xblockElements, function(element) { + var tab_id = element.element.parent().attr("data-tab-id"); + if (element.collectFieldData) { + payload[tab_id] = {"fields": element.collectFieldData()}; + } + else if (element.collectXmlData) { + payload[tab_id] = {"xml": element.collectXmlData()}; + } + }); + + var handler_url = this.xblock.runtime.handlerUrl(this.xblock.element, "save_tab_data"); + return $.post(handler_url, JSON.stringify(payload)); //.success() + }, + getDataEditor: function() { var editor = this.$('.wrapper-comp-editor'); return editor.length === 1 ? editor : null; @@ -142,18 +161,27 @@ define(["jquery", "underscore", "gettext", "js/views/xblock", "js/views/metadata var showEditor = mode === 'editor', dataEditor = this.getDataEditor(), metadataEditor = this.getMetadataEditor(); - if (dataEditor) { - this.setEditorActivation(dataEditor, showEditor); - } - if (metadataEditor) { - this.setEditorActivation(metadataEditor.$el, !showEditor); + if (dataEditor || metadataEditor) { + if (dataEditor) { + this.setTabViewActivation(dataEditor, showEditor); + } + if (metadataEditor) { + this.setTabViewActivation(metadataEditor.$el, !showEditor); + } + } else { + this.$('.component-tab').removeClass('is-active'); + this.$('.component-tab').addClass('is-inactive is-hidden'); + this.setTabViewActivation(this.$('.tab-view-' + mode), true); } this.mode = mode; }, - setEditorActivation: function(editor, isActive) { - editor.removeClass('is-active').removeClass('is-inactive'); + setTabViewActivation: function(editor, isActive) { + editor.removeClass('is-active is-inactive is-hidden'); editor.addClass(isActive ? 'is-active' : 'is-inactive'); + if (!isActive) { + editor.addClass('is-hidden'); + } } }); diff --git a/cms/static/sass/_developer.scss b/cms/static/sass/_developer.scss index f5c69e8e6b77..905c95d489b2 100644 --- a/cms/static/sass/_developer.scss +++ b/cms/static/sass/_developer.scss @@ -8,3 +8,92 @@ // } // -------------------- + +.modal-lg.modal-editor .edit-xblock-modal .modal-content { + height: 417px; + overflow: hidden; +} + +.xblock-tabbed-editor { + .action-item { + display: inline-block; + } + + .is-hidden { + display: none; + } + + .editor-with-tabs { + .editor-tabs .inner_tab_wrap a.tab { + font-weight: normal !important; + } + + .component-tab { + height: 365px; + } + + .edit-header { + border: 0; + background-color: $gray-l4; + padding: ($baseline/2); + + .component-name { + @extend %t-title5; + @extend %t-strong; + display: inline-block; + vertical-align: middle; + width: 48%; + margin-left: ($baseline/2); + color: $black; + + em { + color: inherit; + display: inline; + } + } + + .editor-tabs { + display: inline-block; + width: 48%; + position: relative; + top: auto; + right: auto; + padding: 0; + text-align: right; + + .settings-list { + max-height: 365px; + } + + .inner_tab_wrap { + padding: 0; + + a.tab { + @extend %btn-secondary-gray; + @extend %t-copy-sub1; + @extend %t-regular; + background-image: none; + box-shadow: none; + border: 0; + padding: ($baseline/4) ($baseline/2); + text-transform: uppercase; + + &.current, &.is-set { + background-color: $gray-d1; + color: $white; + } + } + } + } + } + } + + .tabs-wrapper { + overflow-y: scroll; + border: 1px solid $gray-l2; + + .component-tab { + border-top: 0; + } + } +} diff --git a/cms/templates/container.html b/cms/templates/container.html index b24cffe2ea10..4bb45782a59d 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -23,7 +23,7 @@ "editor-mode-button", "upload-dialog", "image-modal", "add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu", "add-xblock-component-menu-problem", "xblock-string-field-editor", "publish-xblock", "publish-history", - "unit-outline", "container-message"] + "unit-outline", "container-message", "metadata-editor"] %> <%block name="header_extras"> % for template_name in templates: @@ -31,6 +31,13 @@ <%static:include path="js/${template_name}.underscore" /> % endfor + +% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry", "metadata-file-uploader-entry", "metadata-file-uploader-item"]: + + +% endfor diff --git a/cms/templates/studio_xblock_tabbed_editor.html b/cms/templates/studio_xblock_tabbed_editor.html new file mode 100644 index 000000000000..4951d8ef1801 --- /dev/null +++ b/cms/templates/studio_xblock_tabbed_editor.html @@ -0,0 +1,23 @@ +
+
+
+ + % if len(tabs) > 1: + + % endif +
+
+
+ % for tab in tabs: +
+ ${tab['rendered_view']} +
+ % endfor +
+
+
+
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 3a7d5699d1de..3f3ef3b99778 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -704,7 +704,98 @@ def get_template(cls, template_id): @XBlock.needs("i18n") -class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): +class MetadataEditingMixin(XBlockMixin): + _services_requested = { + "i18n": "need", + } + + @property + def non_editable_metadata_fields(self): + """ + Return the list of fields that should not be editable in Studio. + + When overriding, be sure to append to the superclasses' list. + """ + # We are not allowing editing of xblock tag and name fields at this time (for any component). + return [XBlock.tags, XBlock.name] + + @property + def editable_metadata_fields(self): + """ + Returns the metadata fields to be edited in Studio. These are fields with scope `Scope.settings`. + + Can be limited by extending `non_editable_metadata_fields`. + """ + metadata_fields = {} + + # Only use the fields from this class, not mixins + fields = getattr(self, 'unmixed_class', self.__class__).fields + + for field in fields.values(): + + if field.scope != Scope.settings or field in self.non_editable_metadata_fields: + continue + + metadata_fields[field.name] = self._create_metadata_editor_info(field) + + return metadata_fields + + def _create_metadata_editor_info(self, field): + """ + Creates the information needed by the metadata editor for a specific field. + """ + def jsonify_value(field, json_choice): + if isinstance(json_choice, dict): + json_choice = dict(json_choice) # make a copy so below doesn't change the original + if 'display_name' in json_choice: + json_choice['display_name'] = get_text(json_choice['display_name']) + if 'value' in json_choice: + json_choice['value'] = field.to_json(json_choice['value']) + else: + json_choice = field.to_json(json_choice) + return json_choice + + def get_text(value): + """Localize a text value that might be None.""" + if value is None: + return None + else: + return self.runtime.service(self, "i18n").ugettext(value) + + # gets the 'default_value' and 'explicitly_set' attrs + metadata_field_editor_info = self.runtime.get_field_provenance(self, field) + metadata_field_editor_info['field_name'] = field.name + metadata_field_editor_info['display_name'] = get_text(field.display_name) + metadata_field_editor_info['help'] = get_text(field.help) + metadata_field_editor_info['value'] = field.read_json(self) + + # We support the following editors: + # 1. A select editor for fields with a list of possible values (includes Booleans). + # 2. Number editors for integers and floats. + # 3. A generic string editor for anything else (editing JSON representation of the value). + editor_type = "Generic" + values = field.values + if isinstance(values, (tuple, list)) and len(values) > 0: + editor_type = "Select" + values = [jsonify_value(field, json_choice) for json_choice in values] + elif isinstance(field, Integer): + editor_type = "Integer" + elif isinstance(field, Float): + editor_type = "Float" + elif isinstance(field, List): + editor_type = "List" + elif isinstance(field, Dict): + editor_type = "Dict" + elif isinstance(field, RelativeTime): + editor_type = "RelativeTime" + metadata_field_editor_info['type'] = editor_type + metadata_field_editor_info['options'] = [] if values is None else values + + return metadata_field_editor_info + + +@XBlock.needs("i18n") +class XModuleDescriptor(XModuleMixin, MetadataEditingMixin, HTMLSnippet, ResourceTemplates, XBlock): """ An XModuleDescriptor is a specification for an element of a course. This could be a problem, an organizational element (a group of content), or a @@ -849,90 +940,6 @@ def __repr__(self): ")".format(self) ) - @property - def non_editable_metadata_fields(self): - """ - Return the list of fields that should not be editable in Studio. - - When overriding, be sure to append to the superclasses' list. - """ - # We are not allowing editing of xblock tag and name fields at this time (for any component). - return [XBlock.tags, XBlock.name] - - @property - def editable_metadata_fields(self): - """ - Returns the metadata fields to be edited in Studio. These are fields with scope `Scope.settings`. - - Can be limited by extending `non_editable_metadata_fields`. - """ - metadata_fields = {} - - # Only use the fields from this class, not mixins - fields = getattr(self, 'unmixed_class', self.__class__).fields - - for field in fields.values(): - - if field.scope != Scope.settings or field in self.non_editable_metadata_fields: - continue - - metadata_fields[field.name] = self._create_metadata_editor_info(field) - - return metadata_fields - - def _create_metadata_editor_info(self, field): - """ - Creates the information needed by the metadata editor for a specific field. - """ - def jsonify_value(field, json_choice): - if isinstance(json_choice, dict): - json_choice = dict(json_choice) # make a copy so below doesn't change the original - if 'display_name' in json_choice: - json_choice['display_name'] = get_text(json_choice['display_name']) - if 'value' in json_choice: - json_choice['value'] = field.to_json(json_choice['value']) - else: - json_choice = field.to_json(json_choice) - return json_choice - - def get_text(value): - """Localize a text value that might be None.""" - if value is None: - return None - else: - return self.runtime.service(self, "i18n").ugettext(value) - - # gets the 'default_value' and 'explicitly_set' attrs - metadata_field_editor_info = self.runtime.get_field_provenance(self, field) - metadata_field_editor_info['field_name'] = field.name - metadata_field_editor_info['display_name'] = get_text(field.display_name) - metadata_field_editor_info['help'] = get_text(field.help) - metadata_field_editor_info['value'] = field.read_json(self) - - # We support the following editors: - # 1. A select editor for fields with a list of possible values (includes Booleans). - # 2. Number editors for integers and floats. - # 3. A generic string editor for anything else (editing JSON representation of the value). - editor_type = "Generic" - values = field.values - if isinstance(values, (tuple, list)) and len(values) > 0: - editor_type = "Select" - values = [jsonify_value(field, json_choice) for json_choice in values] - elif isinstance(field, Integer): - editor_type = "Integer" - elif isinstance(field, Float): - editor_type = "Float" - elif isinstance(field, List): - editor_type = "List" - elif isinstance(field, Dict): - editor_type = "Dict" - elif isinstance(field, RelativeTime): - editor_type = "RelativeTime" - metadata_field_editor_info['type'] = editor_type - metadata_field_editor_info['options'] = [] if values is None else values - - return metadata_field_editor_info - # ~~~~~~~~~~~~~~~ XModule Indirection ~~~~~~~~~~~~~~~~ @property def _xmodule(self): diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 0064263d2102..bdd3f7d2b57e 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -29,9 +29,12 @@ -e git+https://github.com/edx/bok-choy.git@4a259e3548a19e41cc39433caf68ea58d10a27ba#egg=bok_choy -e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash -e git+https://github.com/edx/acid-block.git@df1a7f0cae46567c251d507b8c72168aed8ec042#egg=acid-xblock --e git+https://github.com/edx/edx-ora2.git@release-2014-10-27T19.33#egg=edx-ora2 +-e git+https://github.com/edx/edx-ora2.git@1aaa021445f24e96943b8337c0be812d944525a2#egg=edx-ora2 -e git+https://github.com/edx/opaque-keys.git@0.1.2#egg=opaque-keys -e git+https://github.com/edx/ease.git@97de68448e5495385ba043d3091f570a699d5b5f#egg=ease -e git+https://github.com/edx/i18n-tools.git@56f048af9b6868613c14aeae760548834c495011#egg=i18n-tools -e git+https://github.com/edx/edx-oauth2-provider.git@0.3.1#egg=oauth2-provider -e git+https://github.com/edx/edx-val.git@a3c54afe30375f7a5755ba6f6412a91de23c3b86#egg=edx-val + +# hack hack hack +-e git+https://github.com/Stanford-Online/xblock-image-modal.git@f87f0c58cfd1cfb7b28477130172c1d01f1c27c2#egg=ImageModal