Commit 8f133f66 by Calen Pennington

Merge pull request #6035 from edx/jeskew/xmodule_to_xblock_vertical

Convert Vertical XModule to pure Vertical XBlock.
parents d9611dcd a0bae0c7
...@@ -61,7 +61,7 @@ def see_a_multi_step_component(step, category): ...@@ -61,7 +61,7 @@ def see_a_multi_step_component(step, category):
'Raw HTML': '<p>This template is similar to the Text template. The only difference is', 'Raw HTML': '<p>This template is similar to the Text template. The only difference is',
} }
actual_html = world.css_html(selector, index=idx) actual_html = world.css_html(selector, index=idx)
assert_in(html_matcher[step_hash['Component']], actual_html) assert_in(html_matcher[step_hash['Component']].strip(), actual_html.strip())
else: else:
actual_text = world.css_text(selector, index=idx) actual_text = world.css_text(selector, index=idx)
assert_in(step_hash['Component'].upper(), actual_text) assert_in(step_hash['Component'].upper(), actual_text)
......
...@@ -27,7 +27,7 @@ def add_page(step): ...@@ -27,7 +27,7 @@ def add_page(step):
def see_a_static_page_named_foo(step, name): def see_a_static_page_named_foo(step, name):
pages_css = 'div.xmodule_StaticTabModule' pages_css = 'div.xmodule_StaticTabModule'
page_name_html = world.css_html(pages_css) page_name_html = world.css_html(pages_css)
assert_equal(page_name_html, '\n {name}\n'.format(name=name)) assert_equal(page_name_html.strip(), name)
@step(u'I should not see any static pages$') @step(u'I should not see any static pages$')
......
...@@ -5,6 +5,7 @@ import ddt ...@@ -5,6 +5,7 @@ import ddt
from mock import patch, Mock, PropertyMock from mock import patch, Mock, PropertyMock
from pytz import UTC from pytz import UTC
from pyquery import PyQuery
from webob import Response from webob import Response
from django.http import Http404 from django.http import Http404
...@@ -1026,7 +1027,8 @@ class TestEditItemSplitMongo(TestEditItemSetup): ...@@ -1026,7 +1027,8 @@ class TestEditItemSplitMongo(TestEditItemSetup):
for __ in xrange(3): for __ in xrange(3):
resp = self.client.get(view_url, HTTP_ACCEPT='application/json') resp = self.client.get(view_url, HTTP_ACCEPT='application/json')
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.count('xblock-{}'.format(STUDIO_VIEW)), 1) content = json.loads(resp.content)
self.assertEqual(len(PyQuery(content['html'])('.xblock-{}'.format(STUDIO_VIEW))), 1)
class TestEditSplitModule(ItemTest): class TestEditSplitModule(ItemTest):
......
...@@ -9,16 +9,19 @@ import static_replace ...@@ -9,16 +9,19 @@ import static_replace
import uuid import uuid
import markupsafe import markupsafe
from lxml import html, etree from lxml import html, etree
from contracts import contract
from django.conf import settings from django.conf import settings
from django.utils.timezone import UTC from django.utils.timezone import UTC
from django.utils.html import escape from django.utils.html import escape
from django.contrib.auth.models import User
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from xblock.core import XBlock
from xblock.exceptions import InvalidScopeError from xblock.exceptions import InvalidScopeError
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xmodule.seq_module import SequenceModule from xmodule.seq_module import SequenceModule
from xmodule.vertical_module import VerticalModule from xmodule.vertical_block import VerticalBlock
from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule, PREVIEW_VIEWS, STUDIO_VIEW from xmodule.x_module import shim_xmodule_js, XModuleDescriptor, XModule, PREVIEW_VIEWS, STUDIO_VIEW
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -77,7 +80,11 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer, ...@@ -77,7 +80,11 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
css_classes = [ css_classes = [
'xblock', 'xblock',
'xblock-{}'.format(markupsafe.escape(view)) 'xblock-{}'.format(markupsafe.escape(view)),
'xblock-{}-{}'.format(
markupsafe.escape(view),
markupsafe.escape(block.scope_ids.block_type),
)
] ]
if isinstance(block, (XModule, XModuleDescriptor)): if isinstance(block, (XModule, XModuleDescriptor)):
...@@ -90,7 +97,7 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer, ...@@ -90,7 +97,7 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
css_classes.append('xmodule_' + markupsafe.escape(class_name)) css_classes.append('xmodule_' + markupsafe.escape(class_name))
data['type'] = block.js_module_name data['type'] = block.js_module_name
shim_xmodule_js(frag) shim_xmodule_js(block, frag)
if frag.js_init_fn: if frag.js_init_fn:
data['init'] = frag.js_init_fn data['init'] = frag.js_init_fn
...@@ -189,6 +196,7 @@ def grade_histogram(module_id): ...@@ -189,6 +196,7 @@ def grade_histogram(module_id):
return grades return grades
@contract(user=User, has_instructor_access=bool, block=XBlock, view=basestring, frag=Fragment, context=dict)
def add_staff_markup(user, has_instructor_access, block, view, frag, context): # pylint: disable=unused-argument def add_staff_markup(user, has_instructor_access, block, view, frag, context): # pylint: disable=unused-argument
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
...@@ -200,7 +208,7 @@ def add_staff_markup(user, has_instructor_access, block, view, frag, context): ...@@ -200,7 +208,7 @@ def add_staff_markup(user, has_instructor_access, block, view, frag, context):
Does nothing if module is a SequenceModule. Does nothing if module is a SequenceModule.
""" """
# TODO: make this more general, eg use an XModule attribute instead # TODO: make this more general, eg use an XModule attribute instead
if isinstance(block, VerticalModule) and (not context or not context.get('child_of_vertical', False)): if isinstance(block, VerticalBlock) and (not context or not context.get('child_of_vertical', False)):
# check that the course is a mongo backed Studio course before doing work # check that the course is a mongo backed Studio course before doing work
is_mongo_course = modulestore().get_modulestore_type(block.location.course_key) != ModuleStoreEnum.Type.xml is_mongo_course = modulestore().get_modulestore_type(block.location.course_key) != ModuleStoreEnum.Type.xml
is_studio_course = block.course_edit_method == "Studio" is_studio_course = block.course_edit_method == "Studio"
......
...@@ -9,6 +9,7 @@ For processing xml always prefer this over using lxml.etree directly. ...@@ -9,6 +9,7 @@ For processing xml always prefer this over using lxml.etree directly.
from lxml.etree import * # pylint: disable=wildcard-import, unused-wildcard-import from lxml.etree import * # pylint: disable=wildcard-import, unused-wildcard-import
from lxml.etree import XMLParser as _XMLParser from lxml.etree import XMLParser as _XMLParser
from lxml.etree import _ElementTree # pylint: disable=unused-import
# This should be imported after lxml.etree so that it overrides the following attributes. # This should be imported after lxml.etree so that it overrides the following attributes.
from defusedxml.lxml import parse, fromstring, XML from defusedxml.lxml import parse, fromstring, XML
......
...@@ -22,7 +22,6 @@ XMODULES = [ ...@@ -22,7 +22,6 @@ XMODULES = [
"section = xmodule.backcompat_module:SemanticSectionDescriptor", "section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor", "slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor", "video = xmodule.video_module:VideoDescriptor",
"videoalpha = xmodule.video_module:VideoDescriptor", "videoalpha = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
...@@ -32,7 +31,6 @@ XMODULES = [ ...@@ -32,7 +31,6 @@ XMODULES = [
"static_tab = xmodule.html_module:StaticTabDescriptor", "static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor", "custom_tag_template = xmodule.raw_module:RawDescriptor",
"about = xmodule.html_module:AboutDescriptor", "about = xmodule.html_module:AboutDescriptor",
"wrapper = xmodule.wrapper_module:WrapperDescriptor",
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor", "graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor", "annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"textannotation = xmodule.textannotation_module:TextAnnotationDescriptor", "textannotation = xmodule.textannotation_module:TextAnnotationDescriptor",
...@@ -47,6 +45,8 @@ XMODULES = [ ...@@ -47,6 +45,8 @@ XMODULES = [
] ]
XBLOCKS = [ XBLOCKS = [
"library = xmodule.library_root_xblock:LibraryRoot", "library = xmodule.library_root_xblock:LibraryRoot",
"vertical = xmodule.vertical_block:VerticalBlock",
"wrapper = xmodule.wrapper_module:WrapperBlock",
] ]
setup( setup(
......
...@@ -273,7 +273,7 @@ function PollMain(el) { ...@@ -273,7 +273,7 @@ function PollMain(el) {
if ( if (
(tempEl.tagName.toLowerCase() === 'div') && (tempEl.tagName.toLowerCase() === 'div') &&
($(tempEl).hasClass('xmodule_WrapperModule') === true) ($(tempEl).data('block-type') === 'wrapper')
) { ) {
_this.wrapperSectionEl = tempEl; _this.wrapperSectionEl = tempEl;
......
...@@ -58,14 +58,20 @@ ...@@ -58,14 +58,20 @@
return Descriptor; return Descriptor;
}()); }());
this.XBlockToXModuleShim = function (runtime, element) { this.XBlockToXModuleShim = function (runtime, element, initArgs) {
/* /*
* Load a single module (either an edit module or a display module) * Load a single module (either an edit module or a display module)
* from the supplied element, which should have a data-type attribute * from the supplied element, which should have a data-type attribute
* specifying the class to load * specifying the class to load
*/ */
var moduleType = $(element).data('type'), var moduleType, module;
module;
if (initArgs) {
moduleType = initArgs['xmodule-type'];
}
if (!moduleType) {
moduleType = $(element).data('type');
}
if (moduleType === 'None') { if (moduleType === 'None') {
return; return;
......
from .x_module import XModuleDescriptor, DescriptorSystem """
Code to handle mako templating for XModules and XBlocks.
"""
from xblock.fragment import Fragment
from .x_module import XModuleDescriptor, DescriptorSystem, shim_xmodule_js
class MakoDescriptorSystem(DescriptorSystem): class MakoDescriptorSystem(DescriptorSystem):
...@@ -8,20 +13,19 @@ class MakoDescriptorSystem(DescriptorSystem): ...@@ -8,20 +13,19 @@ class MakoDescriptorSystem(DescriptorSystem):
self.render_template = render_template self.render_template = render_template
class MakoModuleDescriptor(XModuleDescriptor): class MakoTemplateBlockBase(object):
""" """
Module descriptor intended as a mixin that uses a mako template XBlock intended as a mixin that uses a mako template
to specify the module html. to specify the module html.
Expects the descriptor to have the `mako_template` attribute set Expects the descriptor to have the `mako_template` attribute set
with the name of the template to render, and it will pass with the name of the template to render, and it will pass
the descriptor as the `module` parameter to that template the descriptor as the `module` parameter to that template
MakoModuleDescriptor.__init__ takes the same arguments as xmodule.x_module:XModuleDescriptor.__init__
""" """
# pylint: disable=no-member
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MakoModuleDescriptor, self).__init__(*args, **kwargs) super(MakoTemplateBlockBase, self).__init__(*args, **kwargs)
if getattr(self.runtime, 'render_template', None) is None: if getattr(self.runtime, 'render_template', None) is None:
raise TypeError( raise TypeError(
'{runtime} must have a render_template function' '{runtime} must have a render_template function'
...@@ -39,6 +43,21 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -39,6 +43,21 @@ class MakoModuleDescriptor(XModuleDescriptor):
'editable_metadata_fields': self.editable_metadata_fields 'editable_metadata_fields': self.editable_metadata_fields
} }
def studio_view(self, context): # pylint: disable=unused-argument
"""
View used in Studio.
"""
# pylint: disable=no-member
fragment = Fragment(
self.system.render_template(self.mako_template, self.get_context())
)
shim_xmodule_js(self, fragment)
return fragment
class MakoModuleDescriptor(MakoTemplateBlockBase, XModuleDescriptor): # pylint: disable=abstract-method
"""
Mixin to use for XModule descriptors.
"""
def get_html(self): def get_html(self):
return self.system.render_template( return self.studio_view(None).content
self.mako_template, self.get_context())
...@@ -47,7 +47,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -47,7 +47,7 @@ class DraftModuleStore(MongoModuleStore):
This module also includes functionality to promote DRAFT modules (and their children) This module also includes functionality to promote DRAFT modules (and their children)
to published modules. to published modules.
""" """
def get_item(self, usage_key, depth=0, revision=None, **kwargs): def get_item(self, usage_key, depth=0, revision=None, using_descriptor_system=None, **kwargs):
""" """
Returns an XModuleDescriptor instance for the item at usage_key. Returns an XModuleDescriptor instance for the item at usage_key.
...@@ -70,6 +70,9 @@ class DraftModuleStore(MongoModuleStore): ...@@ -70,6 +70,9 @@ class DraftModuleStore(MongoModuleStore):
Note: If the item is in DIRECT_ONLY_CATEGORIES, then returns only the PUBLISHED Note: If the item is in DIRECT_ONLY_CATEGORIES, then returns only the PUBLISHED
version regardless of the revision. version regardless of the revision.
using_descriptor_system (CachingDescriptorSystem): The existing CachingDescriptorSystem
to add data to, and to load the XBlocks from.
Raises: Raises:
xmodule.modulestore.exceptions.InsufficientSpecificationError xmodule.modulestore.exceptions.InsufficientSpecificationError
if any segment of the usage_key is None except revision if any segment of the usage_key is None except revision
...@@ -78,10 +81,14 @@ class DraftModuleStore(MongoModuleStore): ...@@ -78,10 +81,14 @@ class DraftModuleStore(MongoModuleStore):
is found at that usage_key is found at that usage_key
""" """
def get_published(): def get_published():
return wrap_draft(super(DraftModuleStore, self).get_item(usage_key, depth=depth)) return wrap_draft(super(DraftModuleStore, self).get_item(
usage_key, depth=depth, using_descriptor_system=using_descriptor_system
))
def get_draft(): def get_draft():
return wrap_draft(super(DraftModuleStore, self).get_item(as_draft(usage_key), depth=depth)) return wrap_draft(super(DraftModuleStore, self).get_item(
as_draft(usage_key), depth=depth, using_descriptor_system=using_descriptor_system
))
# return the published version if ModuleStoreEnum.RevisionOption.published_only is requested # return the published version if ModuleStoreEnum.RevisionOption.published_only is requested
if revision == ModuleStoreEnum.RevisionOption.published_only: if revision == ModuleStoreEnum.RevisionOption.published_only:
......
...@@ -20,6 +20,7 @@ from nose.plugins.attrib import attr ...@@ -20,6 +20,7 @@ from nose.plugins.attrib import attr
import pymongo import pymongo
from pytz import UTC from pytz import UTC
from xmodule.x_module import XModuleMixin
from xmodule.modulestore.edit_info import EditInfoMixin from xmodule.modulestore.edit_info import EditInfoMixin
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.modulestore.tests.test_cross_modulestore_import_export import MongoContentstoreBuilder from xmodule.modulestore.tests.test_cross_modulestore_import_export import MongoContentstoreBuilder
...@@ -73,7 +74,7 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -73,7 +74,7 @@ class TestMixedModuleStore(CourseComparisonTest):
'default_class': DEFAULT_CLASS, 'default_class': DEFAULT_CLASS,
'fs_root': DATA_DIR, 'fs_root': DATA_DIR,
'render_template': RENDER_TEMPLATE, 'render_template': RENDER_TEMPLATE,
'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin), 'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin, XModuleMixin),
} }
DOC_STORE_CONFIG = { DOC_STORE_CONFIG = {
'host': HOST, 'host': HOST,
......
...@@ -41,8 +41,10 @@ from git.test.lib.asserts import assert_not_none ...@@ -41,8 +41,10 @@ from git.test.lib.asserts import assert_not_none
from xmodule.x_module import XModuleMixin from xmodule.x_module import XModuleMixin
from xmodule.modulestore.mongo.base import as_draft from xmodule.modulestore.mongo.base import as_draft
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.modulestore.tests.utils import LocationMixin
from xmodule.modulestore.edit_info import EditInfoMixin from xmodule.modulestore.edit_info import EditInfoMixin
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import InheritanceMixin
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -124,7 +126,7 @@ class TestMongoModuleStoreBase(unittest.TestCase): ...@@ -124,7 +126,7 @@ class TestMongoModuleStoreBase(unittest.TestCase):
doc_store_config, FS_ROOT, RENDER_TEMPLATE, doc_store_config, FS_ROOT, RENDER_TEMPLATE,
default_class=DEFAULT_CLASS, default_class=DEFAULT_CLASS,
branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred, branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred,
xblock_mixins=(EditInfoMixin,) xblock_mixins=(EditInfoMixin, InheritanceMixin, LocationMixin, XModuleMixin)
) )
import_course_from_xml( import_course_from_xml(
......
...@@ -8,6 +8,7 @@ import mock ...@@ -8,6 +8,7 @@ import mock
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.x_module import XModuleMixin
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.modulestore.mongo import DraftMongoModuleStore from xmodule.modulestore.mongo import DraftMongoModuleStore
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
...@@ -41,7 +42,7 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase): ...@@ -41,7 +42,7 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase):
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': '', 'fs_root': '',
'render_template': mock.Mock(return_value=""), 'render_template': mock.Mock(return_value=""),
'xblock_mixins': (InheritanceMixin,) 'xblock_mixins': (InheritanceMixin, XModuleMixin)
} }
split_course_key = CourseLocator('test_org', 'test_course', 'runid', branch=ModuleStoreEnum.BranchName.draft) split_course_key = CourseLocator('test_org', 'test_course', 'runid', branch=ModuleStoreEnum.BranchName.draft)
......
...@@ -9,6 +9,7 @@ from mock import patch ...@@ -9,6 +9,7 @@ from mock import patch
from xmodule.modulestore.xml import XMLModuleStore from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.x_module import XModuleMixin
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
...@@ -46,7 +47,11 @@ class TestXMLModuleStore(unittest.TestCase): ...@@ -46,7 +47,11 @@ class TestXMLModuleStore(unittest.TestCase):
# Load the course, but don't make error modules. This will succeed, # Load the course, but don't make error modules. This will succeed,
# but will record the errors. # but will record the errors.
modulestore = XMLModuleStore(DATA_DIR, source_dirs=['toy'], load_error_modules=False) modulestore = XMLModuleStore(
DATA_DIR,
source_dirs=['toy'],
xblock_mixins=(XModuleMixin,),
load_error_modules=False)
# Look up the errors during load. There should be none. # Look up the errors during load. There should be none.
errors = modulestore.get_course_errors(SlashSeparatedCourseKey("edX", "toy", "2012_Fall")) errors = modulestore.get_course_errors(SlashSeparatedCourseKey("edX", "toy", "2012_Fall"))
...@@ -119,7 +124,11 @@ class TestXMLModuleStore(unittest.TestCase): ...@@ -119,7 +124,11 @@ class TestXMLModuleStore(unittest.TestCase):
""" """
Test a course whose structure is not a tree. Test a course whose structure is not a tree.
""" """
store = XMLModuleStore(DATA_DIR, source_dirs=['xml_dag']) store = XMLModuleStore(
DATA_DIR,
source_dirs=['xml_dag'],
xblock_mixins=(XModuleMixin,),
)
course_key = store.get_courses()[0].id course_key = store.get_courses()[0].id
mock_logging.warning.assert_called_with( mock_logging.warning.assert_called_with(
......
...@@ -5,6 +5,7 @@ from importlib import import_module ...@@ -5,6 +5,7 @@ from importlib import import_module
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
from unittest import TestCase from unittest import TestCase
from xblock.fields import XBlockMixin from xblock.fields import XBlockMixin
from xmodule.x_module import XModuleMixin
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from xmodule.modulestore.edit_info import EditInfoMixin from xmodule.modulestore.edit_info import EditInfoMixin
...@@ -84,7 +85,7 @@ class MixedSplitTestCase(TestCase): ...@@ -84,7 +85,7 @@ class MixedSplitTestCase(TestCase):
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': DATA_DIR, 'fs_root': DATA_DIR,
'render_template': RENDER_TEMPLATE, 'render_template': RENDER_TEMPLATE,
'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin), 'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin, XModuleMixin),
} }
DOC_STORE_CONFIG = { DOC_STORE_CONFIG = {
'host': MONGO_HOST, 'host': MONGO_HOST,
......
...@@ -32,7 +32,7 @@ from lxml import etree ...@@ -32,7 +32,7 @@ from lxml import etree
from xmodule.modulestore.xml import XMLModuleStore, LibraryXMLModuleStore, ImportSystem from xmodule.modulestore.xml import XMLModuleStore, LibraryXMLModuleStore, ImportSystem
from xblock.runtime import KvsFieldData, DictKeyValueStore from xblock.runtime import KvsFieldData, DictKeyValueStore
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor, XModuleMixin
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
from xblock.fields import Scope, Reference, ReferenceList, ReferenceValueDict from xblock.fields import Scope, Reference, ReferenceList, ReferenceValueDict
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
...@@ -47,6 +47,7 @@ from xmodule.modulestore.exceptions import DuplicateCourseError ...@@ -47,6 +47,7 @@ from xmodule.modulestore.exceptions import DuplicateCourseError
from xmodule.modulestore.mongo.base import MongoRevisionKey from xmodule.modulestore.mongo.base import MongoRevisionKey
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.store_utilities import draft_node_constructor, get_draft_subtree_roots from xmodule.modulestore.store_utilities import draft_node_constructor, get_draft_subtree_roots
from xmodule.modulestore.tests.utils import LocationMixin
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -1031,7 +1032,8 @@ def validate_course_policy(module_store, course_id): ...@@ -1031,7 +1032,8 @@ def validate_course_policy(module_store, course_id):
def perform_xlint( def perform_xlint(
data_dir, source_dirs, data_dir, source_dirs,
default_class='xmodule.raw_module.RawDescriptor', default_class='xmodule.raw_module.RawDescriptor',
load_error_modules=True): load_error_modules=True,
xblock_mixins=(LocationMixin, XModuleMixin)):
err_cnt = 0 err_cnt = 0
warn_cnt = 0 warn_cnt = 0
...@@ -1039,7 +1041,8 @@ def perform_xlint( ...@@ -1039,7 +1041,8 @@ def perform_xlint(
data_dir, data_dir,
default_class=default_class, default_class=default_class,
source_dirs=source_dirs, source_dirs=source_dirs,
load_error_modules=load_error_modules load_error_modules=load_error_modules,
xblock_mixins=xblock_mixins
) )
# check all data source path information # check all data source path information
......
...@@ -4,13 +4,13 @@ Mixin to support editing in Studio. ...@@ -4,13 +4,13 @@ Mixin to support editing in Studio.
from xmodule.x_module import module_attr, STUDENT_VIEW, AUTHOR_VIEW from xmodule.x_module import module_attr, STUDENT_VIEW, AUTHOR_VIEW
class StudioEditableModule(object): class StudioEditableBlock(object):
""" """
Helper methods for supporting Studio editing of xmodules. Helper methods for supporting Studio editing of XBlocks.
This class is only intended to be used with an XModule, as it assumes the existence of This class is only intended to be used with an XBlock!
self.descriptor and self.system.
""" """
has_author_view = True
def render_children(self, context, fragment, can_reorder=False, can_add=False): def render_children(self, context, fragment, can_reorder=False, can_add=False):
""" """
...@@ -19,15 +19,14 @@ class StudioEditableModule(object): ...@@ -19,15 +19,14 @@ class StudioEditableModule(object):
""" """
contents = [] contents = []
for child in self.descriptor.get_children(): # pylint: disable=no-member for child in self.get_children(): # pylint: disable=no-member
if can_reorder: if can_reorder:
context['reorderable_items'].add(child.location) context['reorderable_items'].add(child.location)
child_module = self.system.get_module(child) # pylint: disable=no-member rendered_child = child.render(StudioEditableModule.get_preview_view_name(child), context)
rendered_child = child_module.render(StudioEditableModule.get_preview_view_name(child_module), context)
fragment.add_frag_resources(rendered_child) fragment.add_frag_resources(rendered_child)
contents.append({ contents.append({
'id': child.location.to_deprecated_string(), 'id': unicode(child.location),
'content': rendered_child.content 'content': rendered_child.content
}) })
...@@ -46,6 +45,9 @@ class StudioEditableModule(object): ...@@ -46,6 +45,9 @@ class StudioEditableModule(object):
return AUTHOR_VIEW if hasattr(block, AUTHOR_VIEW) else STUDENT_VIEW return AUTHOR_VIEW if hasattr(block, AUTHOR_VIEW) else STUDENT_VIEW
StudioEditableModule = StudioEditableBlock
class StudioEditableDescriptor(object): class StudioEditableDescriptor(object):
""" """
Helper mixin for supporting Studio editing of xmodules. Helper mixin for supporting Studio editing of xmodules.
......
...@@ -86,6 +86,21 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method ...@@ -86,6 +86,21 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method
def get_asides(self, block): def get_asides(self, block):
return [] return []
def __repr__(self):
"""
Custom hacky repr.
XBlock.Runtime.render() replaces the _view_name attribute while rendering, which
causes rendered comparisons of blocks to fail as unequal. So make the _view_name
attribute None during the base repr - and set it back to original value afterward.
"""
orig_view_name = None
if hasattr(self, '_view_name'):
orig_view_name = self._view_name
self._view_name = None
rt_repr = super(TestModuleSystem, self).__repr__()
self._view_name = orig_view_name
return rt_repr
def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')): def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')):
""" """
...@@ -128,7 +143,7 @@ def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')): ...@@ -128,7 +143,7 @@ def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')):
render_template=mock_render_template, render_template=mock_render_template,
replace_urls=str, replace_urls=str,
user=user, user=user,
get_real_user=lambda(__): user, get_real_user=lambda __: user,
filestore=Mock(name='get_test_system.filestore'), filestore=Mock(name='get_test_system.filestore'),
debug=True, debug=True,
hostname="edx.org", hostname="edx.org",
...@@ -363,11 +378,17 @@ class CourseComparisonTest(BulkAssertionTest): ...@@ -363,11 +378,17 @@ class CourseComparisonTest(BulkAssertionTest):
) )
def assertBlocksEqualByFields(self, expected_block, actual_block): def assertBlocksEqualByFields(self, expected_block, actual_block):
"""
Compare block fields to check for equivalence.
"""
self.assertEqual(expected_block.fields, actual_block.fields) self.assertEqual(expected_block.fields, actual_block.fields)
for field in expected_block.fields.values(): for field in expected_block.fields.values():
self.assertFieldEqual(field, expected_block, actual_block) self.assertFieldEqual(field, expected_block, actual_block)
def assertFieldEqual(self, field, expected_block, actual_block): def assertFieldEqual(self, field, expected_block, actual_block):
"""
Compare a single block field for equivalence.
"""
if isinstance(field, (Reference, ReferenceList, ReferenceValueDict)): if isinstance(field, (Reference, ReferenceList, ReferenceValueDict)):
self.assertReferenceRelativelyEqual(field, expected_block, actual_block) self.assertReferenceRelativelyEqual(field, expected_block, actual_block)
else: else:
...@@ -421,6 +442,9 @@ class CourseComparisonTest(BulkAssertionTest): ...@@ -421,6 +442,9 @@ class CourseComparisonTest(BulkAssertionTest):
self._assertCoursesEqual(expected_items, actual_items, actual_course_key, expect_drafts=True) self._assertCoursesEqual(expected_items, actual_items, actual_course_key, expect_drafts=True)
def _assertCoursesEqual(self, expected_items, actual_items, actual_course_key, expect_drafts=False): def _assertCoursesEqual(self, expected_items, actual_items, actual_course_key, expect_drafts=False):
"""
Actual algorithm to compare courses.
"""
with self.bulk_assertions(): with self.bulk_assertions():
self.assertEqual(len(expected_items), len(actual_items)) self.assertEqual(len(expected_items), len(actual_items))
......
...@@ -7,7 +7,7 @@ import unittest ...@@ -7,7 +7,7 @@ import unittest
import copy import copy
from xmodule.crowdsource_hinter import CrowdsourceHinterModule from xmodule.crowdsource_hinter import CrowdsourceHinterModule
from xmodule.vertical_module import VerticalModule, VerticalDescriptor from xmodule.vertical_block import VerticalBlock
from xmodule.x_module import STUDENT_VIEW from xmodule.x_module import STUDENT_VIEW
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
from xblock.fragment import Fragment from xblock.fragment import Fragment
...@@ -203,8 +203,8 @@ class VerticalWithModulesFactory(object): ...@@ -203,8 +203,8 @@ class VerticalWithModulesFactory(object):
"""Make a vertical.""" """Make a vertical."""
field_data = {'data': VerticalWithModulesFactory.sample_problem_xml} field_data = {'data': VerticalWithModulesFactory.sample_problem_xml}
system = get_test_system() system = get_test_system()
descriptor = VerticalDescriptor.from_xml(VerticalWithModulesFactory.sample_problem_xml, system) descriptor = VerticalBlock.parse_xml(VerticalWithModulesFactory.sample_problem_xml, system)
module = VerticalModule(system, descriptor, field_data) module = VerticalBlock(system, descriptor, field_data)
return module return module
......
...@@ -2,11 +2,14 @@ ...@@ -2,11 +2,14 @@
Tests for StudioEditableModule. Tests for StudioEditableModule.
""" """
from xmodule.tests.test_vertical import BaseVerticalModuleTest from xmodule.tests.test_vertical import BaseVerticalBlockTest
from xmodule.x_module import AUTHOR_VIEW from xmodule.x_module import AUTHOR_VIEW
class StudioEditableModuleTestCase(BaseVerticalModuleTest): class StudioEditableModuleTestCase(BaseVerticalBlockTest):
"""
Class containing StudioEditableModule tests.
"""
def test_render_reorderable_children(self): def test_render_reorderable_children(self):
""" """
Test the behavior of render_reorderable_children. Test the behavior of render_reorderable_children.
......
...@@ -9,12 +9,15 @@ from xmodule.tests.xml import factories as xml ...@@ -9,12 +9,15 @@ from xmodule.tests.xml import factories as xml
from xmodule.x_module import STUDENT_VIEW, AUTHOR_VIEW from xmodule.x_module import STUDENT_VIEW, AUTHOR_VIEW
class BaseVerticalModuleTest(XModuleXmlImportTest): class BaseVerticalBlockTest(XModuleXmlImportTest):
"""
Tests for the BaseVerticalBlock.
"""
test_html_1 = 'Test HTML 1' test_html_1 = 'Test HTML 1'
test_html_2 = 'Test HTML 2' test_html_2 = 'Test HTML 2'
def setUp(self): def setUp(self):
super(BaseVerticalModuleTest, self).setUp() super(BaseVerticalBlockTest, self).setUp()
# construct module # construct module
course = xml.CourseFactory.build() course = xml.CourseFactory.build()
sequence = xml.SequenceFactory.build(parent=course) sequence = xml.SequenceFactory.build(parent=course)
...@@ -35,7 +38,10 @@ class BaseVerticalModuleTest(XModuleXmlImportTest): ...@@ -35,7 +38,10 @@ class BaseVerticalModuleTest(XModuleXmlImportTest):
self.vertical.xmodule_runtime = self.module_system self.vertical.xmodule_runtime = self.module_system
class VerticalModuleTestCase(BaseVerticalModuleTest): class VerticalBlockTestCase(BaseVerticalBlockTest):
"""
Tests for the VerticalBlock.
"""
def test_render_student_view(self): def test_render_student_view(self):
""" """
Test the rendering of the student view. Test the rendering of the student view.
......
...@@ -23,6 +23,7 @@ from unittest.case import SkipTest, TestCase ...@@ -23,6 +23,7 @@ from unittest.case import SkipTest, TestCase
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from xblock.core import XBlock
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
...@@ -42,8 +43,8 @@ from xmodule.crowdsource_hinter import CrowdsourceHinterDescriptor ...@@ -42,8 +43,8 @@ from xmodule.crowdsource_hinter import CrowdsourceHinterDescriptor
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from xmodule.conditional_module import ConditionalDescriptor from xmodule.conditional_module import ConditionalDescriptor
from xmodule.randomize_module import RandomizeDescriptor from xmodule.randomize_module import RandomizeDescriptor
from xmodule.vertical_module import VerticalDescriptor from xmodule.vertical_block import VerticalBlock
from xmodule.wrapper_module import WrapperDescriptor from xmodule.wrapper_module import WrapperBlock
from xmodule.tests import get_test_descriptor_system, get_test_system from xmodule.tests import get_test_descriptor_system, get_test_system
...@@ -74,8 +75,8 @@ CONTAINER_XMODULES = { ...@@ -74,8 +75,8 @@ CONTAINER_XMODULES = {
CrowdsourceHinterDescriptor: [{}], CrowdsourceHinterDescriptor: [{}],
RandomizeDescriptor: [{}], RandomizeDescriptor: [{}],
SequenceDescriptor: [{}], SequenceDescriptor: [{}],
VerticalDescriptor: [{}], VerticalBlock: [{}],
WrapperDescriptor: [{}], WrapperBlock: [{}],
} }
# These modules are editable in studio yet # These modules are editable in studio yet
...@@ -141,7 +142,10 @@ class ContainerModuleRuntimeFactory(ModuleSystemFactory): ...@@ -141,7 +142,10 @@ class ContainerModuleRuntimeFactory(ModuleSystemFactory):
if depth == 0: if depth == 0:
self.get_module.side_effect = lambda x: LeafModuleFactory(descriptor_cls=HtmlDescriptor) self.get_module.side_effect = lambda x: LeafModuleFactory(descriptor_cls=HtmlDescriptor)
else: else:
self.get_module.side_effect = lambda x: ContainerModuleFactory(descriptor_cls=VerticalDescriptor, depth=depth - 1) self.get_module.side_effect = lambda x: ContainerModuleFactory(
descriptor_cls=VerticalBlock,
depth=depth - 1
)
@post_generation @post_generation
def position(self, create, position=2, **kwargs): # pylint: disable=unused-argument, method-hidden def position(self, create, position=2, **kwargs): # pylint: disable=unused-argument, method-hidden
...@@ -166,7 +170,10 @@ class ContainerDescriptorRuntimeFactory(DescriptorSystemFactory): ...@@ -166,7 +170,10 @@ class ContainerDescriptorRuntimeFactory(DescriptorSystemFactory):
if depth == 0: if depth == 0:
self.load_item.side_effect = lambda x: LeafModuleFactory(descriptor_cls=HtmlDescriptor) self.load_item.side_effect = lambda x: LeafModuleFactory(descriptor_cls=HtmlDescriptor)
else: else:
self.load_item.side_effect = lambda x: ContainerModuleFactory(descriptor_cls=VerticalDescriptor, depth=depth - 1) self.load_item.side_effect = lambda x: ContainerModuleFactory(
descriptor_cls=VerticalBlock,
depth=depth - 1
)
@post_generation @post_generation
def position(self, create, position=2, **kwargs): # pylint: disable=unused-argument, method-hidden def position(self, create, position=2, **kwargs): # pylint: disable=unused-argument, method-hidden
...@@ -323,7 +330,12 @@ class TestStudentView(XBlockWrapperTestMixin, TestCase): ...@@ -323,7 +330,12 @@ class TestStudentView(XBlockWrapperTestMixin, TestCase):
This tests that student_view and XModule.get_html produce the same results. This tests that student_view and XModule.get_html produce the same results.
""" """
def skip_if_invalid(self, descriptor_cls): def skip_if_invalid(self, descriptor_cls):
if descriptor_cls.module_class.student_view != XModule.student_view: pure_xblock_class = issubclass(descriptor_cls, XBlock) and not issubclass(descriptor_cls, XModuleDescriptor)
if pure_xblock_class:
student_view = descriptor_cls.student_view
else:
student_view = descriptor_cls.module_class.student_view
if student_view != XModule.student_view:
raise SkipTest(descriptor_cls.__name__ + " implements student_view") raise SkipTest(descriptor_cls.__name__ + " implements student_view")
def check_property(self, descriptor): def check_property(self, descriptor):
...@@ -344,7 +356,10 @@ class TestStudioView(XBlockWrapperTestMixin, TestCase): ...@@ -344,7 +356,10 @@ class TestStudioView(XBlockWrapperTestMixin, TestCase):
if descriptor_cls in NOT_STUDIO_EDITABLE: if descriptor_cls in NOT_STUDIO_EDITABLE:
raise SkipTest(descriptor_cls.__name__ + " is not editable in studio") raise SkipTest(descriptor_cls.__name__ + " is not editable in studio")
if descriptor_cls.studio_view != XModuleDescriptor.studio_view: pure_xblock_class = issubclass(descriptor_cls, XBlock) and not issubclass(descriptor_cls, XModuleDescriptor)
if pure_xblock_class:
raise SkipTest(descriptor_cls.__name__ + " is a pure XBlock and implements studio_view")
elif descriptor_cls.studio_view != XModuleDescriptor.studio_view:
raise SkipTest(descriptor_cls.__name__ + " implements studio_view") raise SkipTest(descriptor_cls.__name__ + " implements studio_view")
def check_property(self, descriptor): def check_property(self, descriptor):
......
...@@ -9,6 +9,7 @@ from factory import Factory, lazy_attribute, post_generation, Sequence ...@@ -9,6 +9,7 @@ from factory import Factory, lazy_attribute, post_generation, Sequence
from lxml import etree from lxml import etree
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.x_module import XModuleMixin
from xmodule.modulestore import only_xmodules from xmodule.modulestore import only_xmodules
...@@ -66,7 +67,7 @@ class XmlImportFactory(Factory): ...@@ -66,7 +67,7 @@ class XmlImportFactory(Factory):
FACTORY_FOR = XmlImportData FACTORY_FOR = XmlImportData
filesystem = MemoryFS() filesystem = MemoryFS()
xblock_mixins = (InheritanceMixin,) xblock_mixins = (InheritanceMixin, XModuleMixin)
xblock_select = only_xmodules xblock_select = only_xmodules
url_name = Sequence(str) url_name = Sequence(str)
attribs = {} attribs = {}
......
"""
VerticalBlock - an XBlock which renders its children in a column.
"""
import logging
from copy import copy
from lxml import etree
from xblock.core import XBlock
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xmodule.x_module import XModule, STUDENT_VIEW from xmodule.mako_module import MakoTemplateBlockBase
from xmodule.seq_module import SequenceDescriptor
from xmodule.progress import Progress from xmodule.progress import Progress
from xmodule.studio_editable import StudioEditableModule, StudioEditableDescriptor from xmodule.seq_module import SequenceFields
from pkg_resources import resource_string from xmodule.studio_editable import StudioEditableBlock
from copy import copy from xmodule.x_module import STUDENT_VIEW, XModuleFields
from xmodule.xml_module import XmlParserMixin
log = logging.getLogger(__name__)
# HACK: This shouldn't be hard-coded to two types # HACK: This shouldn't be hard-coded to two types
# OBSOLETE: This obsoletes 'type' # OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem'] CLASS_PRIORITY = ['video', 'problem']
class VerticalFields(object):
has_children = True
class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParserMixin, MakoTemplateBlockBase, XBlock):
"""
Layout XBlock for rendering subblocks vertically.
"""
mako_template = 'widgets/sequence-edit.html'
js_module_name = "VerticalBlock"
class VerticalModule(VerticalFields, XModule, StudioEditableModule): has_children = True
''' Layout module for laying out submodules vertically.'''
def student_view(self, context): def student_view(self, context):
"""
Renders the student view of the block in the LMS.
"""
fragment = Fragment() fragment = Fragment()
contents = [] contents = []
child_context = {} if not context else copy(context) child_context = {} if not context else copy(context)
child_context['child_of_vertical'] = True child_context['child_of_vertical'] = True
# pylint: disable=no-member
for child in self.get_display_items(): for child in self.get_display_items():
rendered_child = child.render(STUDENT_VIEW, child_context) rendered_child = child.render(STUDENT_VIEW, child_context)
fragment.add_frag_resources(rendered_child) fragment.add_frag_resources(rendered_child)
...@@ -47,7 +61,7 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule): ...@@ -47,7 +61,7 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
""" """
fragment = Fragment() fragment = Fragment()
root_xblock = context.get('root_xblock') root_xblock = context.get('root_xblock')
is_root = root_xblock and root_xblock.location == self.location is_root = root_xblock and root_xblock.location == self.location # pylint: disable=no-member
# For the container page we want the full drag-and-drop, but for unit pages we want # For the container page we want the full drag-and-drop, but for unit pages we want
# a more concise version that appears alongside the "View =>" link-- unless it is # a more concise version that appears alongside the "View =>" link-- unless it is
...@@ -57,37 +71,60 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule): ...@@ -57,37 +71,60 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
return fragment return fragment
def get_progress(self): def get_progress(self):
"""
Returns the progress on this block and all children.
"""
# TODO: Cache progress or children array? # TODO: Cache progress or children array?
children = self.get_children() children = self.get_children() # pylint: disable=no-member
progresses = [child.get_progress() for child in children] progresses = [child.get_progress() for child in children]
progress = reduce(Progress.add_counts, progresses, None) progress = reduce(Progress.add_counts, progresses, None)
return progress return progress
def get_icon_class(self): def get_icon_class(self):
child_classes = set(child.get_icon_class() for child in self.get_children()) """
Returns the highest priority icon class.
"""
child_classes = set(child.get_icon_class() for child in self.get_children()) # pylint: disable=no-member
new_class = 'other' new_class = 'other'
for c in class_priority: for higher_class in CLASS_PRIORITY:
if c in child_classes: if higher_class in child_classes:
new_class = c new_class = higher_class
return new_class return new_class
@classmethod
class VerticalDescriptor(VerticalFields, SequenceDescriptor, StudioEditableDescriptor): def definition_from_xml(cls, xml_object, system):
""" children = []
Descriptor class for editing verticals. for child in xml_object:
""" try:
module_class = VerticalModule child_block = system.process_xml(etree.tostring(child, encoding='unicode')) # pylint: disable=no-member
children.append(child_block.scope_ids.usage_id)
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]} except Exception as exc: # pylint: disable=broad-except
js_module_name = "VerticalDescriptor" log.exception("Unable to load child when parsing Vertical. Continuing...")
if system.error_tracker is not None:
# TODO (victor): Does this need its own definition_to_xml method? Otherwise it looks system.error_tracker(u"ERROR: {0}".format(exc))
# like verticals will get exported as sequentials... continue
return {}, children
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('vertical') # pylint: disable=no-member
for child in self.get_children(): # pylint: disable=no-member
self.runtime.add_block_as_child_node(child, xml_object)
return xml_object
@property @property
def non_editable_metadata_fields(self): def non_editable_metadata_fields(self):
non_editable_fields = super(VerticalDescriptor, self).non_editable_metadata_fields """
Gather all fields which can't be edited.
"""
non_editable_fields = super(VerticalBlock, self).non_editable_metadata_fields
non_editable_fields.extend([ non_editable_fields.extend([
VerticalDescriptor.due, self.fields['due'],
]) ])
return non_editable_fields return non_editable_fields
def studio_view(self, context):
fragment = super(VerticalBlock, self).studio_view(context)
# This continues to use the old XModuleDescriptor javascript code to enabled studio editing.
# TODO: Remove this when studio better supports editing of pure XBlocks.
fragment.add_javascript('VerticalBlock = XModule.Descriptor;')
return fragment
# Same as vertical, # Same as vertical,
# But w/o css delimiters between children # But w/o css delimiters between children
from xmodule.vertical_module import VerticalModule, VerticalDescriptor from xmodule.vertical_block import VerticalBlock
from pkg_resources import resource_string from pkg_resources import resource_string
# HACK: This shouldn't be hard-coded to two types # HACK: This shouldn't be hard-coded to two types
...@@ -9,14 +9,8 @@ from pkg_resources import resource_string ...@@ -9,14 +9,8 @@ from pkg_resources import resource_string
class_priority = ['video', 'problem'] class_priority = ['video', 'problem']
class WrapperModule(VerticalModule): class WrapperBlock(VerticalBlock):
''' Layout module for laying out submodules vertically w/o css delimiters''' '''
Layout block for laying out sub-blocks vertically *w/o* css delimiters.
has_children = True '''
css = {'scss': [resource_string(__name__, 'css/wrapper/display.scss')]} pass
class WrapperDescriptor(VerticalDescriptor):
module_class = WrapperModule
has_children = True
...@@ -239,15 +239,30 @@ class HTMLSnippet(object): ...@@ -239,15 +239,30 @@ class HTMLSnippet(object):
.format(self.__class__)) .format(self.__class__))
def shim_xmodule_js(fragment): def shim_xmodule_js(block, fragment):
""" """
Set up the XBlock -> XModule shim on the supplied :class:`xblock.fragment.Fragment` Set up the XBlock -> XModule shim on the supplied :class:`xblock.fragment.Fragment`
""" """
if not fragment.js_init_fn: if not fragment.js_init_fn:
fragment.initialize_js('XBlockToXModuleShim') fragment.initialize_js('XBlockToXModuleShim')
fragment.json_init_args = {'xmodule-type': block.js_module_name}
class XModuleMixin(XBlockMixin): class XModuleFields(object):
"""
Common fields for XModules.
"""
display_name = String(
display_name="Display Name",
help="This name appears in the horizontal navigation at the top of the page.",
scope=Scope.settings,
# it'd be nice to have a useful default but it screws up other things; so,
# use display_name_with_default for those
default=None
)
class XModuleMixin(XModuleFields, XBlockMixin):
""" """
Fields and methods used by XModules internally. Fields and methods used by XModules internally.
...@@ -278,15 +293,6 @@ class XModuleMixin(XBlockMixin): ...@@ -278,15 +293,6 @@ class XModuleMixin(XBlockMixin):
# in the module # in the module
icon_class = 'other' icon_class = 'other'
display_name = String(
display_name="Display Name",
help="This name appears in the horizontal navigation at the top of the page.",
scope=Scope.settings,
# it'd be nice to have a useful default but it screws up other things; so,
# use display_name_with_default for those
default=None
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.xmodule_runtime = None self.xmodule_runtime = None
self._child_instances = None self._child_instances = None
...@@ -571,6 +577,95 @@ class XModuleMixin(XBlockMixin): ...@@ -571,6 +577,95 @@ class XModuleMixin(XBlockMixin):
self.xmodule_runtime = xmodule_runtime self.xmodule_runtime = xmodule_runtime
self._field_data = field_data self._field_data = field_data
@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):
"""
Convert field value to JSON, if needed.
"""
if isinstance(json_choice, dict):
new_json_choice = dict(json_choice) # make a copy so below doesn't change the original
if 'display_name' in json_choice:
new_json_choice['display_name'] = get_text(json_choice['display_name'])
if 'value' in json_choice:
new_json_choice['value'] = field.to_json(json_choice['value'])
else:
new_json_choice = field.to_json(json_choice)
return new_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 "values_provider" in field.runtime_options:
values = field.runtime_options['values_provider'](self)
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
class ProxyAttribute(object): class ProxyAttribute(object):
""" """
...@@ -965,92 +1060,6 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -965,92 +1060,6 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
")".format(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 "values_provider" in field.runtime_options:
values = field.runtime_options['values_provider'](self)
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 ~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~ XModule Indirection ~~~~~~~~~~~~~~~~
@property @property
def _xmodule(self): def _xmodule(self):
......
...@@ -13,12 +13,16 @@ from xmodule.modulestore import EdxJSONEncoder ...@@ -13,12 +13,16 @@ from xmodule.modulestore import EdxJSONEncoder
import dogstats_wrapper as dog_stats_api import dogstats_wrapper as dog_stats_api
from lxml.etree import ( # pylint: disable=no-name-in-module
Element, ElementTree, XMLParser,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# assume all XML files are persisted as utf-8. # assume all XML files are persisted as utf-8.
edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False, EDX_XML_PARSER = XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True, remove_comments=True, remove_blank_text=True,
encoding='utf-8') encoding='utf-8')
def name_to_pathname(name): def name_to_pathname(name):
...@@ -53,16 +57,6 @@ def is_pointer_tag(xml_obj): ...@@ -53,16 +57,6 @@ def is_pointer_tag(xml_obj):
return len(xml_obj) == 0 and actual_attr == expected_attr and not has_text return len(xml_obj) == 0 and actual_attr == expected_attr and not has_text
def get_metadata_from_xml(xml_object, remove=True):
meta = xml_object.find('meta')
if meta is None:
return ''
dmdata = meta.text
if remove:
xml_object.remove(meta)
return dmdata
def serialize_field(value): def serialize_field(value):
""" """
Return a string version of the value (where value is the JSON-formatted, internally stored value). Return a string version of the value (where value is the JSON-formatted, internally stored value).
...@@ -108,16 +102,30 @@ def deserialize_field(field, value): ...@@ -108,16 +102,30 @@ def deserialize_field(field, value):
return value return value
class XmlDescriptor(XModuleDescriptor): class XmlParserMixin(object):
""" """
Mixin class for standardized parsing of from xml Class containing XML parsing functionality shared between XBlock and XModuleDescriptor.
""" """
# Extension to append to filename paths
filename_extension = 'xml'
xml_attributes = Dict(help="Map of unhandled xml attributes, used only for storage between import and export", xml_attributes = Dict(help="Map of unhandled xml attributes, used only for storage between import and export",
default={}, scope=Scope.settings) default={}, scope=Scope.settings)
# Extension to append to filename paths # VS[compat]. Backwards compatibility code that can go away after
filename_extension = 'xml' # importing 2012 courses.
# A set of metadata key conversions that we want to make
metadata_translations = {
'slug': 'url_name',
'name': 'display_name',
}
@classmethod
def _translate(cls, key):
"""
VS[compat]
"""
return cls.metadata_translations.get(key, key)
# The attributes will be removed from the definition xml passed # The attributes will be removed from the definition xml passed
# to definition_from_xml, and from the xml returned by definition_to_xml # to definition_from_xml, and from the xml returned by definition_to_xml
...@@ -135,6 +143,19 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -135,6 +143,19 @@ class XmlDescriptor(XModuleDescriptor):
metadata_to_export_to_policy = ('discussion_topics', 'checklists') metadata_to_export_to_policy = ('discussion_topics', 'checklists')
@staticmethod
def _get_metadata_from_xml(xml_object, remove=True):
"""
Extract the metadata from the XML.
"""
meta = xml_object.find('meta')
if meta is None:
return ''
dmdata = meta.text
if remove:
xml_object.remove(meta)
return dmdata
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
""" """
...@@ -163,16 +184,16 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -163,16 +184,16 @@ class XmlDescriptor(XModuleDescriptor):
Returns an lxml Element Returns an lxml Element
""" """
return etree.parse(file_object, parser=edx_xml_parser).getroot() return etree.parse(file_object, parser=EDX_XML_PARSER).getroot() # pylint: disable=no-member
@classmethod @classmethod
def load_file(cls, filepath, fs, def_id): # pylint: disable=invalid-name def load_file(cls, filepath, fs, def_id): # pylint: disable=invalid-name
''' """
Open the specified file in fs, and call cls.file_to_xml on it, Open the specified file in fs, and call cls.file_to_xml on it,
returning the lxml object. returning the lxml object.
Add details and reraise on error. Add details and reraise on error.
''' """
try: try:
with fs.open(filepath) as xml_file: with fs.open(filepath) as xml_file:
return cls.file_to_xml(xml_file) return cls.file_to_xml(xml_file)
...@@ -184,7 +205,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -184,7 +205,7 @@ class XmlDescriptor(XModuleDescriptor):
@classmethod @classmethod
def load_definition(cls, xml_object, system, def_id, id_generator): def load_definition(cls, xml_object, system, def_id, id_generator):
''' """
Load a descriptor definition from the specified xml_object. Load a descriptor definition from the specified xml_object.
Subclasses should not need to override this except in special Subclasses should not need to override this except in special
cases (e.g. html module) cases (e.g. html module)
...@@ -194,7 +215,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -194,7 +215,7 @@ class XmlDescriptor(XModuleDescriptor):
system: the modulestore system (aka, runtime) which accesses data and provides access to services system: the modulestore system (aka, runtime) which accesses data and provides access to services
def_id: the definition id for the block--used to compute the usage id and asides ids def_id: the definition id for the block--used to compute the usage id and asides ids
id_generator: used to generate the usage_id id_generator: used to generate the usage_id
''' """
# VS[compat] -- the filename attr should go away once everything is # VS[compat] -- the filename attr should go away once everything is
# converted. (note: make sure html files still work once this goes away) # converted. (note: make sure html files still work once this goes away)
...@@ -234,7 +255,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -234,7 +255,7 @@ class XmlDescriptor(XModuleDescriptor):
# Add the attributes from the pointer node # Add the attributes from the pointer node
definition_xml.attrib.update(xml_object.attrib) definition_xml.attrib.update(xml_object.attrib)
definition_metadata = get_metadata_from_xml(definition_xml) definition_metadata = cls._get_metadata_from_xml(definition_xml)
cls.clean_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml)
definition, children = cls.definition_from_xml(definition_xml, system) definition, children = cls.definition_from_xml(definition_xml, system)
if definition_metadata: if definition_metadata:
...@@ -289,42 +310,51 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -289,42 +310,51 @@ class XmlDescriptor(XModuleDescriptor):
metadata[attr] = value metadata[attr] = value
@classmethod @classmethod
def from_xml(cls, xml_data, system, id_generator): def parse_xml(cls, node, runtime, keys, id_generator): # pylint: disable=unused-argument
""" """
Creates an instance of this descriptor from the supplied xml_data. Use `node` to construct a new block.
This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for Arguments:
this module node (etree.Element): The xml node to parse into an xblock.
system: A DescriptorSystem for interacting with external resources
""" runtime (:class:`.Runtime`): The runtime to use while parsing.
keys (:class:`.ScopeIds`): The keys identifying where this block
will store its data.
id_generator (:class:`.IdGenerator`): An object that will allow the
runtime to generate correct definition and usage ids for
children of this block.
Returns (XBlock): The newly parsed XBlock
xml_object = etree.fromstring(xml_data) """
# VS[compat] -- just have the url_name lookup, once translation is done # VS[compat] -- just have the url_name lookup, once translation is done
url_name = xml_object.get('url_name', xml_object.get('slug')) url_name = node.get('url_name', node.get('slug'))
def_id = id_generator.create_definition(xml_object.tag, url_name) def_id = id_generator.create_definition(node.tag, url_name)
usage_id = id_generator.create_usage(def_id) usage_id = id_generator.create_usage(def_id)
# VS[compat] -- detect new-style each-in-a-file mode # VS[compat] -- detect new-style each-in-a-file mode
if is_pointer_tag(xml_object): if is_pointer_tag(node):
# new style: # new style:
# read the actual definition file--named using url_name.replace(':','/') # read the actual definition file--named using url_name.replace(':','/')
filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name)) filepath = cls._format_filepath(node.tag, name_to_pathname(url_name))
definition_xml = cls.load_file(filepath, system.resources_fs, def_id) definition_xml = cls.load_file(filepath, runtime.resources_fs, def_id)
system.parse_asides(definition_xml, def_id, usage_id, id_generator) runtime.parse_asides(definition_xml, def_id, usage_id, id_generator)
else: else:
filepath = None filepath = None
definition_xml = xml_object definition_xml = node
dog_stats_api.increment( dog_stats_api.increment(
DEPRECATION_VSCOMPAT_EVENT, DEPRECATION_VSCOMPAT_EVENT,
tags=["location:xmlparser_util_mixin_parse_xml"] tags=["location:xmlparser_util_mixin_parse_xml"]
) )
definition, children = cls.load_definition(definition_xml, system, def_id, id_generator) # note this removes metadata # Note: removes metadata.
definition, children = cls.load_definition(definition_xml, runtime, def_id, id_generator)
# VS[compat] -- make Ike's github preview links work in both old and # VS[compat] -- make Ike's github preview links work in both old and
# new file layouts # new file layouts
if is_pointer_tag(xml_object): if is_pointer_tag(node):
# new style -- contents actually at filepath # new style -- contents actually at filepath
definition['filename'] = [filepath, filepath] definition['filename'] = [filepath, filepath]
...@@ -341,7 +371,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -341,7 +371,7 @@ class XmlDescriptor(XModuleDescriptor):
metadata['definition_metadata_err'] = str(err) metadata['definition_metadata_err'] = str(err)
# Set/override any metadata specified by policy # Set/override any metadata specified by policy
cls.apply_policy(metadata, system.get_policy(usage_id)) cls.apply_policy(metadata, runtime.get_policy(usage_id))
field_data = {} field_data = {}
field_data.update(metadata) field_data.update(metadata)
...@@ -352,10 +382,10 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -352,10 +382,10 @@ class XmlDescriptor(XModuleDescriptor):
kvs = InheritanceKeyValueStore(initial_values=field_data) kvs = InheritanceKeyValueStore(initial_values=field_data)
field_data = KvsFieldData(kvs) field_data = KvsFieldData(kvs)
return system.construct_xblock_from_class( return runtime.construct_xblock_from_class(
cls, cls,
# We're loading a descriptor, so student_id is meaningless # We're loading a descriptor, so student_id is meaningless
ScopeIds(None, xml_object.tag, def_id, usage_id), ScopeIds(None, node.tag, def_id, usage_id),
field_data, field_data,
) )
...@@ -374,32 +404,17 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -374,32 +404,17 @@ class XmlDescriptor(XModuleDescriptor):
""" """
return True return True
def export_to_xml(self, resource_fs): def add_xml_to_node(self, node):
""" """
Returns an xml string representing this module, and all modules For exporting, set data on `node` from ourselves.
underneath it. May also write required resources out to resource_fs
Assumes that modules have single parentage (that no module appears twice
in the same course), and that it is thus safe to nest modules as xml
children as appropriate.
The returned XML should be able to be parsed back into an identical
XModuleDescriptor using the from_xml method with the same system, org,
and course
resource_fs is a pyfilesystem object (from the fs package)
""" """
# Set up runtime.export_fs so that it's available through future
# uses of the pure xblock add_xml_to_node api
self.runtime.export_fs = resource_fs
# Get the definition # Get the definition
xml_object = self.definition_to_xml(resource_fs) xml_object = self.definition_to_xml(self.runtime.export_fs)
self.clean_metadata_from_xml(xml_object) self.clean_metadata_from_xml(xml_object)
# Set the tag so we get the file path right # Set the tag on both nodes so we get the file path right.
xml_object.tag = self.category xml_object.tag = self.category
node.tag = self.category
# Add the non-inherited metadata # Add the non-inherited metadata
for attr in sorted(own_metadata(self)): for attr in sorted(own_metadata(self)):
...@@ -422,24 +437,25 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -422,24 +437,25 @@ class XmlDescriptor(XModuleDescriptor):
# Write the definition to a file # Write the definition to a file
url_path = name_to_pathname(self.url_name) url_path = name_to_pathname(self.url_name)
filepath = self._format_filepath(self.category, url_path) filepath = self._format_filepath(self.category, url_path)
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True) self.runtime.export_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
with resource_fs.open(filepath, 'w') as fileobj: with self.runtime.export_fs.open(filepath, 'w') as fileobj:
fileobj.write(etree.tostring(xml_object, pretty_print=True, encoding='utf-8')) ElementTree(xml_object).write(fileobj, pretty_print=True, encoding='utf-8')
# And return just a pointer with the category and filename.
record_object = etree.Element(self.category)
else: else:
record_object = xml_object # Write all attributes from xml_object onto node
node.clear()
node.tag = xml_object.tag
node.text = xml_object.text
node.tail = xml_object.tail
node.attrib = xml_object.attrib
node.extend(xml_object)
record_object.set('url_name', self.url_name) node.set('url_name', self.url_name)
# Special case for course pointers: # Special case for course pointers:
if self.category == 'course': if self.category == 'course':
# add org and course attributes on the pointer tag # add org and course attributes on the pointer tag
record_object.set('org', self.location.org) node.set('org', self.location.org)
record_object.set('course', self.location.course) node.set('course', self.location.course)
return etree.tostring(record_object, pretty_print=True, encoding='utf-8')
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
""" """
...@@ -450,6 +466,86 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -450,6 +466,86 @@ class XmlDescriptor(XModuleDescriptor):
@property @property
def non_editable_metadata_fields(self): def non_editable_metadata_fields(self):
non_editable_fields = super(XmlDescriptor, self).non_editable_metadata_fields """
non_editable_fields.append(XmlDescriptor.xml_attributes) Return a list of all metadata fields that cannot be edited.
"""
non_editable_fields = super(XmlParserMixin, self).non_editable_metadata_fields
non_editable_fields.append(XmlParserMixin.xml_attributes)
return non_editable_fields return non_editable_fields
class XmlDescriptor(XmlParserMixin, XModuleDescriptor): # pylint: disable=abstract-method
"""
Mixin class for standardized parsing of XModule xml.
"""
@classmethod
def from_xml(cls, xml_data, system, id_generator):
"""
Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses.
Args:
xml_data (str): A string of xml that will be translated into data and children
for this module
system (:class:`.XMLParsingSystem):
id_generator (:class:`xblock.runtime.IdGenerator`): Used to generate the
usage_ids and definition_ids when loading this xml
"""
# Shim from from_xml to the parse_xml defined in XmlParserMixin.
# This only exists to satisfy subclasses that both:
# a) define from_xml themselves
# b) call super(..).from_xml(..)
return super(XmlDescriptor, cls).parse_xml(
etree.fromstring(xml_data), # pylint: disable=no-member
system,
None, # This is ignored by XmlParserMixin
id_generator,
)
@classmethod
def parse_xml(cls, node, runtime, keys, id_generator):
"""
Interpret the parsed XML in `node`, creating an XModuleDescriptor.
"""
if cls.from_xml != XmlDescriptor.from_xml:
# Skip the parse_xml from XmlParserMixin to get the shim parse_xml
# from XModuleDescriptor, which actually calls `from_xml`.
return super(XmlParserMixin, cls).parse_xml(node, runtime, keys, id_generator) # pylint: disable=bad-super-call
else:
return super(XmlDescriptor, cls).parse_xml(node, runtime, keys, id_generator)
def export_to_xml(self, resource_fs): # pylint: disable=unused-argument
"""
Returns an xml string representing this module, and all modules
underneath it. May also write required resources out to resource_fs.
Assumes that modules have single parentage (that no module appears twice
in the same course), and that it is thus safe to nest modules as xml
children as appropriate.
The returned XML should be able to be parsed back into an identical
XModuleDescriptor using the from_xml method with the same system, org,
and course
"""
# Shim from export_to_xml to the add_xml_to_node defined in XmlParserMixin.
# This only exists to satisfy subclasses that both:
# a) define export_to_xml themselves
# b) call super(..).export_to_xml(..)
node = Element(self.category)
super(XmlDescriptor, self).add_xml_to_node(node)
return etree.tostring(node) # pylint: disable=no-member
def add_xml_to_node(self, node):
"""
Export this :class:`XModuleDescriptor` as XML, by setting attributes on the provided
`node`.
"""
if self.export_to_xml != XmlDescriptor.export_to_xml:
# Skip the add_xml_to_node from XmlParserMixin to get the shim add_xml_to_node
# from XModuleDescriptor, which actually calls `export_to_xml`.
super(XmlParserMixin, self).add_xml_to_node(node) # pylint: disable=bad-super-call
else:
super(XmlDescriptor, self).add_xml_to_node(node)
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
} }
function initArgs(element) { function initArgs(element) {
var initargs = $('.xblock_json_init_args', element).text(); var initargs = $(element).children('.xblock-json-init-args').remove().text();
return initargs ? JSON.parse(initargs) : {}; return initargs ? JSON.parse(initargs) : {};
} }
......
<div class="${' '.join(classes) | n}" ${data_attributes}> <div class="${' '.join(classes) | n}" ${data_attributes}>
% if js_init_parameters: % if js_init_parameters:
<script type="json/xblock-args" class="xblock_json_init_args"> <script type="json/xblock-args" class="xblock-json-init-args">
${js_init_parameters} ${js_init_parameters}
</script> </script>
% endif % endif
${content} ${content}
</div> </div>
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
"graceperiod": "2 days 5 hours 59 minutes 59 seconds", "graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00", "start": "2015-07-17T12:00",
"display_name": "Toy Course", "display_name": "Toy Course",
"graded": "true", "graded": "true"
}, },
"tabs": [ "tabs": [
{"type": "courseware"}, {"type": "courseware"},
...@@ -19,5 +19,5 @@ ...@@ -19,5 +19,5 @@
}, },
"html/secret:toylab": { "html/secret:toylab": {
"display_name": "Toy lab" "display_name": "Toy lab"
}, }
} }
...@@ -3,6 +3,6 @@ ...@@ -3,6 +3,6 @@
"graceperiod": "2 days 5 hours 59 minutes 59 seconds", "graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00", "start": "2015-07-17T12:00",
"display_name": "Toy Course", "display_name": "Toy Course",
"graded": "true", "graded": "true"
} }
} }
...@@ -13,7 +13,7 @@ from student.models import CourseEnrollment ...@@ -13,7 +13,7 @@ from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from courseware.courses import get_course_by_id from courseware.courses import get_course_by_id
from xmodule import seq_module, vertical_module from xmodule import seq_module, vertical_block
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -181,7 +181,7 @@ def get_courseware_with_tabs(course_id): ...@@ -181,7 +181,7 @@ def get_courseware_with_tabs(course_id):
}, { }, {
'clickable_tab_count': 1, 'clickable_tab_count': 1,
'section_name': 'System Usage Sequence', 'section_name': 'System Usage Sequence',
'tab_classes': ['VerticalDescriptor'] 'tab_classes': ['VerticalBlock']
}, { }, {
'clickable_tab_count': 0, 'clickable_tab_count': 0,
'section_name': 'Lab0: Using the tools', 'section_name': 'Lab0: Using the tools',
...@@ -196,7 +196,7 @@ def get_courseware_with_tabs(course_id): ...@@ -196,7 +196,7 @@ def get_courseware_with_tabs(course_id):
'sections': [{ 'sections': [{
'clickable_tab_count': 4, 'clickable_tab_count': 4,
'section_name': 'Administrivia and Circuit Elements', 'section_name': 'Administrivia and Circuit Elements',
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor'] 'tab_classes': ['VerticalBlock', 'VerticalBlock', 'VerticalBlock', 'VerticalBlock']
}, { }, {
'clickable_tab_count': 0, 'clickable_tab_count': 0,
'section_name': 'Basic Circuit Analysis', 'section_name': 'Basic Circuit Analysis',
...@@ -215,7 +215,7 @@ def get_courseware_with_tabs(course_id): ...@@ -215,7 +215,7 @@ def get_courseware_with_tabs(course_id):
'sections': [{ 'sections': [{
'clickable_tab_count': 2, 'clickable_tab_count': 2,
'section_name': 'Midterm Exam', 'section_name': 'Midterm Exam',
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor'] 'tab_classes': ['VerticalBlock', 'VerticalBlock']
}] }]
}] }]
""" """
...@@ -228,7 +228,7 @@ def get_courseware_with_tabs(course_id): ...@@ -228,7 +228,7 @@ def get_courseware_with_tabs(course_id):
'section_name': s.display_name_with_default, 'section_name': s.display_name_with_default,
'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0, 'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0,
'tabs': [{ 'tabs': [{
'children_count': len(t.get_children()) if (type(t) == vertical_module.VerticalDescriptor) else 0, 'children_count': len(t.get_children()) if (type(t) == vertical_block.VerticalBlock) else 0,
'class': t.__class__.__name__} for t in s.get_children() 'class': t.__class__.__name__} for t in s.get_children()
] ]
} for s in c.get_children() if not s.hide_from_toc] } for s in c.get_children() if not s.hide_from_toc]
......
...@@ -126,9 +126,12 @@ class CommandsTestBase(ModuleStoreTestCase): ...@@ -126,9 +126,12 @@ class CommandsTestBase(ModuleStoreTestCase):
self.assertEqual(dump[child_id]['category'], 'videosequence') self.assertEqual(dump[child_id]['category'], 'videosequence')
self.assertEqual(len(dump[child_id]['children']), 2) self.assertEqual(len(dump[child_id]['children']), 2)
video_id = test_course_key.make_usage_key('video', 'Welcome').to_deprecated_string() video_id = unicode(test_course_key.make_usage_key('video', 'Welcome'))
self.assertEqual(dump[video_id]['category'], 'video') self.assertEqual(dump[video_id]['category'], 'video')
self.assertEqual(len(dump[video_id]['metadata']), 5) self.assertItemsEqual(
dump[video_id]['metadata'].keys(),
['download_video', 'youtube_id_0_75', 'youtube_id_1_0', 'youtube_id_1_25', 'youtube_id_1_5']
)
self.assertIn('youtube_id_1_0', dump[video_id]['metadata']) self.assertIn('youtube_id_1_0', dump[video_id]['metadata'])
# Check if there are the right number of elements # Check if there are the right number of elements
......
...@@ -16,6 +16,7 @@ from django.contrib.auth.models import AnonymousUser ...@@ -16,6 +16,7 @@ from django.contrib.auth.models import AnonymousUser
from mock import MagicMock, patch, Mock from mock import MagicMock, patch, Mock
from opaque_keys.edx.keys import UsageKey, CourseKey from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from pyquery import PyQuery
from courseware.module_render import hash_resource from courseware.module_render import hash_resource
from xblock.field_data import FieldData from xblock.field_data import FieldData
from xblock.runtime import Runtime from xblock.runtime import Runtime
...@@ -430,7 +431,8 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -430,7 +431,8 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
content = json.loads(response.content) content = json.loads(response.content)
for section in expected: for section in expected:
self.assertIn(section, content) self.assertIn(section, content)
self.assertIn('<div class="xblock xblock-student_view xmodule_display', content['html']) doc = PyQuery(content['html'])
self.assertEquals(len(doc('div.xblock-student_view-videosequence')), 1)
@ddt.ddt @ddt.ddt
...@@ -567,7 +569,7 @@ class TestHtmlModifiers(ModuleStoreTestCase): ...@@ -567,7 +569,7 @@ class TestHtmlModifiers(ModuleStoreTestCase):
) )
result_fragment = module.render(STUDENT_VIEW) result_fragment = module.render(STUDENT_VIEW)
self.assertIn('div class="xblock xblock-student_view xmodule_display xmodule_HtmlModule"', result_fragment.content) self.assertEquals(len(PyQuery(result_fragment.content)('div.xblock.xblock-student_view.xmodule_HtmlModule')), 1)
def test_xmodule_display_wrapper_disabled(self): def test_xmodule_display_wrapper_disabled(self):
module = render.get_module( module = render.get_module(
...@@ -798,8 +800,8 @@ class MongoViewInStudioTest(ViewInStudioTest): ...@@ -798,8 +800,8 @@ class MongoViewInStudioTest(ViewInStudioTest):
# Render the parent vertical, then check that there is only a single "View Unit in Studio" link. # Render the parent vertical, then check that there is only a single "View Unit in Studio" link.
result_fragment = self.module.render(STUDENT_VIEW) result_fragment = self.module.render(STUDENT_VIEW)
# The single "View Unit in Studio" link should appear before the first xmodule vertical definition. # The single "View Unit in Studio" link should appear before the first xmodule vertical definition.
parts = result_fragment.content.split('xmodule_VerticalModule') parts = result_fragment.content.split('data-block-type="vertical"')
self.assertEqual(3, len(parts), "Did not find two vertical modules") self.assertEqual(3, len(parts), "Did not find two vertical blocks")
self.assertIn('View Unit in Studio', parts[0]) self.assertIn('View Unit in Studio', parts[0])
self.assertNotIn('View Unit in Studio', parts[1]) self.assertNotIn('View Unit in Studio', parts[1])
self.assertNotIn('View Unit in Studio', parts[2]) self.assertNotIn('View Unit in Studio', parts[2])
......
...@@ -345,6 +345,13 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -345,6 +345,13 @@ def _index_bulk_op(request, course_key, chapter, section, position):
""" """
Render the index page for the specified course. Render the index page for the specified course.
""" """
# Verify that position a string is in fact an int
if position is not None:
try:
int(position)
except ValueError:
raise Http404("Position {} is not an integer!".format(position))
user = request.user user = request.user
course = get_course_with_access(user, 'load', course_key, depth=2) course = get_course_with_access(user, 'load', course_key, depth=2)
...@@ -493,13 +500,6 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -493,13 +500,6 @@ def _index_bulk_op(request, course_key, chapter, section, position):
section_descriptor, depth=None section_descriptor, depth=None
) )
# Verify that position a string is in fact an int
if position is not None:
try:
int(position)
except ValueError:
raise Http404("Position {} is not an integer!".format(position))
section_module = get_module_for_descriptor( section_module = get_module_for_descriptor(
request.user, request.user,
request, request,
......
...@@ -162,7 +162,7 @@ div.course-wrapper { ...@@ -162,7 +162,7 @@ div.course-wrapper {
} }
} }
section.xmodule_WrapperModule div.vert-mod > div { section.xblock-student_view-wrapper div.vert-mod > div {
border-bottom: none; border-bottom: none;
} }
......
...@@ -144,6 +144,7 @@ pep8==1.5.7 ...@@ -144,6 +144,7 @@ pep8==1.5.7
PyContracts==1.7.1 PyContracts==1.7.1
pylint==1.4.2 pylint==1.4.2
python-subunit==0.0.16 python-subunit==0.0.16
pyquery==1.2.9
radon==1.2 radon==1.2
rednose==0.3 rednose==0.3
selenium==2.42.1 selenium==2.42.1
......
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