Skip to content

Commit

Permalink
Convert 'library' (root block) to pure XBlock
Browse files Browse the repository at this point in the history
  • Loading branch information
bradenmacdonald committed Nov 26, 2014
1 parent 44888f4 commit b0662e7
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 31 deletions.
6 changes: 4 additions & 2 deletions common/lib/xmodule/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"html = xmodule.html_module:HtmlDescriptor",
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"library = xmodule.library_module:LibraryDescriptor",
"error = xmodule.error_module:ErrorDescriptor",
"peergrading = xmodule.peer_grading_module:PeerGradingDescriptor",
"poll_question = xmodule.poll_module:PollDescriptor",
Expand Down Expand Up @@ -45,6 +44,9 @@
"crowdsource_hinter = xmodule.crowdsource_hinter:CrowdsourceHinterDescriptor",
"lti = xmodule.lti_module:LTIDescriptor",
]
XBLOCKS = [
"library = xmodule.library_root_xblock:LibraryRoot",
]

setup(
name="XModule",
Expand All @@ -65,7 +67,7 @@
# See http://guide.python-distribute.org/creation.html#entry-points
# for a description of entry_points
entry_points={
'xblock.v1': XMODULES,
'xblock.v1': XMODULES + XBLOCKS,
'xmodule.v1': XMODULES,
'console_scripts': [
'xmodule_assets = xmodule.static_content:main',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
"""
'library' XBlock/XModule
The "library" XBlock/XModule is the root of every content library structure
tree. All content blocks in the library are its children. It is analagous to
the "course" XBlock/XModule used as the root of each normal course structure
tree.
'library' XBlock (LibraryRoot)
"""
import logging

from xmodule.vertical_module import VerticalDescriptor, VerticalModule

from .studio_editable import StudioEditableModule
from xblock.core import XBlock
from xblock.fields import Scope, String, List
from xblock.fragment import Fragment

log = logging.getLogger(__name__)

# Make '_' a no-op so we can scrape strings
_ = lambda text: text


class LibraryFields(object):
class LibraryRoot(XBlock):
"""
Fields of the "library" XBlock - see below.
The LibraryRoot is the root XBlock of a content library. All other blocks in
the library are its children. It contains metadata such as the library's
display_name.
"""
display_name = String(
help=_("Enter the name of the library as it should appear in Studio."),
Expand All @@ -34,41 +32,55 @@ class LibraryFields(object):
scope=Scope.settings
)
has_children = True


class LibraryDescriptor(LibraryFields, VerticalDescriptor):
"""
Descriptor for our library XBlock/XModule.
"""
module_class = VerticalModule

def __init__(self, *args, **kwargs):
"""
Expects the same arguments as XModuleDescriptor.__init__
"""
super(LibraryDescriptor, self).__init__(*args, **kwargs)
has_author_view = True

def __unicode__(self):
return u"Library: {}".format(self.display_name)

def __str__(self):
return "Library: {}".format(self.display_name)

def author_view(self, context):
"""
Renders the Studio preview view, which supports drag and drop.
"""
fragment = Fragment()
contents = []

for child_key in self.children: # pylint: disable=E1101
context['reorderable_items'].add(child_key)
child = self.runtime.get_block(child_key)
rendered_child = self.runtime.render_child(child, StudioEditableModule.get_preview_view_name(child), context)
fragment.add_frag_resources(rendered_child)

contents.append({
'id': unicode(child_key),
'content': rendered_child.content,
})

fragment.add_content(self.runtime.render_template("studio_render_children_view.html", {
'items': contents,
'xblock_context': context,
'can_add': True,
'can_reorder': True,
}))
return fragment

@property
def display_org_with_default(self):
"""
Org display names are not implemented. This just provides API compatibility with CourseDescriptor.
Always returns the raw 'org' field from the key.
"""
return self.location.course_key.org
return self.scope_ids.usage_id.course_key.org

@property
def display_number_with_default(self):
"""
Display numbers are not implemented. This just provides API compatibility with CourseDescriptor.
Always returns the raw 'library' field from the key.
"""
return self.location.course_key.library
return self.scope_ids.usage_id.course_key.library

@classmethod
def from_xml(cls, xml_data, system, id_generator):
Expand Down
2 changes: 1 addition & 1 deletion common/lib/xmodule/xmodule/modulestore/mixed.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ def create_library(self, org, library, user_id, fields, **kwargs):
fields (dict): Fields to set on the course at initialization - e.g. display_name
kwargs: Any optional arguments understood by a subset of modulestores to customize instantiation
Returns: a LibraryDescriptor
Returns: a LibraryRoot
"""
# first make sure an existing course/lib doesn't already exist in the mapping
lib_key = LibraryLocator(org=org, library=library)
Expand Down
33 changes: 32 additions & 1 deletion common/lib/xmodule/xmodule/modulestore/tests/test_libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@
"""
from bson.objectid import ObjectId
import ddt
from mock import patch
from opaque_keys.edx.locator import LibraryLocator
from xblock.fragment import Fragment
from xblock.runtime import Runtime as VanillaRuntime
from xmodule.modulestore.exceptions import DuplicateCourseError
from xmodule.modulestore.tests.factories import LibraryFactory, ItemFactory, check_mongo_calls
from xmodule.modulestore.tests.utils import MixedSplitTestCase
from xmodule.x_module import AUTHOR_VIEW


@ddt.ddt
class TestLibraries(MixedSplitTestCase):
"""
Test for libraries.
Mostly tests code found throughout split mongo, but also tests library_module.py
Mostly tests code found throughout split mongo, but also tests library_root_xblock.py
"""
def test_create_library(self):
"""
Expand Down Expand Up @@ -166,3 +170,30 @@ def test_get_lib_version(self):
lib = self.store.get_library(lib_key, remove_version=False, remove_branch=False)
version = lib.location.library_key.version_guid
self.assertIsInstance(version, ObjectId)

@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.render', VanillaRuntime.render)
def test_library_author_view(self):
"""
Test that LibraryRoot.author_view can run and includes content from its
children.
We have to patch the runtime (module system) in order to be able to
render blocks in our test environment.
"""
library = LibraryFactory.create(modulestore=self.store)
# Add one HTML block to the library:
ItemFactory.create(
category="html",
parent_location=library.location,
user_id=self.user_id,
publish_item=False,
modulestore=self.store,
)
library = self.store.get_library(library.location.library_key)

context = {'reorderable_items': set(), }
# Patch the HTML block to always render "Hello world"
message = u"Hello world"
hello_render = lambda _, context: Fragment(message)
with patch('xmodule.html_module.HtmlDescriptor.author_view', hello_render, create=True):
result = library.render(AUTHOR_VIEW, context)
self.assertIn(message, result.content)
27 changes: 25 additions & 2 deletions common/lib/xmodule/xmodule/modulestore/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
Helper classes and methods for running modulestore tests without Django.
"""
from importlib import import_module
from opaque_keys.edx.keys import UsageKey
from unittest import TestCase
from xblock.fields import XBlockMixin
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from xmodule.modulestore.edit_info import EditInfoMixin
Expand Down Expand Up @@ -40,17 +42,38 @@ def create_modulestore_instance(engine, contentstore, doc_store_config, options,
)


class LocationMixin(XBlockMixin):
"""
Adds a `location` property to an :class:`XBlock` so it is more compatible
with old-style :class:`XModule` API. This is a simplified version of
:class:`XModuleMixin`.
"""
@property
def location(self):
""" Get the UsageKey of this block. """
return self.scope_ids.usage_id

@location.setter
def location(self, value):
""" Set the UsageKey of this block. """
assert isinstance(value, UsageKey)
self.scope_ids = self.scope_ids._replace( # pylint: disable=attribute-defined-outside-init,protected-access
def_id=value,
usage_id=value,
)


class MixedSplitTestCase(TestCase):
"""
Stripped-down version of ModuleStoreTestCase that can be used without Django
(i.e. for testing in common/lib/ ). Sets up MixedModuleStore and Split.
"""
RENDER_TEMPLATE = lambda t_n, d, ctx = None, nsp = 'main': ''
RENDER_TEMPLATE = lambda t_n, d, ctx = None, nsp = 'main': u'{}: {}, {}'.format(t_n, repr(d), repr(ctx))
modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': DATA_DIR,
'render_template': RENDER_TEMPLATE,
'xblock_mixins': (EditInfoMixin, InheritanceMixin),
'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin),
}
DOC_STORE_CONFIG = {
'host': MONGO_HOST,
Expand Down

0 comments on commit b0662e7

Please sign in to comment.