diff --git a/docs/index.rst b/docs/index.rst index f1aed6c02..02944e382 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,5 +22,6 @@ in depth and guides developers through the process of creating an XBlock. fragment plugins exceptions + xblock-utils/index .. _EdX XBlock Tutorial: http://edx.readthedocs.org/projects/xblock-tutorial/en/latest/index.html diff --git a/docs/xblock-utils/Images/Screenshot_1.png b/docs/xblock-utils/Images/Screenshot_1.png new file mode 100644 index 000000000..663cb0015 Binary files /dev/null and b/docs/xblock-utils/Images/Screenshot_1.png differ diff --git a/docs/xblock-utils/Images/Screenshot_2.png b/docs/xblock-utils/Images/Screenshot_2.png new file mode 100644 index 000000000..ba2cbf46b Binary files /dev/null and b/docs/xblock-utils/Images/Screenshot_2.png differ diff --git a/docs/xblock-utils/index.rst b/docs/xblock-utils/index.rst new file mode 100644 index 000000000..0f9ecb5bb --- /dev/null +++ b/docs/xblock-utils/index.rst @@ -0,0 +1,179 @@ +.. _XBlock Utils: + + +Xblock.utils +############ + +Package having various utilities for XBlocks +******************************************** + + +Purpose +======= + +``xblock/utils`` package contains a collection of utility functions and base test classes that are useful for any XBlock. + + +Documentation +============= + + +StudioEditableXBlockMixin +------------------------- + +.. code:: python + + from xblock.utils.studio_editable import StudioEditableXBlockMixin + +This mixin will automatically generate a working ``studio_view`` form +that allows content authors to edit the fields of your XBlock. To use, +simply add the class to your base class list, and add a new class field +called ``editable_fields``, set to a tuple of the names of the fields +you want your user to be able to edit. + +.. code:: python + + @XBlock.needs("i18n") + class ExampleBlock(StudioEditableXBlockMixin, XBlock): + ... + mode = String( + display_name="Mode", + help="Determines the behaviour of this component. Standard is recommended.", + default='standard', + scope=Scope.content, + values=('standard', 'crazy') + ) + editable_fields = ('mode', 'display_name') + +That's all you need to do. The mixin will read the optional +``display_name``, ``help``, ``default``, and ``values`` settings from +the fields you mention and build the editor form as well as an AJAX save +handler. + +If you want to validate the data, you can override +``validate_field_data(self, validation, data)`` and/or +``clean_studio_edits(self, data)`` - see the source code for details. + +Supported field types: + +* Boolean: + ``field_name = Boolean(display_name="Field Name")`` +* Float: + ``field_name = Float(display_name="Field Name")`` +* Integer: + ``field_name = Integer(display_name="Field Name")`` +* String: + ``field_name = String(display_name="Field Name")`` +* String (multiline): + ``field_name = String(multiline_editor=True, resettable_editor=False)`` +* String (html): + ``field_name = String(multiline_editor='html', resettable_editor=False)`` + +Any of the above will use a dropdown menu if they have a pre-defined +list of possible values. + +* List of unordered unique values (i.e. sets) drawn from a small set of + possible values: + ``field_name = List(list_style='set', list_values_provider=some_method)`` + + - The ``List`` declaration must include the property ``list_style='set'`` to + indicate that the ``List`` field is being used with set semantics. + - The ``List`` declaration must also define a ``list_values_provider`` method + which will be called with the block as its only parameter and which must + return a list of possible values. +* Rudimentary support for Dict, ordered List, and any other JSONField-derived field types + + - ``list_field = List(display_name="Ordered List", default=[])`` + - ``dict_field = Dict(display_name="Normal Dict", default={})`` + +Supported field options (all field types): + +* ``values`` can define a list of possible options, changing the UI element + to a select box. Values can be set to any of the formats `defined in the + XBlock source code <https://github.com/openedx/XBlock/blob/master/xblock/fields.py>`__: + + - A finite set of elements: ``[1, 2, 3]`` + - A finite set of elements where the display names differ from the values:: + + [ + {"display_name": "Always", "value": "always"}, + {"display_name": "Past Due", "value": "past_due"}, + ] + + - A range for floating point numbers with specific increments: + ``{"min": 0 , "max": 10, "step": .1}`` + - A callable that returns one of the above. (Note: the callable does + *not* get passed the XBlock instance or runtime, so it cannot be a + normal member function) +* ``values_provider`` can define a callable that accepts the XBlock + instance as an argument, and returns a list of possible values in one + of the formats listed above. +* ``resettable_editor`` - defaults to ``True``. Set ``False`` to hide the + "Reset" button used to return a field to its default value by removing + the field's value from the XBlock instance. + +Basic screenshot: |Screenshot 1| + +StudioContainerXBlockMixin +-------------------------- + +.. code:: python + + from xblock.utils.studio_editable import StudioContainerXBlockMixin + +This mixin helps to create XBlocks that allow content authors to add, +remove, or reorder child blocks. By removing any existing +``author_view`` and adding this mixin, you'll get editable, +re-orderable, and deletable child support in Studio. To enable authors to +add arbitrary blocks as children, simply override ``author_edit_view`` +and set ``can_add=True`` when calling ``render_children`` - see the +source code. To restrict authors so they can add only specific types of +child blocks or a limited number of children requires custom HTML. + +An example is the mentoring XBlock: |Screenshot 2| + + +child\_isinstance +------------------------- + +.. code:: python + + from xblock.utils.helpers import child_isinstance + +If your XBlock needs to find children/descendants of a particular +class/mixin, you should use + +.. code:: python + + child_isinstance(self, child_usage_id, SomeXBlockClassOrMixin) + +rather than calling + +.. code:: python + + isinstance(self.runtime.get_block(child_usage_id), SomeXBlockClassOrMixin) + +On runtimes such as those in edx-platform, ``child_isinstance`` is +orders of magnitude faster. + +.. |Screenshot 1| image:: Images/Screenshot_1.png +.. |Screenshot 2| image:: Images/Screenshot_2.png + +XBlockWithSettingsMixin +------------------------- + +This mixin provides access to instance-wide XBlock-specific configuration settings. +See :ref:`accessing-xblock-specific-settings` for details. + +ThemableXBlockMixin +------------------------- + +This mixin provides XBlock theming capabilities built on top of XBlock-specific settings. +See :ref:`theming-support` for details. + +To learn more, refer to the page. + +.. toctree:: + :caption: Contents: + + settings-and-theme-support diff --git a/docs/xblock-utils/settings-and-theme-support.rst b/docs/xblock-utils/settings-and-theme-support.rst new file mode 100644 index 000000000..1b4b9fa83 --- /dev/null +++ b/docs/xblock-utils/settings-and-theme-support.rst @@ -0,0 +1,157 @@ +.. _settings-and-theme-support: + + +Settings and theme support +########################## + +.. _accessing-xblock-specific-settings: + +Accessing XBlock specific settings +********************************** + +XBlock utils provide a mixin to simplify accessing instance-wide +XBlock-specific configuration settings: ``XBlockWithSettingsMixin``. +This mixin aims to provide a common interface for pulling XBlock +settings from the LMS +`SettingsService <https://github.com/edx/edx-platform/blob/master/common/lib/xmodule/xmodule/services.py>`__. + +``SettingsService`` allows individual XBlocks to access environment and +django settings in an isolated manner: + +- XBlock settings are represented as dictionary stored in `django + settings <https://github.com/edx/edx-platform/blob/master/cms/envs/aws.py#L341-342>`__ + and populated from environment \*.json files (cms.env.json and + lms.env.json) +- Each XBlock is associated with a particular key in that dictionary: + by default an XBlock's class name is used, but XBlocks can override + it using the ``block_settings_key`` attribute/property. + +Please note that at the time of writing the implementation of +``SettingsService`` assumed "good citizenship" behavior on the part of +XBlocks, i.e. it does not check for key collisions and allows modifying +mutable settings. Both ``SettingsService`` and +``XBlockWithSettingsMixin`` are not concerned with contents of settings +bucket and return them as is. Refer to the ``SettingsService`` docstring +and implementation for more details. + +Using XBlockWithSettingsMixin +============================= + +In order to use ``SettingsService`` and ``XBlockWithSettingsMixin``, a +client XBlock *must* require it via standard +``XBlock.wants('settings')`` or ``XBlock.needs('settings')`` decorators. +The mixins themselves are not decorated as this would not result in all +descendant XBlocks to also be decorated. + +With ``XBlockWithSettingsMixin`` and ``wants`` decorator applied, +obtaining XBlock settings is as simple as + +.. code:: python + + self.get_xblock_settings() # returns settings bucket or None + self.get_xblock_settings(default=something) # returns settings bucket or "something" + +In case of missing or inaccessible XBlock settings (i.e. no settings +service in runtime, no ``XBLOCK_SETTINGS`` in settings, or XBlock +settings key is not found) ``default`` value is used. + +.. _theming-support: + +Theming support +*************** + +XBlock theming support is built on top of XBlock-specific settings. +XBlock utils provide ``ThemableXBlockMixin`` to streamline using XBlock +themes. + +XBlock theme support is designed with two major design goals: + +- Allow for a different look and feel of an XBlock in different + environments. +- Use a pluggable approach to hosting themes, so that adding a new + theme will not require forking an XBlock. + +The first goal made using ``SettingsService`` and +``XBlockWithSettingsMixin`` an obvious choice to store and obtain theme +configuration. The second goal dictated the configuration format - it is +a dictionary (or dictionary-like object) with the following keys: + +- ``package`` - "top-level" selector specifying package which hosts + theme files +- ``locations`` - a list of locations within that package + +Examples: + +.. code:: python + + # will search for files red.css and small.css in my_xblock package + { + 'package': 'my_xblock', + 'locations': ['red.css', 'small.css'] + } + + # will search for files public/themes/red.css in my_other_xblock.assets package + default_theme_config = { + 'package': 'my_other_xblock.assets', + 'locations': ['public/themes/red.css'] + } + +Theme files must be included into package (see `python +docs <https://docs.python.org/2/distutils/setupscript.html#installing-package-data>`__ +for details). At the time of writing it is not possible to fetch theme +files from multiple packages. + +**Note:** XBlock themes are *not* LMS themes - they are just additional +CSS files included into an XBlock fragment when the corresponding XBlock +is rendered. However, it is possible to misuse this feature to change +look and feel of the entire LMS, as contents of CSS files are not +checked and might contain selectors that apply to elements outside of +the XBlock in question. Hence, it is advised to scope all CSS rules +belonging to a theme with a global CSS selector +``.themed-xblock.<root xblock element class>``, e.g. +``.themed-xblock.poll-block``. Note that the ``themed-xblock`` class is +not automatically added by ``ThemableXBlockMixin``, so one needs to add +it manually. + +Using ThemableXBlockMixin +========================= + +In order to use ``ThemableXBlockMixin``, a descendant XBlock must also +be a descendant of ``XBlockWithSettingsMixin`` (``XBlock.wants`` +decorator requirement applies) or provide a similar interface for +obtaining the XBlock settings bucket. + +There are three configuration parameters that govern +``ThemableXBlockMixin`` behavior: + +- ``default_theme_config`` - default theme configuration in case no + theme configuration can be obtained +- ``theme_key`` - a key in XBlock settings bucket that stores theme + configuration +- ``block_settings_key`` - inherited from ``XBlockWithSettingsMixin`` + if used in conjunction with it + +It is safe to omit ``default_theme_config`` or set it to ``None`` in +case no default theme is available. In this case, +``ThemableXBlockMixin`` will skip including theme files if no theme is +specified via settings. + +``ThemableXBlockMixin`` exposes two methods: + +- ``get_theme()`` - this is used to get theme configuration. Default + implementation uses ``get_xblock_settings`` and ``theme_key``, + descendants are free to override it. Normally, it should not be + called directly. +- ``include_theme_files(fragment)`` - this method is an entry point to + ``ThemableXBlockMixin`` functionality. It calls ``get_theme`` to + obtain theme configuration, fetches theme files and includes them + into fragment. ``fragment`` must be an + `XBlock.Fragment <https://github.com/edx/XBlock/blob/master/xblock/fragment.py>`__ + instance. + +So, having met usage requirements and set up theme configuration +parameters, including theme into XBlock fragment is a one liner: + +.. code:: python + + self.include_theme_files(fragment)