Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds xblock-utils repository code into this repository #669

Merged
merged 3 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ jobs:
toxenv: [quality, django32, django42]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: setup python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install pip
run: pip install -r requirements/pip.txt

- name: Install Dependencies
run: pip install -r requirements/ci.txt

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Unreleased
----------


1.8.0 - 2023-09-25
------------------
* Added `xblock-utils <https://github.com/openedx/xblock-utils>`_ repository code into this repository along with docs.

* Docs moved into the docs/ directory.

* See https://github.com/openedx/xblock-utils/issues/197 for more details.

1.7.0 - 2023-08-03
------------------

Expand Down
7 changes: 7 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import pytest


# https://pytest-django.readthedocs.io/en/latest/faq.html#how-can-i-give-database-access-to-all-my-tests-without-the-django-db-marker
@pytest.fixture(autouse=True)
def enable_db_access_for_all_tests(db):
pass
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Binary file added docs/xblock-utils/Images/Screenshot_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/xblock-utils/Images/Screenshot_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
179 changes: 179 additions & 0 deletions docs/xblock-utils/index.rst
Original file line number Diff line number Diff line change
@@ -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
157 changes: 157 additions & 0 deletions docs/xblock-utils/settings-and-theme-support.rst
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

fs
lxml
mako
farhan marked this conversation as resolved.
Show resolved Hide resolved
markupsafe
python-dateutil
pytz
Expand Down
Loading