diff --git a/deform/field.py b/deform/field.py index 9ddb1ba1..3758bd8d 100644 --- a/deform/field.py +++ b/deform/field.py @@ -448,17 +448,21 @@ def get_widget_requirements(self): requirements in :ref:`get_widget_requirements`. """ L = [] - requirements = self.widget.requirements + + requirements = [req for req in self.widget.requirements] + [ + req + for child in self.children + for req in child.get_widget_requirements() + ] + if requirements: for requirement in requirements: - reqt = tuple(requirement) - if reqt not in L: - L.append(reqt) - for child in self.children: - for requirement in child.get_widget_requirements(): - reqt = tuple(requirement) - if reqt not in L: - L.append(reqt) + if isinstance(requirement, dict): + L.append(requirement) + else: + reqt = tuple(requirement) + if reqt not in L: + L.append(reqt) return L def get_widget_resources(self, requirements=None): @@ -485,7 +489,19 @@ def get_widget_resources(self, requirements=None): """ if requirements is None: requirements = self.get_widget_requirements() - return self.resource_registry(requirements) + resources = self.resource_registry( + (req for req in requirements if not isinstance(req, dict)) + ) + for req in requirements: + if not isinstance(req, dict): + continue + for key in {'js', 'css'}.intersection(req): + value = req[key] + if isinstance(value, str): + resources[key].append(value) + else: + resources[key].extend(value) + return resources def set_widgets(self, values, separator="."): """set widgets of the child fields of this field diff --git a/deform/tests/test_field.py b/deform/tests/test_field.py index 255afb10..1bcc93c6 100644 --- a/deform/tests/test_field.py +++ b/deform/tests/test_field.py @@ -346,9 +346,9 @@ def test_get_widget_requirements(self): result, [("abc", "123"), ("ghi", "789"), ("def", "456")] ) - def test_get_widget_resources(self): + def test_get_widget_resources_with_registry(self): def resource_registry(requirements): - self.assertEqual(requirements, [("abc", "123")]) + self.assertEqual(list(requirements), [("abc", "123")]) return "OK" schema = DummySchema() @@ -358,6 +358,17 @@ def resource_registry(requirements): result = field.get_widget_resources() self.assertEqual(result, "OK") + def test_get_widget_resources_without_registry(self): + schema = DummySchema() + field = self._makeOne(schema) + field.widget.requirements = ( + {"js": "123.js", "css": ["123.css"]}, + {"css": ["1.css", "2.css"]}, + ) + result = field.get_widget_resources() + self.assertEqual(result['js'], ['123.js']) + self.assertEqual(result['css'], ["123.css", "1.css", "2.css"]) + def test_clone(self): schema = DummySchema() field = self._makeOne(schema, renderer="abc") diff --git a/deform/widget.py b/deform/widget.py index 6cbbb98b..f5d54cb1 100644 --- a/deform/widget.py +++ b/deform/widget.py @@ -141,9 +141,21 @@ class Widget(object): be added to the input tag. requirements - A sequence of two-tuples in the form ``( (requirement_name, - version_id), ...)`` indicating the logical external - requirements needed to make this widget render properly within + + The requirements are specified as a sequence of either of the + following. + + 1. Two-tuples in the form ``(requirement_name, version_id)``. + The **logical** requirement name identifiers are resolved to + concrete files using the ``resource_registry``. + 2. Dicts in the form ``{requirement_type: + requirement_location(s)}``. The ``resource_registry`` is + bypassed. This is useful for creating custom widgets with + their own resources. + + Requirements specified as a sequence of two-tuples should be in the + form ``( (requirement_name, version_id), ...)`` indicating the logical + external requirements needed to make this widget render properly within a form. The ``requirement_name`` is a string that *logically* (not concretely, it is not a filename) identifies *one or more* Javascript or CSS resources that must be included in the @@ -153,7 +165,23 @@ class Widget(object): 'tinymce' for Tiny MCE). The ``version_id`` is a string indicating the version number (or ``None`` if no particular version is required). For example, a rich text widget might - declare ``requirements = (('tinymce', '3.3.8'),)``. See also: + declare ``requirements = (('tinymce', '3.3.8'),)``. + + Requirements specified as a sequence of dicts should be in the form + ``({requirement_type: requirement_location(s)}, ...)``. The + ``requirement_type`` key must be either ``js`` or ``css``. The + ``requirement_location(s)`` value must be either a string or a list of + strings. Each string must resolve to a concrete resource. For example, + a widget might declare: + + .. code-block:: python + + requirements = ( + {"js": "deform:static/tinymce/tinymce.min.js"}, + {"css": "deform:static/tinymce/tinymce.min.css"}, + ) + + See also: :ref:`specifying_widget_requirements` and :ref:`widget_requirements`. @@ -843,7 +871,7 @@ class RichTextWidget(TextInputWidget): delayed_load = False strip = True template = "richtext" - requirements = (("tinymce", None),) + requirements = ({"js": "deform:static/tinymce/tinymce.min.js"},) #: Default options passed to TinyMCE. Customise by using :attr:`options`. default_options = ( @@ -1164,7 +1192,13 @@ class Select2Widget(SelectWidget): """ template = "select2" - requirements = (("deform", None), ("select2", None)) + requirements = ( + ("deform", None), + { + "js": "deform:static/select2/select2.js", + "css": "deform:static/select2/select2.css", + }, + ) class RadioChoiceWidget(SelectWidget): @@ -1562,7 +1596,10 @@ class SequenceWidget(Widget): min_len = None max_len = None orderable = False - requirements = (("deform", None), ("sortable", None)) + requirements = ( + ("deform", None), + {"js": "deform:static/scripts/jquery-sortable.js"}, + ) def prototype(self, field): # we clone the item field to bump the oid (for easier @@ -1722,7 +1759,7 @@ class FileUploadWidget(Widget): readonly_template = "readonly/file_upload" accept = None - requirements = (("fileupload", None),) + requirements = ({"js": "deform:static/scripts/file_upload.js"},) _pstruct_schema = SchemaNode( Mapping(), @@ -2140,8 +2177,6 @@ def __call__(self, requirements): ) } }, - "tinymce": {None: {"js": "deform:static/tinymce/tinymce.min.js"}}, - "sortable": {None: {"js": "deform:static/scripts/jquery-sortable.js"}}, "typeahead": { None: { "js": "deform:static/scripts/typeahead.min.js", @@ -2168,13 +2203,6 @@ def __call__(self, requirements): ), } }, - "select2": { - None: { - "js": "deform:static/select2/select2.js", - "css": "deform:static/select2/select2.css", - } - }, - "fileupload": {None: {"js": "deform:static/scripts/file_upload.js"}}, } default_resource_registry = ResourceRegistry() diff --git a/docs/widget.rst b/docs/widget.rst index 8dcd38ee..6542adf5 100644 --- a/docs/widget.rst +++ b/docs/widget.rst @@ -253,16 +253,29 @@ See also the documentation of the ``resource_registry`` argument in Specifying Widget Requirements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When creating a new widget, you may specify its requirements by using -the ``requirements`` attribute: +When instantiating a new widget, you may specify its requirements by using the ``requirements`` attribute. +The requirements are specified as a sequence of two-tuples, dicts, or a combination of both two-tuples and dicts. + +The two-tuple form uses the resource registry and is used by most of the core Deform widgets. +The two-tuple form takes advantage of Deform's abstraction layer through the resource registry. + +The dict form bypasses the resource registry. +The dict form may be easier to implement than the two-tuple form for custom widgets because it does not introduce an implicit dependency on the resource registry. +This is especially true if the required resources are tightly coupled to a custom widget. + + +.. _two-tuple-widget-requirements: + +Using two-tuples for specifying widget requirements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python - :linenos: + :linenos: - from deform.widget import Widget + from deform.widget import Widget - class MyWidget(Widget): - requirements = ( ('jquery', '1.4.2'), ) + class MyWidget(Widget): + requirements = ( ("jquery", "1.4.2"), ) There are no hard-and-fast rules about the composition of a requirement name. Your widget's docstring should explain what its @@ -283,6 +296,34 @@ constructing the form. The default resource registry (:attr:`deform.widget.resource_registry`) does not contain resource mappings for your newly-created requirement. + +.. _dict-widget-requirements: + +Using dicts for specifying widget requirements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Requirements specified as a sequence of dicts should be in the form``({requirement_type: requirement_location(s)}, ...)``. +The ``requirement_type`` key must be either ``js`` or ``css``. +The ``requirement_location(s)`` value must be either a string or a list of strings. +Each string must resolve to a concrete resource. + +.. code-block:: python + :linenos: + + from deform.widget import Widget + + class MyWidget(Widget): + requirements = ( { + "js": "my:static/path/to/jquery.js", + "css": [ + "my:static/path/to/jquery.css", + "my:static:path/to/bootstrap.css"], + } ) + +The supplied paths are resolved by ``request.get_path()`` so the required +static resources should be included in the Pyramid config. + + .. _writing_a_widget: Writing Your Own Widget