diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 49ca79bb0..57983c284 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,8 @@ These are notable changes in XBlock. 0.4 - In Progress ----------------- +* Separate Fragment class out into new web-fragments package + * Make Scope enums (UserScope.* and BlockScope.*) into Sentinels, rather than just ints, so that they can have more meaningful string representations. diff --git a/requirements.txt b/requirements.txt index 2dc9c9abf..427e883a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,6 +26,9 @@ ddt==0.8.0 # For generating new XBlocks cookiecutter +# For web fragments +web-fragments + # Our own XBlocks -e . diff --git a/setup.py b/setup.py index 1101c3361..a40211878 100755 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ 'pyyaml', 'six', 'webob', + 'web-fragments', ], extras_require={ 'django': ['django-pyfs'] diff --git a/xblock/VERSION.txt b/xblock/VERSION.txt index 1f7716999..489c893e2 100644 --- a/xblock/VERSION.txt +++ b/xblock/VERSION.txt @@ -1 +1 @@ -0.4.13 +0.4.14 diff --git a/xblock/fragment.py b/xblock/fragment.py index d6e99e836..f2c7e1432 100644 --- a/xblock/fragment.py +++ b/xblock/fragment.py @@ -1,269 +1,27 @@ -"""Fragments for XBlocks. - -This code is in the Runtime layer. - +""" +Makes the Fragment class available through the old namespace location. """ -from collections import namedtuple - - -FragmentResource = namedtuple("FragmentResource", "kind, data, mimetype, placement") # pylint: disable=C0103 - - -class Fragment(object): - """A fragment of a web page, for XBlock views to return. +import warnings - A fragment consists of HTML for the body of the page, and a series of - resources needed by the body. Resources are specified with a MIME type - (such as "application/javascript" or "text/css") that determines how they - are inserted into the page. The resource is provided either as literal - text, or as a URL. Text will be included on the page, wrapped - appropriately for the MIME type. URLs will be used as-is on the page. +import web_fragments.fragment - Resources are only inserted into the page once, even if many Fragments - in the page ask for them. Determining duplicates is done by simple text - matching. +class Fragment(web_fragments.fragment.Fragment): """ - def __init__(self, content=None): - #: The html content for this Fragment - self.content = u"" - - self._resources = [] - self.js_init_fn = None - self.js_init_version = None - self.json_init_args = None - - if content is not None: - self.add_content(content) - - @property - def resources(self): - r""" - Returns list of unique `FragmentResource`\s by order of first appearance. - """ - seen = set() - # seen.add always returns None, so 'not seen.add(x)' is always True, - # but will only be called if the value is not already in seen (because - # 'and' short-circuits) - return [x for x in self._resources if x not in seen and not seen.add(x)] - - def to_pods(self): - """ - Returns the data in a dictionary. - - 'pods' = Plain Old Data Structure. - """ - return { - 'content': self.content, - 'resources': [r._asdict() for r in self.resources], # pylint: disable=W0212 - 'js_init_fn': self.js_init_fn, - 'js_init_version': self.js_init_version, - 'json_init_args': self.json_init_args - } - - @classmethod - def from_pods(cls, pods): - """ - Returns a new Fragment from a `pods`. - - `pods` is a Plain Old Data Structure, a Python dictionary with - keys `content`, `resources`, `js_init_fn`, and `js_init_version`. - - """ - frag = cls() - frag.content = pods['content'] - frag.resources = [FragmentResource(**d) for d in pods['resources']] - frag.js_init_fn = pods['js_init_fn'] - frag.js_init_version = pods['js_init_version'] - return frag - - def add_content(self, content): - """Add content to this fragment. - - `content` is a Unicode string, HTML to append to the body of the - fragment. It must not contain a ```` tag, or otherwise assume - that it is the only content on the page. - - """ - assert isinstance(content, unicode) - self.content += content - - def _default_placement(self, mimetype): - """Decide where a resource will go, if the user didn't say.""" - if mimetype == 'application/javascript': - return 'foot' - return 'head' - - def add_resource(self, text, mimetype, placement=None): - """Add a resource needed by this Fragment. - - Other helpers, such as :func:`add_css` or :func:`add_javascript` are - more convenient for those common types of resource. - - `text`: the actual text of this resource, as a unicode string. - - `mimetype`: the MIME type of the resource. - - `placement`: where on the page the resource should be placed: - - None: let the Fragment choose based on the MIME type. - - "head": put this resource in the ```` of the page. - - "foot": put this resource at the end of the ```` of the - page. - - """ - if not placement: - placement = self._default_placement(mimetype) - res = FragmentResource('text', text, mimetype, placement) - self._resources.append(res) - - def add_resource_url(self, url, mimetype, placement=None): - """Add a resource by URL needed by this Fragment. - - Other helpers, such as :func:`add_css_url` or - :func:`add_javascript_url` are more convenent for those common types of - resource. - - `url`: the URL to the resource. - - Other parameters are as defined for :func:`add_resource`. - - """ - if not placement: - placement = self._default_placement(mimetype) - self._resources.append(FragmentResource('url', url, mimetype, placement)) - - def add_css(self, text): - """Add literal CSS to the Fragment.""" - self.add_resource(text, 'text/css') + A wrapper around web_fragments.fragment.Fragment that provides + backwards compatibility for the old location. - def add_css_url(self, url): - """Add a CSS URL to the Fragment.""" - self.add_resource_url(url, 'text/css') - - def add_javascript(self, text): - """Add literal Javascript to the Fragment.""" - self.add_resource(text, 'application/javascript') - - def add_javascript_url(self, url): - """Add a Javascript URL to the Fragment.""" - self.add_resource_url(url, 'application/javascript') - - def add_frag_resources(self, frag): - """Add all the resources from `frag` to my resources. - - This is used by an XBlock to collect resources from Fragments produced - by its children. - - `frag` is a Fragment. - - The content from the Fragment is ignored. The caller must collect - together the content into this Fragment's content. - - """ - self._resources.extend(frag.resources) - - def add_frags_resources(self, frags): - """Add all the resources from `frags` to my resources. - - This is used by an XBlock to collect resources from Fragments produced - by its children. - - `frags` is a sequence of Fragments. - - The content from the Fragments is ignored. The caller must collect - together the content into this Fragment's content. - - """ - for resource in frags: - self.add_frag_resources(resource) - - def initialize_js(self, js_func, json_args=None): - """Register a Javascript function to initialize the Javascript resources. - - `js_func` is the name of a Javascript function defined by one of the - Javascript resources. As part of setting up the browser's runtime - environment, the function will be invoked, passing a runtime object - and a DOM element. - - """ - # This is version 1 of the interface. - self.js_init_fn = js_func - self.js_init_version = 1 - if json_args: - self.json_init_args = json_args - - # Implementation methods: don't override - # TODO: [rocha] should this go in the runtime? - - def body_html(self): - """Get the body HTML for this Fragment. - - Returns a Unicode string, the HTML content for the ```` section - of the page. - - """ - return self.content - - def head_html(self): - """Get the head HTML for this Fragment. - - Returns a Unicode string, the HTML content for the ```` section - of the page. - - """ - return self.resources_to_html("head") - - def foot_html(self): - """Get the foot HTML for this Fragment. - - Returns a Unicode string, the HTML content for the end of the - ```` section of the page. - - """ - return self.resources_to_html("foot") - - def resources_to_html(self, placement): - """Get some resource HTML for this Fragment. - - `placement` is "head" or "foot". - - Returns a unicode string, the HTML for the head or foot of the page. - - """ - # TODO: [rocha] aggregate and wrap css and javascript. - # - non url js could be wrapped in an anonymous function - # - non url css could be rewritten to match the wrapper tag - - return '\n'.join( - self.resource_to_html(resource) - for resource in self.resources - if resource.placement == placement + Deprecated. + """ + def __init__(self, *args, **kwargs): + warnings.warn( + 'xblock.fragment is deprecated. Please use web_fragments.fragment instead', + DeprecationWarning, + stacklevel=2 ) + super(Fragment, self).__init__(*args, **kwargs) - @staticmethod - def resource_to_html(resource): - """ - Returns `resource` wrapped in the appropriate html tag for it's mimetype. - """ - if resource.mimetype == "text/css": - if resource.kind == "text": - return u"" % resource.data - elif resource.kind == "url": - return u"" % resource.data - - elif resource.mimetype == "application/javascript": - if resource.kind == "text": - return u"" % resource.data - elif resource.kind == "url": - return u"" % resource.data - - elif resource.mimetype == "text/html": - assert resource.kind == "text" - return resource.data - - else: - raise Exception("Never heard of mimetype %r" % resource.mimetype) + # Provide older names for renamed methods + add_frag_resources = web_fragments.fragment.Fragment.add_fragment_resources + add_frags_resources = web_fragments.fragment.Fragment.add_resources diff --git a/xblock/runtime.py b/xblock/runtime.py index 4a91239b9..fa4ac6113 100644 --- a/xblock/runtime.py +++ b/xblock/runtime.py @@ -15,9 +15,9 @@ from StringIO import StringIO from collections import namedtuple +from web_fragments.fragment import Fragment from xblock.fields import Field, BlockScope, Scope, ScopeIds, UserScope from xblock.field_data import FieldData -from xblock.fragment import Fragment from xblock.exceptions import ( NoSuchViewError, NoSuchHandlerError, @@ -909,7 +909,7 @@ def _wrap_ele(self, block, view, frag, extra_data=None): js=json_init) wrapped.add_content(html) - wrapped.add_frag_resources(frag) + wrapped.add_fragment_resources(frag) return wrapped # Asides @@ -996,13 +996,13 @@ def layout_asides(self, block, context, frag, view_name, aside_frag_fns): aside_frag_fns list((aside, aside_fn)): The asides and closures for rendering to call """ result = Fragment(frag.content) - result.add_frag_resources(frag) + result.add_fragment_resources(frag) for aside, aside_fn in aside_frag_fns: aside_frag = self.wrap_aside(block, aside, view_name, aside_fn(block, context), context) aside.save() result.add_content(aside_frag.content) - result.add_frag_resources(aside_frag) + result.add_fragment_resources(aside_frag) return result diff --git a/xblock/test/test_asides.py b/xblock/test/test_asides.py index 1f45d26a3..be475aedd 100644 --- a/xblock/test/test_asides.py +++ b/xblock/test/test_asides.py @@ -2,9 +2,9 @@ Test XBlock Aside """ from unittest import TestCase +from web_fragments.fragment import Fragment from xblock.core import XBlockAside, XBlock from xblock.fields import ScopeIds, Scope, String -from xblock.fragment import Fragment from xblock.runtime import DictKeyValueStore, KvsFieldData from xblock.test.test_runtime import TestXBlock from xblock.test.tools import TestRuntime diff --git a/xblock/test/test_fragment.py b/xblock/test/test_fragment.py new file mode 100644 index 000000000..592f68a90 --- /dev/null +++ b/xblock/test/test_fragment.py @@ -0,0 +1,23 @@ +""" +Unit tests for the Fragment class. + +Note: this class has been deprecated in favor of web_fragments.fragment.Fragment +""" + +from unittest import TestCase + +from xblock.fragment import Fragment + + +class TestFragment(TestCase): + """ + Unit tests for fragments. + """ + def test_fragment(self): + """ + Test the delegated Fragment class. + """ + TEST_HTML = u'

Hello, world!

' + fragment = Fragment() + fragment.add_content(TEST_HTML) + self.assertEqual(fragment.body_html(), TEST_HTML) diff --git a/xblock/test/test_runtime.py b/xblock/test/test_runtime.py index fa5bf7d92..7ddc1eac1 100644 --- a/xblock/test/test_runtime.py +++ b/xblock/test/test_runtime.py @@ -8,6 +8,7 @@ from mock import Mock, patch from unittest import TestCase +from web_fragments.fragment import Fragment from xblock.core import XBlock, XBlockMixin from xblock.fields import BlockScope, Scope, String, ScopeIds, List, UserScope, Integer from xblock.exceptions import ( @@ -26,7 +27,6 @@ Mixologist, ObjectAggregator, ) -from xblock.fragment import Fragment from xblock.field_data import DictFieldData, FieldData from xblock.test.tools import (