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):
'Raw HTML': '<p>This template is similar to the Text template. The only difference is',
}
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:
actual_text = world.css_text(selector, index=idx)
assert_in(step_hash['Component'].upper(), actual_text)
......
......@@ -27,7 +27,7 @@ def add_page(step):
def see_a_static_page_named_foo(step, name):
pages_css = 'div.xmodule_StaticTabModule'
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$')
......
......@@ -5,6 +5,7 @@ import ddt
from mock import patch, Mock, PropertyMock
from pytz import UTC
from pyquery import PyQuery
from webob import Response
from django.http import Http404
......@@ -1026,7 +1027,8 @@ class TestEditItemSplitMongo(TestEditItemSetup):
for __ in xrange(3):
resp = self.client.get(view_url, HTTP_ACCEPT='application/json')
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):
......
......@@ -9,16 +9,19 @@ import static_replace
import uuid
import markupsafe
from lxml import html, etree
from contracts import contract
from django.conf import settings
from django.utils.timezone import UTC
from django.utils.html import escape
from django.contrib.auth.models import User
from edxmako.shortcuts import render_to_string
from xblock.core import XBlock
from xblock.exceptions import InvalidScopeError
from xblock.fragment import Fragment
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.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
......@@ -77,7 +80,11 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
css_classes = [
'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)):
......@@ -90,7 +97,7 @@ def wrap_xblock(runtime_class, block, view, frag, context, usage_id_serializer,
css_classes.append('xmodule_' + markupsafe.escape(class_name))
data['type'] = block.js_module_name
shim_xmodule_js(frag)
shim_xmodule_js(block, frag)
if frag.js_init_fn:
data['init'] = frag.js_init_fn
......@@ -189,6 +196,7 @@ def grade_histogram(module_id):
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
"""
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):
Does nothing if module is a SequenceModule.
"""
# 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
is_mongo_course = modulestore().get_modulestore_type(block.location.course_key) != ModuleStoreEnum.Type.xml
is_studio_course = block.course_edit_method == "Studio"
......
......@@ -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 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.
from defusedxml.lxml import parse, fromstring, XML
......
......@@ -22,7 +22,6 @@ XMODULES = [
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor",
"videoalpha = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
......@@ -32,7 +31,6 @@ XMODULES = [
"static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor",
"about = xmodule.html_module:AboutDescriptor",
"wrapper = xmodule.wrapper_module:WrapperDescriptor",
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"annotatable = xmodule.annotatable_module:AnnotatableDescriptor",
"textannotation = xmodule.textannotation_module:TextAnnotationDescriptor",
......@@ -47,6 +45,8 @@ XMODULES = [
]
XBLOCKS = [
"library = xmodule.library_root_xblock:LibraryRoot",
"vertical = xmodule.vertical_block:VerticalBlock",
"wrapper = xmodule.wrapper_module:WrapperBlock",
]
setup(
......
......@@ -273,7 +273,7 @@ function PollMain(el) {
if (
(tempEl.tagName.toLowerCase() === 'div') &&
($(tempEl).hasClass('xmodule_WrapperModule') === true)
($(tempEl).data('block-type') === 'wrapper')
) {
_this.wrapperSectionEl = tempEl;
......
......@@ -58,14 +58,20 @@
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)
* from the supplied element, which should have a data-type attribute
* specifying the class to load
*/
var moduleType = $(element).data('type'),
module;
var moduleType, module;
if (initArgs) {
moduleType = initArgs['xmodule-type'];
}
if (!moduleType) {
moduleType = $(element).data('type');
}
if (moduleType === 'None') {
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):
......@@ -8,20 +13,19 @@ class MakoDescriptorSystem(DescriptorSystem):
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.
Expects the descriptor to have the `mako_template` attribute set
with the name of the template to render, and it will pass
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):
super(MakoModuleDescriptor, self).__init__(*args, **kwargs)
super(MakoTemplateBlockBase, self).__init__(*args, **kwargs)
if getattr(self.runtime, 'render_template', None) is None:
raise TypeError(
'{runtime} must have a render_template function'
......@@ -39,6 +43,21 @@ class MakoModuleDescriptor(XModuleDescriptor):
'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):
return self.system.render_template(
self.mako_template, self.get_context())
return self.studio_view(None).content
......@@ -47,7 +47,7 @@ class DraftModuleStore(MongoModuleStore):
This module also includes functionality to promote DRAFT modules (and their children)
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.
......@@ -70,6 +70,9 @@ class DraftModuleStore(MongoModuleStore):
Note: If the item is in DIRECT_ONLY_CATEGORIES, then returns only the PUBLISHED
version regardless of the revision.
using_descriptor_system (CachingDescriptorSystem): The existing CachingDescriptorSystem
to add data to, and to load the XBlocks from.
Raises:
xmodule.modulestore.exceptions.InsufficientSpecificationError
if any segment of the usage_key is None except revision
......@@ -78,10 +81,14 @@ class DraftModuleStore(MongoModuleStore):
is found at that usage_key
"""
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():
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
if revision == ModuleStoreEnum.RevisionOption.published_only:
......
......@@ -20,6 +20,7 @@ from nose.plugins.attrib import attr
import pymongo
from pytz import UTC
from xmodule.x_module import XModuleMixin
from xmodule.modulestore.edit_info import EditInfoMixin
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.modulestore.tests.test_cross_modulestore_import_export import MongoContentstoreBuilder
......@@ -73,7 +74,7 @@ class TestMixedModuleStore(CourseComparisonTest):
'default_class': DEFAULT_CLASS,
'fs_root': DATA_DIR,
'render_template': RENDER_TEMPLATE,
'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin),
'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin, XModuleMixin),
}
DOC_STORE_CONFIG = {
'host': HOST,
......
......@@ -41,8 +41,10 @@ from git.test.lib.asserts import assert_not_none
from xmodule.x_module import XModuleMixin
from xmodule.modulestore.mongo.base import as_draft
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.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import InheritanceMixin
log = logging.getLogger(__name__)
......@@ -124,7 +126,7 @@ class TestMongoModuleStoreBase(unittest.TestCase):
doc_store_config, FS_ROOT, RENDER_TEMPLATE,
default_class=DEFAULT_CLASS,
branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred,
xblock_mixins=(EditInfoMixin,)
xblock_mixins=(EditInfoMixin, InheritanceMixin, LocationMixin, XModuleMixin)
)
import_course_from_xml(
......
......@@ -8,6 +8,7 @@ import mock
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from xmodule.modulestore import ModuleStoreEnum
from xmodule.x_module import XModuleMixin
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.modulestore.mongo import DraftMongoModuleStore
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
......@@ -41,7 +42,7 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase):
'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': '',
'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)
......
......@@ -9,6 +9,7 @@ from mock import patch
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.x_module import XModuleMixin
from xmodule.tests import DATA_DIR
from opaque_keys.edx.locations import SlashSeparatedCourseKey
......@@ -46,7 +47,11 @@ class TestXMLModuleStore(unittest.TestCase):
# Load the course, but don't make error modules. This will succeed,
# 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.
errors = modulestore.get_course_errors(SlashSeparatedCourseKey("edX", "toy", "2012_Fall"))
......@@ -119,7 +124,11 @@ class TestXMLModuleStore(unittest.TestCase):
"""
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
mock_logging.warning.assert_called_with(
......
......@@ -5,6 +5,7 @@ from importlib import import_module
from opaque_keys.edx.keys import UsageKey
from unittest import TestCase
from xblock.fields import XBlockMixin
from xmodule.x_module import XModuleMixin
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from xmodule.modulestore.edit_info import EditInfoMixin
......@@ -84,7 +85,7 @@ class MixedSplitTestCase(TestCase):
'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': DATA_DIR,
'render_template': RENDER_TEMPLATE,
'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin),
'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin, XModuleMixin),
}
DOC_STORE_CONFIG = {
'host': MONGO_HOST,
......
......@@ -32,7 +32,7 @@ from lxml import etree
from xmodule.modulestore.xml import XMLModuleStore, LibraryXMLModuleStore, ImportSystem
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 xblock.fields import Scope, Reference, ReferenceList, ReferenceValueDict
from xmodule.contentstore.content import StaticContent
......@@ -47,6 +47,7 @@ from xmodule.modulestore.exceptions import DuplicateCourseError
from xmodule.modulestore.mongo.base import MongoRevisionKey
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.store_utilities import draft_node_constructor, get_draft_subtree_roots
from xmodule.modulestore.tests.utils import LocationMixin
log = logging.getLogger(__name__)
......@@ -1031,7 +1032,8 @@ def validate_course_policy(module_store, course_id):
def perform_xlint(
data_dir, source_dirs,
default_class='xmodule.raw_module.RawDescriptor',
load_error_modules=True):
load_error_modules=True,
xblock_mixins=(LocationMixin, XModuleMixin)):
err_cnt = 0
warn_cnt = 0
......@@ -1039,7 +1041,8 @@ def perform_xlint(
data_dir,
default_class=default_class,
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
......
......@@ -4,13 +4,13 @@ Mixin to support editing in Studio.
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
self.descriptor and self.system.
This class is only intended to be used with an XBlock!
"""
has_author_view = True
def render_children(self, context, fragment, can_reorder=False, can_add=False):
"""
......@@ -19,15 +19,14 @@ class StudioEditableModule(object):
"""
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:
context['reorderable_items'].add(child.location)
child_module = self.system.get_module(child) # pylint: disable=no-member
rendered_child = child_module.render(StudioEditableModule.get_preview_view_name(child_module), context)
rendered_child = child.render(StudioEditableModule.get_preview_view_name(child), context)
fragment.add_frag_resources(rendered_child)
contents.append({
'id': child.location.to_deprecated_string(),
'id': unicode(child.location),
'content': rendered_child.content
})
......@@ -46,6 +45,9 @@ class StudioEditableModule(object):
return AUTHOR_VIEW if hasattr(block, AUTHOR_VIEW) else STUDENT_VIEW
StudioEditableModule = StudioEditableBlock
class StudioEditableDescriptor(object):
"""
Helper mixin for supporting Studio editing of xmodules.
......
......@@ -86,6 +86,21 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method
def get_asides(self, block):
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')):
"""
......@@ -128,7 +143,7 @@ def get_test_system(course_id=SlashSeparatedCourseKey('org', 'course', 'run')):
render_template=mock_render_template,
replace_urls=str,
user=user,
get_real_user=lambda(__): user,
get_real_user=lambda __: user,
filestore=Mock(name='get_test_system.filestore'),
debug=True,
hostname="edx.org",
......@@ -363,11 +378,17 @@ class CourseComparisonTest(BulkAssertionTest):
)
def assertBlocksEqualByFields(self, expected_block, actual_block):
"""
Compare block fields to check for equivalence.
"""
self.assertEqual(expected_block.fields, actual_block.fields)
for field in expected_block.fields.values():
self.assertFieldEqual(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)):
self.assertReferenceRelativelyEqual(field, expected_block, actual_block)
else:
......@@ -421,6 +442,9 @@ class CourseComparisonTest(BulkAssertionTest):
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):
"""
Actual algorithm to compare courses.
"""
with self.bulk_assertions():
self.assertEqual(len(expected_items), len(actual_items))
......
......@@ -7,7 +7,7 @@ import unittest
import copy
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 xblock.field_data import DictFieldData
from xblock.fragment import Fragment
......@@ -203,8 +203,8 @@ class VerticalWithModulesFactory(object):
"""Make a vertical."""
field_data = {'data': VerticalWithModulesFactory.sample_problem_xml}
system = get_test_system()
descriptor = VerticalDescriptor.from_xml(VerticalWithModulesFactory.sample_problem_xml, system)
module = VerticalModule(system, descriptor, field_data)
descriptor = VerticalBlock.parse_xml(VerticalWithModulesFactory.sample_problem_xml, system)
module = VerticalBlock(system, descriptor, field_data)
return module
......
......@@ -2,11 +2,14 @@
Tests for StudioEditableModule.
"""
from xmodule.tests.test_vertical import BaseVerticalModuleTest
from xmodule.tests.test_vertical import BaseVerticalBlockTest
from xmodule.x_module import AUTHOR_VIEW
class StudioEditableModuleTestCase(BaseVerticalModuleTest):
class StudioEditableModuleTestCase(BaseVerticalBlockTest):
"""
Class containing StudioEditableModule tests.
"""
def test_render_reorderable_children(self):
"""
Test the behavior of render_reorderable_children.
......
......@@ -9,12 +9,15 @@ from xmodule.tests.xml import factories as xml
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_2 = 'Test HTML 2'
def setUp(self):
super(BaseVerticalModuleTest, self).setUp()
super(BaseVerticalBlockTest, self).setUp()
# construct module
course = xml.CourseFactory.build()
sequence = xml.SequenceFactory.build(parent=course)
......@@ -35,7 +38,10 @@ class BaseVerticalModuleTest(XModuleXmlImportTest):
self.vertical.xmodule_runtime = self.module_system
class VerticalModuleTestCase(BaseVerticalModuleTest):
class VerticalBlockTestCase(BaseVerticalBlockTest):
"""
Tests for the VerticalBlock.
"""
def test_render_student_view(self):
"""
Test the rendering of the student view.
......
......@@ -23,6 +23,7 @@ from unittest.case import SkipTest, TestCase
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xblock.core import XBlock
from opaque_keys.edx.locations import Location
......@@ -42,8 +43,8 @@ from xmodule.crowdsource_hinter import CrowdsourceHinterDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.conditional_module import ConditionalDescriptor
from xmodule.randomize_module import RandomizeDescriptor
from xmodule.vertical_module import VerticalDescriptor
from xmodule.wrapper_module import WrapperDescriptor
from xmodule.vertical_block import VerticalBlock
from xmodule.wrapper_module import WrapperBlock
from xmodule.tests import get_test_descriptor_system, get_test_system
......@@ -74,8 +75,8 @@ CONTAINER_XMODULES = {
CrowdsourceHinterDescriptor: [{}],
RandomizeDescriptor: [{}],
SequenceDescriptor: [{}],
VerticalDescriptor: [{}],
WrapperDescriptor: [{}],
VerticalBlock: [{}],
WrapperBlock: [{}],
}
# These modules are editable in studio yet
......@@ -141,7 +142,10 @@ class ContainerModuleRuntimeFactory(ModuleSystemFactory):
if depth == 0:
self.get_module.side_effect = lambda x: LeafModuleFactory(descriptor_cls=HtmlDescriptor)
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
def position(self, create, position=2, **kwargs): # pylint: disable=unused-argument, method-hidden
......@@ -166,7 +170,10 @@ class ContainerDescriptorRuntimeFactory(DescriptorSystemFactory):
if depth == 0:
self.load_item.side_effect = lambda x: LeafModuleFactory(descriptor_cls=HtmlDescriptor)
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
def position(self, create, position=2, **kwargs): # pylint: disable=unused-argument, method-hidden
......@@ -323,7 +330,12 @@ class TestStudentView(XBlockWrapperTestMixin, TestCase):
This tests that student_view and XModule.get_html produce the same results.
"""
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")
def check_property(self, descriptor):
......@@ -344,7 +356,10 @@ class TestStudioView(XBlockWrapperTestMixin, TestCase):
if descriptor_cls in NOT_STUDIO_EDITABLE:
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")
def check_property(self, descriptor):
......
......@@ -9,6 +9,7 @@ from factory import Factory, lazy_attribute, post_generation, Sequence
from lxml import etree
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.x_module import XModuleMixin
from xmodule.modulestore import only_xmodules
......@@ -66,7 +67,7 @@ class XmlImportFactory(Factory):
FACTORY_FOR = XmlImportData
filesystem = MemoryFS()
xblock_mixins = (InheritanceMixin,)
xblock_mixins = (InheritanceMixin, XModuleMixin)
xblock_select = only_xmodules
url_name = Sequence(str)
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 xmodule.x_module import XModule, STUDENT_VIEW
from xmodule.seq_module import SequenceDescriptor
from xmodule.mako_module import MakoTemplateBlockBase
from xmodule.progress import Progress
from xmodule.studio_editable import StudioEditableModule, StudioEditableDescriptor
from pkg_resources import resource_string
from copy import copy
from xmodule.seq_module import SequenceFields
from xmodule.studio_editable import StudioEditableBlock
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
# 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):
''' Layout module for laying out submodules vertically.'''
has_children = True
def student_view(self, context):
"""
Renders the student view of the block in the LMS.
"""
fragment = Fragment()
contents = []
child_context = {} if not context else copy(context)
child_context['child_of_vertical'] = True
# pylint: disable=no-member
for child in self.get_display_items():
rendered_child = child.render(STUDENT_VIEW, child_context)
fragment.add_frag_resources(rendered_child)
......@@ -47,7 +61,7 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
"""
fragment = Fragment()
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
# a more concise version that appears alongside the "View =>" link-- unless it is
......@@ -57,37 +71,60 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
return fragment
def get_progress(self):
"""
Returns the progress on this block and all children.
"""
# 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]
progress = reduce(Progress.add_counts, progresses, None)
return progress
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'
for c in class_priority:
if c in child_classes:
new_class = c
for higher_class in CLASS_PRIORITY:
if higher_class in child_classes:
new_class = higher_class
return new_class
class VerticalDescriptor(VerticalFields, SequenceDescriptor, StudioEditableDescriptor):
"""
Descriptor class for editing verticals.
"""
module_class = VerticalModule
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
js_module_name = "VerticalDescriptor"
# TODO (victor): Does this need its own definition_to_xml method? Otherwise it looks
# like verticals will get exported as sequentials...
@classmethod
def definition_from_xml(cls, xml_object, system):
children = []
for child in xml_object:
try:
child_block = system.process_xml(etree.tostring(child, encoding='unicode')) # pylint: disable=no-member
children.append(child_block.scope_ids.usage_id)
except Exception as exc: # pylint: disable=broad-except
log.exception("Unable to load child when parsing Vertical. Continuing...")
if system.error_tracker is not None:
system.error_tracker(u"ERROR: {0}".format(exc))
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
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([
VerticalDescriptor.due,
self.fields['due'],
])
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,
# 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
# HACK: This shouldn't be hard-coded to two types
......@@ -9,14 +9,8 @@ from pkg_resources import resource_string
class_priority = ['video', 'problem']
class WrapperModule(VerticalModule):
''' Layout module for laying out submodules vertically w/o css delimiters'''
has_children = True
css = {'scss': [resource_string(__name__, 'css/wrapper/display.scss')]}
class WrapperDescriptor(VerticalDescriptor):
module_class = WrapperModule
has_children = True
class WrapperBlock(VerticalBlock):
'''
Layout block for laying out sub-blocks vertically *w/o* css delimiters.
'''
pass
......@@ -239,15 +239,30 @@ class HTMLSnippet(object):
.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`
"""
if not fragment.js_init_fn:
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.
......@@ -278,15 +293,6 @@ class XModuleMixin(XBlockMixin):
# in the module
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):
self.xmodule_runtime = None
self._child_instances = None
......@@ -571,6 +577,95 @@ class XModuleMixin(XBlockMixin):
self.xmodule_runtime = xmodule_runtime
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):
"""
......@@ -965,92 +1060,6 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
")".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 ~~~~~~~~~~~~~~~~
@property
def _xmodule(self):
......
......@@ -32,7 +32,7 @@
}
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) : {};
}
......
<div class="${' '.join(classes) | n}" ${data_attributes}>
% 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}
</script>
% endif
${content}
${content}
</div>
......@@ -3,7 +3,7 @@
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"display_name": "Toy Course",
"graded": "true",
"graded": "true"
},
"tabs": [
{"type": "courseware"},
......@@ -19,5 +19,5 @@
},
"html/secret:toylab": {
"display_name": "Toy lab"
},
}
}
......@@ -3,6 +3,6 @@
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"display_name": "Toy Course",
"graded": "true",
"graded": "true"
}
}
......@@ -13,7 +13,7 @@ from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
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
logger = getLogger(__name__)
......@@ -181,7 +181,7 @@ def get_courseware_with_tabs(course_id):
}, {
'clickable_tab_count': 1,
'section_name': 'System Usage Sequence',
'tab_classes': ['VerticalDescriptor']
'tab_classes': ['VerticalBlock']
}, {
'clickable_tab_count': 0,
'section_name': 'Lab0: Using the tools',
......@@ -196,7 +196,7 @@ def get_courseware_with_tabs(course_id):
'sections': [{
'clickable_tab_count': 4,
'section_name': 'Administrivia and Circuit Elements',
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor', 'VerticalDescriptor']
'tab_classes': ['VerticalBlock', 'VerticalBlock', 'VerticalBlock', 'VerticalBlock']
}, {
'clickable_tab_count': 0,
'section_name': 'Basic Circuit Analysis',
......@@ -215,7 +215,7 @@ def get_courseware_with_tabs(course_id):
'sections': [{
'clickable_tab_count': 2,
'section_name': 'Midterm Exam',
'tab_classes': ['VerticalDescriptor', 'VerticalDescriptor']
'tab_classes': ['VerticalBlock', 'VerticalBlock']
}]
}]
"""
......@@ -228,7 +228,7 @@ def get_courseware_with_tabs(course_id):
'section_name': s.display_name_with_default,
'clickable_tab_count': len(s.get_children()) if (type(s) == seq_module.SequenceDescriptor) else 0,
'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()
]
} for s in c.get_children() if not s.hide_from_toc]
......
......@@ -126,9 +126,12 @@ class CommandsTestBase(ModuleStoreTestCase):
self.assertEqual(dump[child_id]['category'], 'videosequence')
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(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'])
# Check if there are the right number of elements
......
......@@ -16,6 +16,7 @@ from django.contrib.auth.models import AnonymousUser
from mock import MagicMock, patch, Mock
from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from pyquery import PyQuery
from courseware.module_render import hash_resource
from xblock.field_data import FieldData
from xblock.runtime import Runtime
......@@ -430,7 +431,8 @@ class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase):
content = json.loads(response.content)
for section in expected:
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
......@@ -567,7 +569,7 @@ class TestHtmlModifiers(ModuleStoreTestCase):
)
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):
module = render.get_module(
......@@ -798,8 +800,8 @@ class MongoViewInStudioTest(ViewInStudioTest):
# Render the parent vertical, then check that there is only a single "View Unit in Studio" link.
result_fragment = self.module.render(STUDENT_VIEW)
# The single "View Unit in Studio" link should appear before the first xmodule vertical definition.
parts = result_fragment.content.split('xmodule_VerticalModule')
self.assertEqual(3, len(parts), "Did not find two vertical modules")
parts = result_fragment.content.split('data-block-type="vertical"')
self.assertEqual(3, len(parts), "Did not find two vertical blocks")
self.assertIn('View Unit in Studio', parts[0])
self.assertNotIn('View Unit in Studio', parts[1])
self.assertNotIn('View Unit in Studio', parts[2])
......
......@@ -345,6 +345,13 @@ def _index_bulk_op(request, course_key, chapter, section, position):
"""
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
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):
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(
request.user,
request,
......
......@@ -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;
}
......
......@@ -144,6 +144,7 @@ pep8==1.5.7
PyContracts==1.7.1
pylint==1.4.2
python-subunit==0.0.16
pyquery==1.2.9
radon==1.2
rednose==0.3
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