From ed045db230b0627c7764994d1fd89c2fa3fafe9c Mon Sep 17 00:00:00 2001 From: farhan Date: Mon, 25 Sep 2023 20:10:19 +0500 Subject: [PATCH 1/3] feat: Adds xblock-utils repository code into this repository --- .github/workflows/ci.yml | 7 +- CHANGELOG.rst | 8 + conftest.py | 7 + requirements/base.in | 1 + requirements/test.in | 1 + setup.py | 6 + xblock/__init__.py | 2 +- xblock/test/settings.py | 1 + xblock/test/test_plugin.py | 6 +- xblock/test/utils/__init__.py | 0 xblock/test/utils/data/__init__.py | 0 xblock/test/utils/data/another_template.xml | 1 + .../test/utils/data/l10n_django_template.txt | 3 + .../utils/data/simple_django_template.txt | 14 + .../test/utils/data/simple_mako_template.txt | 18 + xblock/test/utils/data/simple_template.xml | 6 + .../test/utils/data/trans_django_template.txt | 8 + .../data/translations/eo/LC_MESSAGES/text.mo | Bin 0 -> 488 bytes .../data/translations/eo/LC_MESSAGES/text.po | 29 + xblock/test/utils/test_helpers.py | 81 +++ xblock/test/utils/test_publish_event.py | 100 ++++ xblock/test/utils/test_resources.py | 234 ++++++++ xblock/test/utils/test_settings.py | 163 ++++++ xblock/utils/__init__.py | 0 xblock/utils/helpers.py | 25 + xblock/utils/public/studio_container.js | 63 +++ xblock/utils/public/studio_edit.js | 175 ++++++ xblock/utils/publish_event.py | 38 ++ xblock/utils/resources.py | 107 ++++ xblock/utils/settings.py | 88 +++ xblock/utils/studio_editable.py | 511 ++++++++++++++++++ xblock/utils/templates/add_buttons.html | 22 + .../utils/templates/default_preview_view.html | 3 + xblock/utils/templates/studio_edit.html | 113 ++++ xblock/utils/templatetags/__init__.py | 0 xblock/utils/templatetags/i18n.py | 73 +++ 36 files changed, 1908 insertions(+), 6 deletions(-) create mode 100644 conftest.py create mode 100644 xblock/test/utils/__init__.py create mode 100644 xblock/test/utils/data/__init__.py create mode 100644 xblock/test/utils/data/another_template.xml create mode 100644 xblock/test/utils/data/l10n_django_template.txt create mode 100644 xblock/test/utils/data/simple_django_template.txt create mode 100644 xblock/test/utils/data/simple_mako_template.txt create mode 100644 xblock/test/utils/data/simple_template.xml create mode 100644 xblock/test/utils/data/trans_django_template.txt create mode 100644 xblock/test/utils/data/translations/eo/LC_MESSAGES/text.mo create mode 100644 xblock/test/utils/data/translations/eo/LC_MESSAGES/text.po create mode 100644 xblock/test/utils/test_helpers.py create mode 100644 xblock/test/utils/test_publish_event.py create mode 100644 xblock/test/utils/test_resources.py create mode 100644 xblock/test/utils/test_settings.py create mode 100644 xblock/utils/__init__.py create mode 100644 xblock/utils/helpers.py create mode 100644 xblock/utils/public/studio_container.js create mode 100644 xblock/utils/public/studio_edit.js create mode 100644 xblock/utils/publish_event.py create mode 100644 xblock/utils/resources.py create mode 100644 xblock/utils/settings.py create mode 100644 xblock/utils/studio_editable.py create mode 100644 xblock/utils/templates/add_buttons.html create mode 100644 xblock/utils/templates/default_preview_view.html create mode 100644 xblock/utils/templates/studio_edit.html create mode 100644 xblock/utils/templatetags/__init__.py create mode 100644 xblock/utils/templatetags/i18n.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca3fff68f..0efc7b6b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9eb07fdbd..52bcdde15 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,14 @@ Unreleased ---------- +1.8.0 - 2023-09-25 +------------------ +* Added `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 ------------------ diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..a90ee5eec --- /dev/null +++ b/conftest.py @@ -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 diff --git a/requirements/base.in b/requirements/base.in index b70901c8f..5bb2941e0 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -3,6 +3,7 @@ fs lxml +mako markupsafe python-dateutil pytz diff --git a/requirements/test.in b/requirements/test.in index 4a5d2a57b..7ddb1238f 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -18,3 +18,4 @@ pytest pytest-cov pytest-django tox +xblock-sdk diff --git a/setup.py b/setup.py index b22473b45..e921e4220 100755 --- a/setup.py +++ b/setup.py @@ -36,10 +36,16 @@ def get_version(*file_paths): 'xblock', 'xblock.django', 'xblock.reference', + 'xblock.utils', 'xblock.test', 'xblock.test.django', + 'xblock.test.utils', ], include_package_data=True, + package_data={ + 'xblock.utils': ['public/*', 'templates/*', 'templatetags/*'], + 'xblock.test.utils': ['data/*'], + }, install_requires=[ 'fs', 'lxml', diff --git a/xblock/__init__.py b/xblock/__init__.py index b68166211..014831f2c 100644 --- a/xblock/__init__.py +++ b/xblock/__init__.py @@ -27,4 +27,4 @@ def __init__(self, *args, **kwargs): # without causing a circular import xblock.fields.XBlockMixin = XBlockMixin -__version__ = '1.7.0' +__version__ = '1.8.0' diff --git a/xblock/test/settings.py b/xblock/test/settings.py index ee62f3874..5fb5608d9 100644 --- a/xblock/test/settings.py +++ b/xblock/test/settings.py @@ -109,6 +109,7 @@ # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', + 'workbench' ) # A sample logging configuration. The only tangible logging diff --git a/xblock/test/test_plugin.py b/xblock/test/test_plugin.py index 126e2e77b..c2832cdb1 100644 --- a/xblock/test/test_plugin.py +++ b/xblock/test/test_plugin.py @@ -75,13 +75,13 @@ def _num_plugins_cached(): return len(plugin.PLUGIN_CACHE) -@XBlock.register_temp_plugin(AmbiguousBlock1, "thumbs") +@XBlock.register_temp_plugin(AmbiguousBlock1, "ambiguous_block_1") def test_plugin_caching(): plugin.PLUGIN_CACHE = {} assert _num_plugins_cached() == 0 - XBlock.load_class("thumbs") + XBlock.load_class("ambiguous_block_1") assert _num_plugins_cached() == 1 - XBlock.load_class("thumbs") + XBlock.load_class("ambiguous_block_1") assert _num_plugins_cached() == 1 diff --git a/xblock/test/utils/__init__.py b/xblock/test/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/xblock/test/utils/data/__init__.py b/xblock/test/utils/data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/xblock/test/utils/data/another_template.xml b/xblock/test/utils/data/another_template.xml new file mode 100644 index 000000000..df6391b1f --- /dev/null +++ b/xblock/test/utils/data/another_template.xml @@ -0,0 +1 @@ +This is an even simpler xml template. diff --git a/xblock/test/utils/data/l10n_django_template.txt b/xblock/test/utils/data/l10n_django_template.txt new file mode 100644 index 000000000..9d796d378 --- /dev/null +++ b/xblock/test/utils/data/l10n_django_template.txt @@ -0,0 +1,3 @@ +{% load l10n %} +{{ 1000|localize }} +{{ 1000|unlocalize }} diff --git a/xblock/test/utils/data/simple_django_template.txt b/xblock/test/utils/data/simple_django_template.txt new file mode 100644 index 000000000..00566d218 --- /dev/null +++ b/xblock/test/utils/data/simple_django_template.txt @@ -0,0 +1,14 @@ +This is a simple template example. + +This template can make use of the following context variables: +Name: {{name}} +List: {{items|safe}} + +It can also do some fancy things with them: +Default value if name is empty: {{name|default:"Default Name"}} +Length of the list: {{items|length}} +Items of the list:{% for item in items %} {{item}}{% endfor %} + +Although it is simple, it can also contain non-ASCII characters: + +Thé Fütüré øf Ønlïné Édüçätïøn Ⱡσяєм ι# Før änýøné, änýwhéré, änýtïmé Ⱡσяєм # diff --git a/xblock/test/utils/data/simple_mako_template.txt b/xblock/test/utils/data/simple_mako_template.txt new file mode 100644 index 000000000..4f0a77a47 --- /dev/null +++ b/xblock/test/utils/data/simple_mako_template.txt @@ -0,0 +1,18 @@ +This is a simple template example. + +This template can make use of the following context variables: +Name: ${name} +List: ${items} + +It can also do some fancy things with them: +Default value if name is empty: ${ name or "Default Name"} +Length of the list: ${len(items)} +Items of the list:\ +% for item in items: + ${item}\ +% endfor + + +Although it is simple, it can also contain non-ASCII characters: + +Thé Fütüré øf Ønlïné Édüçätïøn Ⱡσяєм ι# Før änýøné, änýwhéré, änýtïmé Ⱡσяєм # diff --git a/xblock/test/utils/data/simple_template.xml b/xblock/test/utils/data/simple_template.xml new file mode 100644 index 000000000..e60fdc535 --- /dev/null +++ b/xblock/test/utils/data/simple_template.xml @@ -0,0 +1,6 @@ + + This is a simple xml template. + + {{url_name}} + + diff --git a/xblock/test/utils/data/trans_django_template.txt b/xblock/test/utils/data/trans_django_template.txt new file mode 100644 index 000000000..b144d96f6 --- /dev/null +++ b/xblock/test/utils/data/trans_django_template.txt @@ -0,0 +1,8 @@ +{% load i18n %} +{% trans "Translate 1" %} +{% trans "Translate 2" as var %} +{{ var }} +{% blocktrans %} +Multi-line translation +with variable: {{name}} +{% endblocktrans %} diff --git a/xblock/test/utils/data/translations/eo/LC_MESSAGES/text.mo b/xblock/test/utils/data/translations/eo/LC_MESSAGES/text.mo new file mode 100644 index 0000000000000000000000000000000000000000..d931e686327a9c1476af88e5d485492621b3d288 GIT binary patch literal 488 zcmaKo%}xR_5XbA+rQSRm55>d;Vya!fLYC}-AS5IU5?#D%fe~s-OS%R1HB7wv9=?n5 zS)4@$Pfq&FOq==7zwPJ2?kmCAL5`3zvWLV-7I%n1B(jgZActFoykR~@-m%WkKQQB3 z$2L~kOhUU0?G@L`0P{|oR7*-|?BBjvrk?-WT89>~ zG?Rs#08c>9aNq58TbmmMXM#4Z@nBH)JV$>IPyT$ar80Fkno5^~j|NGb*EW6GtM0+^ zmKoWxGLw+ihRUyja3g= Y;u_{!H+;wT{{E!DlD#yR39uab0=0I5YXATM literal 0 HcmV?d00001 diff --git a/xblock/test/utils/data/translations/eo/LC_MESSAGES/text.po b/xblock/test/utils/data/translations/eo/LC_MESSAGES/text.po new file mode 100644 index 000000000..81052c559 --- /dev/null +++ b/xblock/test/utils/data/translations/eo/LC_MESSAGES/text.po @@ -0,0 +1,29 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2016-03-30 16:54+0500\n" +"PO-Revision-Date: 2016-03-30 16:54+0500\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: eo\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: data/trans_django_template.txt +msgid "Translate 1" +msgstr "tRaNsLaTe !" + +#: data/trans_django_template.txt +msgid "Translate 2" +msgstr "" + +#: data/trans_django_template.txt +msgid "" +"\n" +"Multi-line translation" +"\n" +"with variable: %(name)s" +"\n" +msgstr "\nmUlTi_LiNe TrAnSlAtIoN: %(name)s\n" diff --git a/xblock/test/utils/test_helpers.py b/xblock/test/utils/test_helpers.py new file mode 100644 index 000000000..2c342a2c5 --- /dev/null +++ b/xblock/test/utils/test_helpers.py @@ -0,0 +1,81 @@ +""" +Tests for helpers.py +""" + +import unittest + +from workbench.runtime import WorkbenchRuntime + +from xblock.core import XBlock +from xblock.utils.helpers import child_isinstance + + +# pylint: disable=unnecessary-pass +class DogXBlock(XBlock): + """ Test XBlock representing any dog. Raises error if instantiated. """ + pass + + +# pylint: disable=unnecessary-pass +class GoldenRetrieverXBlock(DogXBlock): + """ Test XBlock representing a golden retriever """ + pass + + +# pylint: disable=unnecessary-pass +class CatXBlock(XBlock): + """ Test XBlock representing any cat """ + pass + + +class BasicXBlock(XBlock): + """ Basic XBlock """ + has_children = True + + +class TestChildIsInstance(unittest.TestCase): + """ + Test child_isinstance helper method, in the workbench runtime. + """ + + @XBlock.register_temp_plugin(GoldenRetrieverXBlock, "gr") + @XBlock.register_temp_plugin(CatXBlock, "cat") + @XBlock.register_temp_plugin(BasicXBlock, "block") + def test_child_isinstance(self): + """ + Check that child_isinstance() works on direct children + """ + runtime = WorkbenchRuntime() + root_id = runtime.parse_xml_string(' ') + root = runtime.get_block(root_id) + self.assertFalse(child_isinstance(root, root.children[0], DogXBlock)) + self.assertFalse(child_isinstance(root, root.children[0], GoldenRetrieverXBlock)) + self.assertTrue(child_isinstance(root, root.children[0], BasicXBlock)) + + self.assertFalse(child_isinstance(root, root.children[1], DogXBlock)) + self.assertFalse(child_isinstance(root, root.children[1], GoldenRetrieverXBlock)) + self.assertTrue(child_isinstance(root, root.children[1], CatXBlock)) + + self.assertFalse(child_isinstance(root, root.children[2], CatXBlock)) + self.assertTrue(child_isinstance(root, root.children[2], DogXBlock)) + self.assertTrue(child_isinstance(root, root.children[2], GoldenRetrieverXBlock)) + + @XBlock.register_temp_plugin(GoldenRetrieverXBlock, "gr") + @XBlock.register_temp_plugin(CatXBlock, "cat") + @XBlock.register_temp_plugin(BasicXBlock, "block") + def test_child_isinstance_descendants(self): + """ + Check that child_isinstance() works on deeper descendants + """ + runtime = WorkbenchRuntime() + root_id = runtime.parse_xml_string(' ') + root = runtime.get_block(root_id) + block = root.runtime.get_block(root.children[0]) + self.assertIsInstance(block, BasicXBlock) + + self.assertFalse(child_isinstance(root, block.children[0], DogXBlock)) + self.assertTrue(child_isinstance(root, block.children[0], CatXBlock)) + + self.assertTrue(child_isinstance(root, block.children[1], DogXBlock)) + self.assertTrue(child_isinstance(root, block.children[1], GoldenRetrieverXBlock)) + self.assertFalse(child_isinstance(root, block.children[1], CatXBlock)) diff --git a/xblock/test/utils/test_publish_event.py b/xblock/test/utils/test_publish_event.py new file mode 100644 index 000000000..6c463495c --- /dev/null +++ b/xblock/test/utils/test_publish_event.py @@ -0,0 +1,100 @@ +""" +Test cases for xblock/utils/publish_event.py +""" + + +import unittest + +import simplejson as json + +from xblock.utils.publish_event import PublishEventMixin + + +class EmptyMock(): + pass + + +class RequestMock: + method = "POST" + + def __init__(self, data): + self.body = json.dumps(data).encode('utf-8') + + +class RuntimeMock: + last_call = None + + def publish(self, block, event_type, data): + self.last_call = (block, event_type, data) + + +class XBlockMock: + def __init__(self): + self.runtime = RuntimeMock() + + +class ObjectUnderTest(XBlockMock, PublishEventMixin): + pass + + +class TestPublishEventMixin(unittest.TestCase): + """ + Test cases for PublishEventMixin + """ + def assert_no_calls_made(self, block): + self.assertFalse(block.last_call) + + def assert_success(self, response): + self.assertEqual(json.loads(response.body)['result'], 'success') + + def assert_error(self, response): + self.assertEqual(json.loads(response.body)['result'], 'error') + + def test_error_when_no_event_type(self): + block = ObjectUnderTest() + + response = block.publish_event(RequestMock({})) + + self.assert_error(response) + self.assert_no_calls_made(block.runtime) + + def test_uncustomized_publish_event(self): + block = ObjectUnderTest() + + event_data = {"one": 1, "two": 2, "bool": True} + data = dict(event_data) + data["event_type"] = "test.event.uncustomized" + + response = block.publish_event(RequestMock(data)) + + self.assert_success(response) + self.assertEqual(block.runtime.last_call, (block, "test.event.uncustomized", event_data)) + + def test_publish_event_with_additional_data(self): + block = ObjectUnderTest() + block.additional_publish_event_data = {"always_present": True, "block_id": "the-block-id"} + + event_data = {"foo": True, "bar": False, "baz": None} + data = dict(event_data) + data["event_type"] = "test.event.customized" + + response = block.publish_event(RequestMock(data)) + + expected_data = dict(event_data) + expected_data.update(block.additional_publish_event_data) + + self.assert_success(response) + self.assertEqual(block.runtime.last_call, (block, "test.event.customized", expected_data)) + + def test_publish_event_fails_with_duplicate_data(self): + block = ObjectUnderTest() + block.additional_publish_event_data = {"good_argument": True, "clashing_argument": True} + + event_data = {"fine_argument": True, "clashing_argument": False} + data = dict(event_data) + data["event_type"] = "test.event.clashing" + + response = block.publish_event(RequestMock(data)) + + self.assert_error(response) + self.assert_no_calls_made(block.runtime) diff --git a/xblock/test/utils/test_resources.py b/xblock/test/utils/test_resources.py new file mode 100644 index 000000000..d95b0114d --- /dev/null +++ b/xblock/test/utils/test_resources.py @@ -0,0 +1,234 @@ +""" +Tests for resources.py +""" + + +import gettext +import unittest +from unittest.mock import patch, DEFAULT + +from pkg_resources import resource_filename + +from xblock.utils.resources import ResourceLoader + +expected_string = """\ +This is a simple template example. + +This template can make use of the following context variables: +Name: {{name}} +List: {{items|safe}} + +It can also do some fancy things with them: +Default value if name is empty: {{name|default:"Default Name"}} +Length of the list: {{items|length}} +Items of the list:{% for item in items %} {{item}}{% endfor %} + +Although it is simple, it can also contain non-ASCII characters: + +Thé Fütüré øf Ønlïné Édüçätïøn Ⱡσяєм ι# Før änýøné, änýwhéré, änýtïmé Ⱡσяєм # +""" + + +example_context = { + "name": "This is a fine name", + "items": [1, 2, 3, 4, "a", "b", "c"], +} + + +expected_filled_template = """\ +This is a simple template example. + +This template can make use of the following context variables: +Name: This is a fine name +List: [1, 2, 3, 4, 'a', 'b', 'c'] + +It can also do some fancy things with them: +Default value if name is empty: This is a fine name +Length of the list: 7 +Items of the list: 1 2 3 4 a b c + +Although it is simple, it can also contain non-ASCII characters: + +Thé Fütüré øf Ønlïné Édüçätïøn Ⱡσяєм ι# Før änýøné, änýwhéré, änýtïmé Ⱡσяєм # +""" + +expected_not_translated_template = """\ + +Translate 1 + +Translate 2 + +Multi-line translation +with variable: This is a fine name + +""" + +expected_translated_template = """\ + +tRaNsLaTe ! + +Translate 2 + +mUlTi_LiNe TrAnSlAtIoN: This is a fine name + +""" + +expected_localized_template = """\ + +1000 +1000 +""" + +example_id = "example-unique-id" + +expected_filled_js_template = """\ +\ +""".format(expected_filled_template) + +expected_filled_translated_js_template = """\ +\ +""".format(expected_translated_template) + +expected_filled_not_translated_js_template = """\ +\ +""".format(expected_not_translated_template) + +expected_filled_localized_js_template = """\ +\ +""".format(expected_localized_template) + +another_template = """\ +This is an even simpler xml template. +""" + + +simple_template = """\ + + This is a simple xml template. + + simple_template + + +""" + + +expected_scenarios_with_identifiers = [ + ("another_template", "Another Template", another_template), + ("simple_template", "Simple Template", simple_template), +] + + +expected_scenarios = [(t, c) for (i, t, c) in expected_scenarios_with_identifiers] + + +class MockI18nService: + """ + I18n service used for testing translations. + """ + def __init__(self): + + locale_dir = 'data/translations' + locale_path = resource_filename(__name__, locale_dir) + domain = 'text' + self.mock_translator = gettext.translation( + domain, + locale_path, + ['eo'], + ) + + def __getattr__(self, name): + return getattr(self.mock_translator, name) + + +class TestResourceLoader(unittest.TestCase): + """ + Unit Tests for ResourceLoader + """ + + def test_load_unicode(self): + s = ResourceLoader(__name__).load_unicode("data/simple_django_template.txt") + self.assertEqual(s, expected_string) + + def test_load_unicode_from_another_module(self): + s = ResourceLoader("xblock.test.utils.data").load_unicode("simple_django_template.txt") + self.assertEqual(s, expected_string) + + def test_render_django_template(self): + loader = ResourceLoader(__name__) + s = loader.render_django_template("data/simple_django_template.txt", example_context) + self.assertEqual(s, expected_filled_template) + + def test_render_django_template_translated(self): + loader = ResourceLoader(__name__) + s = loader.render_django_template("data/trans_django_template.txt", + context=example_context, + i18n_service=MockI18nService()) + self.assertEqual(s, expected_translated_template) + + # Test that the language changes were reverted + s = loader.render_django_template("data/trans_django_template.txt", example_context) + self.assertEqual(s, expected_not_translated_template) + + def test_render_django_template_localized(self): + # Test that default template tags like l10n are loaded + loader = ResourceLoader(__name__) + s = loader.render_django_template("data/l10n_django_template.txt", + context=example_context, + i18n_service=MockI18nService()) + self.assertEqual(s, expected_localized_template) + + def test_render_mako_template(self): + loader = ResourceLoader(__name__) + s = loader.render_mako_template("data/simple_mako_template.txt", example_context) + self.assertEqual(s, expected_filled_template) + + @patch('warnings.warn', DEFAULT) + def test_render_template_deprecated(self, mock_warn): + loader = ResourceLoader(__name__) + s = loader.render_template("data/simple_django_template.txt", example_context) + self.assertTrue(mock_warn.called) + self.assertEqual(s, expected_filled_template) + + def test_render_js_template(self): + loader = ResourceLoader(__name__) + s = loader.render_js_template("data/simple_django_template.txt", example_id, example_context) + self.assertEqual(s, expected_filled_js_template) + + def test_render_js_template_translated(self): + loader = ResourceLoader(__name__) + s = loader.render_js_template("data/trans_django_template.txt", + example_id, + context=example_context, + i18n_service=MockI18nService()) + self.assertEqual(s, expected_filled_translated_js_template) + + # Test that the language changes were reverted + s = loader.render_js_template("data/trans_django_template.txt", example_id, example_context) + self.assertEqual(s, expected_filled_not_translated_js_template) + + def test_render_js_template_localized(self): + # Test that default template tags like l10n are loaded + loader = ResourceLoader(__name__) + s = loader.render_js_template("data/l10n_django_template.txt", + example_id, + context=example_context, + i18n_service=MockI18nService()) + self.assertEqual(s, expected_filled_localized_js_template) + + def test_load_scenarios(self): + loader = ResourceLoader(__name__) + scenarios = loader.load_scenarios_from_path("data") + self.assertEqual(scenarios, expected_scenarios) + + def test_load_scenarios_with_identifiers(self): + loader = ResourceLoader(__name__) + scenarios = loader.load_scenarios_from_path("data", include_identifier=True) + self.assertEqual(scenarios, expected_scenarios_with_identifiers) diff --git a/xblock/test/utils/test_settings.py b/xblock/test/utils/test_settings.py new file mode 100644 index 000000000..f5593a553 --- /dev/null +++ b/xblock/test/utils/test_settings.py @@ -0,0 +1,163 @@ +""" +Test cases for xblock/utils/settings.py +""" +import itertools +import unittest +from unittest.mock import Mock, MagicMock, patch + +import ddt + +from xblock.core import XBlock +from xblock.utils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin + + +@XBlock.wants('settings') +class DummyXBlockWithSettings(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): + """ + A dummy XBlock test class provides configurable theme support via Settings Service + """ + block_settings_key = 'dummy_settings_bucket' + default_theme_config = { + 'package': 'xblock_utils', + 'locations': ['qwe.css'] + } + + +@XBlock.wants('settings') +class OtherXBlockWithSettings(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): + """ + Another XBlock test class provides configurable theme support via Settings Service + """ + block_settings_key = 'other_settings_bucket' + theme_key = 'other_xblock_theme' + default_theme_config = { + 'package': 'xblock_utils', + 'locations': ['qwe.css'] + } + + +@ddt.ddt +class TestXBlockWithSettingsMixin(unittest.TestCase): + """ + Test cases for XBlockWithSettingsMixin + """ + def setUp(self): + self.settings_service = Mock() + self.runtime = Mock() + self.runtime.service = Mock(return_value=self.settings_service) + + @ddt.data(None, 1, "2", [3, 4], {5: '6'}) + def test_no_settings_service_return_default(self, default_value): + xblock = DummyXBlockWithSettings(self.runtime, scope_ids=Mock()) + self.runtime.service.return_value = None + self.assertEqual(xblock.get_xblock_settings(default=default_value), default_value) + + @ddt.data(*itertools.product( + (DummyXBlockWithSettings, OtherXBlockWithSettings), + (None, 1, "2", [3, 4], {5: '6'}), + (None, 'default1') + )) + @ddt.unpack + def test_invokes_get_settings_bucket_and_returns_result(self, block, settings_service_return_value, default): + xblock = block(self.runtime, scope_ids=Mock()) + + self.settings_service.get_settings_bucket = Mock(return_value=settings_service_return_value) + self.assertEqual(xblock.get_xblock_settings(default=default), settings_service_return_value) + self.settings_service.get_settings_bucket.assert_called_with(xblock, default=default) + + +@ddt.ddt +class TestThemableXBlockMixin(unittest.TestCase): + """ + Test cases for ThemableXBlockMixin + """ + def setUp(self): + self.service_mock = Mock() + self.runtime_mock = Mock() + self.runtime_mock.service = Mock(return_value=self.service_mock) + + @ddt.data(DummyXBlockWithSettings, OtherXBlockWithSettings) + def test_theme_uses_default_theme_if_settings_service_is_not_available(self, xblock_class): + xblock = xblock_class(self.runtime_mock, scope_ids=Mock()) + self.runtime_mock.service = Mock(return_value=None) + self.assertEqual(xblock.get_theme(), xblock_class.default_theme_config) + + @ddt.data(DummyXBlockWithSettings, OtherXBlockWithSettings) + def test_theme_uses_default_theme_if_no_theme_is_set(self, xblock_class): + xblock = xblock_class(self.runtime_mock, scope_ids=Mock()) + self.service_mock.get_settings_bucket = Mock(return_value=None) + self.assertEqual(xblock.get_theme(), xblock_class.default_theme_config) + self.service_mock.get_settings_bucket.assert_called_once_with(xblock, default={}) + + @ddt.data(*itertools.product( + (DummyXBlockWithSettings, OtherXBlockWithSettings), + (123, object()) + )) + @ddt.unpack + def test_theme_raises_if_theme_object_is_not_iterable(self, xblock_class, theme_config): + xblock = xblock_class(self.runtime_mock, scope_ids=Mock()) + self.service_mock.get_settings_bucket = Mock(return_value=theme_config) + with self.assertRaises(TypeError): + xblock.get_theme() + self.service_mock.get_settings_bucket.assert_called_once_with(xblock, default={}) + + @ddt.data(*itertools.product( + (DummyXBlockWithSettings, OtherXBlockWithSettings), + ({}, {'mass': 123}, {'spin': {}}, {'parity': "1"}) + )) + @ddt.unpack + def test_theme_uses_default_theme_if_no_mentoring_theme_is_set_up(self, xblock_class, theme_config): + xblock = xblock_class(self.runtime_mock, scope_ids=Mock()) + self.service_mock.get_settings_bucket = Mock(return_value=theme_config) + self.assertEqual(xblock.get_theme(), xblock_class.default_theme_config) + self.service_mock.get_settings_bucket.assert_called_once_with(xblock, default={}) + + @ddt.data(*itertools.product( + (DummyXBlockWithSettings, OtherXBlockWithSettings), + (123, [1, 2, 3], {'package': 'qwerty', 'locations': ['something_else.css']}), + )) + @ddt.unpack + def test_theme_correctly_returns_configured_theme(self, xblock_class, theme_config): + xblock = xblock_class(self.runtime_mock, scope_ids=Mock()) + self.service_mock.get_settings_bucket = Mock(return_value={xblock_class.theme_key: theme_config}) + self.assertEqual(xblock.get_theme(), theme_config) + + @ddt.data(DummyXBlockWithSettings, OtherXBlockWithSettings) + def test_theme_files_are_loaded_from_correct_package(self, xblock_class): + xblock = xblock_class(self.runtime_mock, scope_ids=Mock()) + fragment = MagicMock() + package_name = 'some_package' + theme_config = {xblock_class.theme_key: {'package': package_name, 'locations': ['lms.css']}} + self.service_mock.get_settings_bucket = Mock(return_value=theme_config) + with patch("xblock.utils.settings.ResourceLoader") as patched_resource_loader: + xblock.include_theme_files(fragment) + patched_resource_loader.assert_called_with(package_name) + + @ddt.data( + ('dummy_block', ['']), + ('dummy_block', ['public/themes/lms.css']), + ('other_block', ['public/themes/lms.css', 'public/themes/lms.part2.css']), + ('dummy_app.dummy_block', ['typography.css', 'icons.css']), + ) + @ddt.unpack + def test_theme_files_are_added_to_fragment(self, package_name, locations): + xblock = DummyXBlockWithSettings(self.runtime_mock, scope_ids=Mock()) + fragment = MagicMock() + theme_config = {DummyXBlockWithSettings.theme_key: {'package': package_name, 'locations': locations}} + self.service_mock.get_settings_bucket = Mock(return_value=theme_config) + with patch("xblock.utils.settings.ResourceLoader.load_unicode") as patched_load_unicode: + xblock.include_theme_files(fragment) + for location in locations: + patched_load_unicode.assert_any_call(location) + + self.assertEqual(patched_load_unicode.call_count, len(locations)) + + @ddt.data(None, {}, {'locations': ['red.css']}) + def test_invalid_default_theme_config(self, theme_config): + xblock = DummyXBlockWithSettings(self.runtime_mock, scope_ids=Mock()) + xblock.default_theme_config = theme_config + self.service_mock.get_settings_bucket = Mock(return_value={}) + fragment = MagicMock() + with patch("xblock.utils.settings.ResourceLoader.load_unicode") as patched_load_unicode: + xblock.include_theme_files(fragment) + patched_load_unicode.assert_not_called() diff --git a/xblock/utils/__init__.py b/xblock/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/xblock/utils/helpers.py b/xblock/utils/helpers.py new file mode 100644 index 000000000..941daa3b4 --- /dev/null +++ b/xblock/utils/helpers.py @@ -0,0 +1,25 @@ +""" +Useful helper methods +""" + + +def child_isinstance(block, child_id, block_class_or_mixin): + """ + Efficiently check if a child of an XBlock is an instance of the given class. + + Arguments: + block -- the parent (or ancestor) of the child block in question + child_id -- the usage key of the child block we are wondering about + block_class_or_mixin -- We return true if block's child indentified by child_id is an + instance of this. + + This method is equivalent to + + isinstance(block.runtime.get_block(child_id), block_class_or_mixin) + + but is far more efficient, as it avoids the need to instantiate the child. + """ + def_id = block.runtime.id_reader.get_definition_id(child_id) + type_name = block.runtime.id_reader.get_block_type(def_id) + child_class = block.runtime.load_block_type(type_name) + return issubclass(child_class, block_class_or_mixin) diff --git a/xblock/utils/public/studio_container.js b/xblock/utils/public/studio_container.js new file mode 100644 index 000000000..90d7cfaca --- /dev/null +++ b/xblock/utils/public/studio_container.js @@ -0,0 +1,63 @@ +function StudioContainerXBlockWithNestedXBlocksMixin(runtime, element) { + var $buttons = $(".add-xblock-component-button", element), + $addComponent = $('.add-xblock-component', element), + $element = $(element); + + function isSingleInstance($button) { + return $button.data('single-instance'); + } + + // We use delegated events here, i.e., not binding a click event listener + // directly to $buttons, because we want to make sure any other click event + // listeners of the button are called first before we disable the button. + // Ref: OSPR-1393 + $addComponent.on('click', '.add-xblock-component-button', function(ev) { + var $button = $(ev.currentTarget); + if ($button.is('.disabled')) { + ev.preventDefault(); + ev.stopPropagation(); + } else { + if (isSingleInstance($button)) { + $button.addClass('disabled'); + $button.attr('disabled', 'disabled'); + } + } + }); + + function updateButtons() { + var nestedBlockLocations = $.map($element.find(".studio-xblock-wrapper"), function(block_wrapper) { + return $(block_wrapper).data('locator'); + }); + + $buttons.each(function() { + var $this = $(this); + if (!isSingleInstance($this)) { + return; + } + var category = $this.data('category'); + var childExists = false; + + // FIXME: This is potentially buggy - if some XBlock's category is a substring of some other XBlock category + // it will exhibit wrong behavior. However, it's not possible to do anything about that unless studio runtime + // announces which block was deleted, not it's parent. + for (var i = 0; i < nestedBlockLocations.length; i++) { + if (nestedBlockLocations[i].indexOf(category) > -1) { + childExists = true; + break; + } + } + + if (childExists) { + $this.attr('disabled', 'disabled'); + $this.addClass('disabled') + } + else { + $this.removeAttr('disabled'); + $this.removeClass('disabled'); + } + }); + } + + updateButtons(); + runtime.listenTo('deleted-child', updateButtons); +} diff --git a/xblock/utils/public/studio_edit.js b/xblock/utils/public/studio_edit.js new file mode 100644 index 000000000..499319c45 --- /dev/null +++ b/xblock/utils/public/studio_edit.js @@ -0,0 +1,175 @@ +/* Javascript for StudioEditableXBlockMixin. */ +function StudioEditableXBlockMixin(runtime, element) { + "use strict"; + + var fields = []; + var tinyMceAvailable = (typeof $.fn.tinymce !== 'undefined'); // Studio includes a copy of tinyMCE and its jQuery plugin + var datepickerAvailable = (typeof $.fn.datepicker !== 'undefined'); // Studio includes datepicker jQuery plugin + + $(element).find('.field-data-control').each(function() { + var $field = $(this); + var $wrapper = $field.closest('li'); + var $resetButton = $wrapper.find('button.setting-clear'); + var type = $wrapper.data('cast'); + fields.push({ + name: $wrapper.data('field-name'), + isSet: function() { return $wrapper.hasClass('is-set'); }, + hasEditor: function() { return tinyMceAvailable && $field.tinymce(); }, + val: function() { + var val = $field.val(); + // Cast values to the appropriate type so that we send nice clean JSON over the wire: + if (type == 'boolean') + return (val == 'true' || val == '1'); + if (type == "integer") + return parseInt(val, 10); + if (type == "float") + return parseFloat(val); + if (type == "generic" || type == "list" || type == "set") { + val = val.trim(); + if (val === "") + val = null; + else + val = JSON.parse(val); // TODO: handle parse errors + } + return val; + }, + removeEditor: function() { + $field.tinymce().remove(); + } + }); + var fieldChanged = function() { + // Field value has been modified: + $wrapper.addClass('is-set'); + $resetButton.removeClass('inactive').addClass('active'); + }; + $field.bind("change input paste", fieldChanged); + $resetButton.click(function() { + $field.val($wrapper.attr('data-default')); // Use attr instead of data to force treating the default value as a string + $wrapper.removeClass('is-set'); + $resetButton.removeClass('active').addClass('inactive'); + }); + if (type == 'html' && tinyMceAvailable) { + tinyMCE.baseURL = baseUrl + "/js/vendor/tinymce/js/tinymce"; + $field.tinymce({ + theme: 'silver', + skin: 'studio-tmce5', + content_css: 'studio-tmce5', + height: '200px', + formats: { code: { inline: 'code' } }, + codemirror: { path: "" + baseUrl + "/js/vendor" }, + convert_urls: false, + plugins: "lists, link, codemirror", + menubar: false, + statusbar: false, + toolbar_items_size: 'small', + toolbar: "formatselect | styleselect | bold italic underline forecolor | bullist numlist outdent indent blockquote | link unlink | code", + resize: "both", + extended_valid_elements : 'i[class],span[class]', + setup : function(ed) { + ed.on('change', fieldChanged); + } + }); + } + + if (type == 'datepicker' && datepickerAvailable) { + $field.datepicker('destroy'); + $field.datepicker({dateFormat: "m/d/yy"}); + } + }); + + $(element).find('.wrapper-list-settings .list-set').each(function() { + var $optionList = $(this); + var $checkboxes = $(this).find('input'); + var $wrapper = $optionList.closest('li'); + var $resetButton = $wrapper.find('button.setting-clear'); + + fields.push({ + name: $wrapper.data('field-name'), + isSet: function() { return $wrapper.hasClass('is-set'); }, + hasEditor: function() { return false; }, + val: function() { + var val = []; + $checkboxes.each(function() { + if ($(this).is(':checked')) { + val.push(JSON.parse($(this).val())); + } + }); + return val; + } + }); + var fieldChanged = function() { + // Field value has been modified: + $wrapper.addClass('is-set'); + $resetButton.removeClass('inactive').addClass('active'); + }; + $checkboxes.bind("change input", fieldChanged); + + $resetButton.click(function() { + var defaults = JSON.parse($wrapper.attr('data-default')); + $checkboxes.each(function() { + var val = JSON.parse($(this).val()); + $(this).prop('checked', defaults.indexOf(val) > -1); + }); + $wrapper.removeClass('is-set'); + $resetButton.removeClass('active').addClass('inactive'); + }); + }); + + var studio_submit = function(data) { + var handlerUrl = runtime.handlerUrl(element, 'submit_studio_edits'); + runtime.notify('save', {state: 'start', message: gettext("Saving")}); + $.ajax({ + type: "POST", + url: handlerUrl, + data: JSON.stringify(data), + dataType: "json", + global: false, // Disable Studio's error handling that conflicts with studio's notify('save') and notify('cancel') :-/ + success: function(response) { runtime.notify('save', {state: 'end'}); } + }).fail(function(jqXHR) { + var message = gettext("This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online."); + if (jqXHR.responseText) { // Is there a more specific error message we can show? + try { + message = JSON.parse(jqXHR.responseText).error; + if (typeof message === "object" && message.messages) { + // e.g. {"error": {"messages": [{"text": "Unknown user 'bob'!", "type": "error"}, ...]}} etc. + message = $.map(message.messages, function(msg) { return msg.text; }).join(", "); + } + } catch (error) { message = jqXHR.responseText.substr(0, 300); } + } + runtime.notify('error', {title: gettext("Unable to update settings"), message: message}); + }); + }; + + $('.save-button', element).bind('click', function(e) { + e.preventDefault(); + var values = {}; + var notSet = []; // List of field names that should be set to default values + for (var i in fields) { + var field = fields[i]; + if (field.isSet()) { + values[field.name] = field.val(); + } else { + notSet.push(field.name); + } + // Remove TinyMCE instances to make sure jQuery does not try to access stale instances + // when loading editor for another block: + if (field.hasEditor()) { + field.removeEditor(); + } + } + studio_submit({values: values, defaults: notSet}); + }); + + $(element).find('.cancel-button').bind('click', function(e) { + // Remove TinyMCE instances to make sure jQuery does not try to access stale instances + // when loading editor for another block: + for (var i in fields) { + var field = fields[i]; + if (field.hasEditor()) { + field.removeEditor(); + } + } + e.preventDefault(); + runtime.notify('cancel', {}); + }); +} diff --git a/xblock/utils/publish_event.py b/xblock/utils/publish_event.py new file mode 100644 index 000000000..570b182ec --- /dev/null +++ b/xblock/utils/publish_event.py @@ -0,0 +1,38 @@ +""" +PublishEventMixin: A mixin for publishing events from an XBlock +""" + +from xblock.core import XBlock + + +class PublishEventMixin: + """ + A mixin for publishing events from an XBlock + + Requires the object to have a runtime.publish method. + """ + additional_publish_event_data = {} + + @XBlock.json_handler + def publish_event(self, data, suffix=''): # pylint: disable=unused-argument + """ + AJAX handler to allow client-side code to publish a server-side event + """ + try: + event_type = data.pop('event_type') + except KeyError: + return {'result': 'error', 'message': 'Missing event_type in JSON data'} + + return self.publish_event_from_dict(event_type, data) + + def publish_event_from_dict(self, event_type, data): + """ + Combine 'data' with self.additional_publish_event_data and publish an event + """ + for key, value in self.additional_publish_event_data.items(): + if key in data: + return {'result': 'error', 'message': f'Key should not be in publish_event data: {key}'} + data[key] = value + + self.runtime.publish(self, event_type, data) + return {'result': 'success'} diff --git a/xblock/utils/resources.py b/xblock/utils/resources.py new file mode 100644 index 000000000..1066ffd59 --- /dev/null +++ b/xblock/utils/resources.py @@ -0,0 +1,107 @@ +""" +Helper class (ResourceLoader) for loading resources used by an XBlock +""" + +import os +import sys +import warnings + +import pkg_resources +from django.template import Context, Template, Engine +from django.template.backends.django import get_installed_libraries +from mako.lookup import TemplateLookup as MakoTemplateLookup +from mako.template import Template as MakoTemplate + + +class ResourceLoader: + """Loads resources relative to the module named by the module_name parameter.""" + def __init__(self, module_name): + self.module_name = module_name + + def load_unicode(self, resource_path): + """ + Gets the content of a resource + """ + resource_content = pkg_resources.resource_string(self.module_name, resource_path) + return resource_content.decode('utf-8') + + def render_django_template(self, template_path, context=None, i18n_service=None): + """ + Evaluate a django template by resource path, applying the provided context. + """ + context = context or {} + context['_i18n_service'] = i18n_service + libraries = { + 'i18n': 'xblock.utils.templatetags.i18n', + } + + installed_libraries = get_installed_libraries() + installed_libraries.update(libraries) + engine = Engine(libraries=installed_libraries) + + template_str = self.load_unicode(template_path) + template = Template(template_str, engine=engine) + rendered = template.render(Context(context)) + + return rendered + + def render_mako_template(self, template_path, context=None): + """ + Evaluate a mako template by resource path, applying the provided context + Note: This function has been deprecated. Consider using Django templates or React UI instead of mako. + """ + warnings.warn( + 'ResourceLoader.render_mako_template has been deprecated. ' + 'Use Django templates or React UI instead of mako.', + DeprecationWarning, stacklevel=3, + ) + context = context or {} + template_str = self.load_unicode(template_path) + lookup = MakoTemplateLookup(directories=[pkg_resources.resource_filename(self.module_name, '')]) + template = MakoTemplate(template_str, lookup=lookup) + return template.render(**context) + + def render_template(self, template_path, context=None): + """ + This function has been deprecated. It calls render_django_template to support backwards compatibility. + """ + warnings.warn( + "ResourceLoader.render_template has been deprecated in favor of ResourceLoader.render_django_template" + ) + return self.render_django_template(template_path, context) + + def render_js_template(self, template_path, element_id, context=None, i18n_service=None): + """ + Render a js template. + """ + context = context or {} + return "".format( + element_id, + self.render_django_template(template_path, context, i18n_service) + ) + + def load_scenarios_from_path(self, relative_scenario_dir, include_identifier=False): + """ + Returns an array of (title, xmlcontent) from files contained in a specified directory, + formatted as expected for the return value of the workbench_scenarios() method. + + If `include_identifier` is True, returns an array of (identifier, title, xmlcontent). + """ + base_dir = os.path.dirname(os.path.realpath(sys.modules[self.module_name].__file__)) + scenario_dir = os.path.join(base_dir, relative_scenario_dir) + + scenarios = [] + if os.path.isdir(scenario_dir): + for template in sorted(os.listdir(scenario_dir)): + if not template.endswith('.xml'): + continue + identifier = template[:-4] + title = identifier.replace('_', ' ').title() + template_path = os.path.join(relative_scenario_dir, template) + scenario = str(self.render_django_template(template_path, {"url_name": identifier})) + if not include_identifier: + scenarios.append((title, scenario)) + else: + scenarios.append((identifier, title, scenario)) + + return scenarios diff --git a/xblock/utils/settings.py b/xblock/utils/settings.py new file mode 100644 index 000000000..31c3b9a8f --- /dev/null +++ b/xblock/utils/settings.py @@ -0,0 +1,88 @@ +""" +This module contains a mixins that allows third party XBlocks to access Settings Service in edX LMS. +""" + +from xblock.utils.resources import ResourceLoader + + +class XBlockWithSettingsMixin: + """ + This XBlock Mixin provides access to XBlock settings service + Descendant Xblock must add @XBlock.wants('settings') declaration + + Configuration: + block_settings_key: string - XBlock settings is essentially a dictionary-like object (key-value storage). + Each XBlock must provide a key to look its settings up in this storage. + Settings Service uses `block_settings_key` attribute to get the XBlock settings key + If the `block_settings_key` is not provided the XBlock class name will be used. + """ + # block_settings_key = "XBlockName" # (Optional) + + def get_xblock_settings(self, default=None): + """ + Gets XBlock-specific settings for current XBlock + + Returns default if settings service is not available. + + Parameters: + default - default value to be used in two cases: + * No settings service is available + * As a `default` parameter to `SettingsService.get_settings_bucket` + """ + settings_service = self.runtime.service(self, "settings") + if settings_service: + return settings_service.get_settings_bucket(self, default=default) + return default + + +class ThemableXBlockMixin: + """ + This XBlock Mixin provides configurable theme support via Settings Service. + This mixin implies XBlockWithSettingsMixin is already mixed in into Descendant XBlock + + Parameters: + default_theme_config: dict - default theme configuration in case no theme configuration is obtained from + Settings Service + theme_key: string - XBlock settings key to look theme up + block_settings_key: string - (implicit) + + Examples: + + Looks up red.css and small.css in `my_xblock` package: + default_theme_config = { + 'package': 'my_xblock', + 'locations': ['red.css', 'small.css'] + } + + Looks up public/themes/red.css in my_other_xblock.assets + default_theme_config = { + 'package': 'my_other_xblock.assets', + 'locations': ['public/themes/red.css'] + } + """ + default_theme_config = None + theme_key = "theme" + + def get_theme(self): + """ + Gets theme settings from settings service. Falls back to default (LMS) theme + if settings service is not available, xblock theme settings are not set or does + contain mentoring theme settings. + """ + xblock_settings = self.get_xblock_settings(default={}) + if xblock_settings and self.theme_key in xblock_settings: + return xblock_settings[self.theme_key] + return self.default_theme_config + + def include_theme_files(self, fragment): + """ + Gets theme configuration and renders theme css into fragment + """ + theme = self.get_theme() + if not theme or 'package' not in theme: + return + + theme_package, theme_files = theme.get('package', None), theme.get('locations', []) + resource_loader = ResourceLoader(theme_package) + for theme_file in theme_files: + fragment.add_css(resource_loader.load_unicode(theme_file)) diff --git a/xblock/utils/studio_editable.py b/xblock/utils/studio_editable.py new file mode 100644 index 000000000..b705854cb --- /dev/null +++ b/xblock/utils/studio_editable.py @@ -0,0 +1,511 @@ +""" +This module contains a mixin that allows third party XBlocks to be easily edited within edX +Studio just like the built-in modules. No configuration required, just add +StudioEditableXBlockMixin to your XBlock. +""" + +# Imports ########################################################### + + +import logging + +import simplejson as json +from web_fragments.fragment import Fragment + +from xblock.core import XBlock, XBlockMixin +from xblock.exceptions import JsonHandlerError, NoSuchViewError +from xblock.fields import Scope, JSONField, List, Integer, Float, Boolean, String, DateTime +from xblock.utils.resources import ResourceLoader +from xblock.validation import Validation + +# Globals ########################################################### + +log = logging.getLogger(__name__) +loader = ResourceLoader(__name__) + + +# Classes ########################################################### + + +class FutureFields: + """ + A helper class whose attribute values come from the specified dictionary or fallback object. + + This is only used by StudioEditableXBlockMixin and is not meant to be re-used anywhere else! + + This class wraps an XBlock and makes it appear that some of the block's field values have + been changed to new values or deleted (and reset to default values). It does so without + actually modifying the XBlock. The only reason we need this is because the XBlock validation + API is built around attribute access, but often we want to validate data that's stored in a + dictionary before making changes to an XBlock's attributes (since any changes made to the + XBlock may get persisted even if validation fails). + """ + + def __init__(self, new_fields_dict, newly_removed_fields, fallback_obj): + """ + Create an instance whose attributes come from new_fields_dict and fallback_obj. + + Arguments: + new_fields_dict -- A dictionary of values that will appear as attributes of this object + newly_removed_fields -- A list of field names for which we will not use fallback_obj + fallback_obj -- An XBlock to use as a provider for any attributes not in new_fields_dict + """ + self._new_fields_dict = new_fields_dict + self._blacklist = newly_removed_fields + self._fallback_obj = fallback_obj + + def __getattr__(self, name): + try: + return self._new_fields_dict[name] + except KeyError: + if name in self._blacklist: + # Pretend like this field is not actually set, since we're going to be resetting it to default + return self._fallback_obj.fields[name].default + return getattr(self._fallback_obj, name) + + +class StudioEditableXBlockMixin: + """ + An XBlock mixin to provide a configuration UI for an XBlock in Studio. + """ + editable_fields = () # Set this to a list of the names of fields to appear in the editor + + def studio_view(self, context): + """ + Render a form for editing this XBlock + """ + fragment = Fragment() + context = {'fields': []} + # Build a list of all the fields that can be edited: + for field_name in self.editable_fields: + field = self.fields[field_name] + assert field.scope in (Scope.content, Scope.settings), ( + "Only Scope.content or Scope.settings fields can be used with " + "StudioEditableXBlockMixin. Other scopes are for user-specific data and are " + "not generally created/configured by content authors in Studio." + ) + field_info = self._make_field_info(field_name, field) + if field_info is not None: + context["fields"].append(field_info) + fragment.content = loader.render_django_template('templates/studio_edit.html', context) + fragment.add_javascript(loader.load_unicode('public/studio_edit.js')) + fragment.initialize_js('StudioEditableXBlockMixin') + return fragment + + def _make_field_info(self, field_name, field): # pylint: disable=too-many-statements + """ + Create the information that the template needs to render a form field for this field. + """ + supported_field_types = ( + (Integer, 'integer'), + (Float, 'float'), + (Boolean, 'boolean'), + (String, 'string'), + (List, 'list'), + (DateTime, 'datepicker'), + (JSONField, 'generic'), # This is last so as a last resort we display a text field w/ the JSON string + ) + if self.service_declaration("i18n"): + ugettext = self.ugettext + else: + + def ugettext(text): + """ Dummy ugettext method that doesn't do anything """ + return text + + info = { + 'name': field_name, + # pylint: disable=translation-of-non-string + 'display_name': ugettext(field.display_name) if field.display_name else "", + 'is_set': field.is_set_on(self), + 'default': field.default, + 'value': field.read_from(self), + 'has_values': False, + # pylint: disable=translation-of-non-string + 'help': ugettext(field.help) if field.help else "", + 'allow_reset': field.runtime_options.get('resettable_editor', True), + 'list_values': None, # Only available for List fields + 'has_list_values': False, # True if list_values_provider exists, even if it returned no available options + } + for type_class, type_name in supported_field_types: + if isinstance(field, type_class): + info['type'] = type_name + # If String fields are declared like String(..., multiline_editor=True), then call them "text" type: + editor_type = field.runtime_options.get('multiline_editor') + if type_class is String and editor_type: + if editor_type == "html": + info['type'] = 'html' + else: + info['type'] = 'text' + if type_class is List and field.runtime_options.get('list_style') == "set": + # List represents unordered, unique items, optionally drawn from list_values_provider() + info['type'] = 'set' + elif type_class is List: + info['type'] = "generic" # disable other types of list for now until properly implemented + break + if "type" not in info: + raise NotImplementedError("StudioEditableXBlockMixin currently only supports fields derived from JSONField") + if info["type"] in ("list", "set"): + info["value"] = [json.dumps(val) for val in info["value"]] + info["default"] = json.dumps(info["default"]) + elif info["type"] == "generic": + # Convert value to JSON string if we're treating this field generically: + info["value"] = json.dumps(info["value"]) + info["default"] = json.dumps(info["default"]) + elif info["type"] == "datepicker": + if info["value"]: + info["value"] = info["value"].strftime("%m/%d/%Y") + if info["default"]: + info["default"] = info["default"].strftime("%m/%d/%Y") + + if 'values_provider' in field.runtime_options: + values = field.runtime_options["values_provider"](self) + else: + values = field.values + if values and not isinstance(field, Boolean): + # This field has only a limited number of pre-defined options. + # Protip: when defining the field, values= can be a callable. + if isinstance(field.values, dict) and isinstance(field, (Float, Integer)): + # e.g. {"min": 0 , "max": 10, "step": .1} + for option in field.values: + if option in ("min", "max", "step"): + info[option] = field.values.get(option) + else: + raise KeyError("Invalid 'values' key. Should be like values={'min': 1, 'max': 10, 'step': 1}") + elif isinstance(values[0], dict) and "display_name" in values[0] and "value" in values[0]: + # e.g. [ {"display_name": "Always", "value": "always"}, ... ] + for value in values: + assert "display_name" in value and "value" in value + info['values'] = values + else: + # e.g. [1, 2, 3] - we need to convert it to the [{"display_name": x, "value": x}] format + info['values'] = [{"display_name": str(val), "value": val} for val in values] + info['has_values'] = 'values' in info + if info["type"] in ("list", "set") and field.runtime_options.get('list_values_provider'): + list_values = field.runtime_options['list_values_provider'](self) + # list_values must be a list of values or {"display_name": x, "value": y} objects + # Furthermore, we need to convert all values to JSON since they could be of any type + if list_values and isinstance(list_values[0], dict) and "display_name" in list_values[0]: + # e.g. [ {"display_name": "Always", "value": "always"}, ... ] + for entry in list_values: + assert "display_name" in entry and "value" in entry + entry["value"] = json.dumps(entry["value"]) + else: + # e.g. [1, 2, 3] - we need to convert it to the [{"display_name": x, "value": x}] format + list_values = [json.dumps(val) for val in list_values] + list_values = [{"display_name": str(val), "value": val} for val in list_values] + info['list_values'] = list_values + info['has_list_values'] = True + return info + + @XBlock.json_handler + def submit_studio_edits(self, data, suffix=''): # pylint: disable=unused-argument + """ + AJAX handler for studio_view() Save button + """ + values = {} # dict of new field values we are updating + to_reset = [] # list of field names to delete from this XBlock + for field_name in self.editable_fields: + field = self.fields[field_name] + if field_name in data['values']: + if isinstance(field, JSONField): + values[field_name] = field.from_json(data['values'][field_name]) + else: + raise JsonHandlerError(400, f"Unsupported field type: {field_name}") + elif field_name in data['defaults'] and field.is_set_on(self): + to_reset.append(field_name) + self.clean_studio_edits(values) + validation = Validation(self.scope_ids.usage_id) + # We cannot set the fields on self yet, because even if validation fails, studio is going to save any changes we + # make. So we create a "fake" object that has all the field values we are about to set. + preview_data = FutureFields( + new_fields_dict=values, + newly_removed_fields=to_reset, + fallback_obj=self + ) + self.validate_field_data(validation, preview_data) + if validation: + for field_name, value in values.items(): + setattr(self, field_name, value) + for field_name in to_reset: + self.fields[field_name].delete_from(self) + return {'result': 'success'} + else: + raise JsonHandlerError(400, validation.to_json()) + + def clean_studio_edits(self, data): + """ + Given POST data dictionary 'data', clean the data before validating it. + e.g. fix capitalization, remove trailing spaces, etc. + """ + # Example: + # if "name" in data: + # data["name"] = data["name"].strip() + + def validate_field_data(self, validation, data): + """ + Validate this block's field data. Instead of checking fields like self.name, check the + fields set on data, e.g. data.name. This allows the same validation method to be re-used + for the studio editor. Any errors found should be added to "validation". + + This method should not return any value or raise any exceptions. + All of this XBlock's fields should be found in "data", even if they aren't being changed + or aren't even set (i.e. are defaults). + """ + # Example: + # if data.count <=0: + # validation.add(ValidationMessage(ValidationMessage.ERROR, u"Invalid count")) + + def validate(self): + """ + Validates the state of this XBlock. + + Subclasses should override validate_field_data() to validate fields and override this + only for validation not related to this block's field values. + """ + validation = super().validate() + self.validate_field_data(validation, self) + return validation + + +@XBlock.needs('mako') +class StudioContainerXBlockMixin(XBlockMixin): + """ + An XBlock mixin to provide convenient use of an XBlock in Studio + that wants to allow the user to assign children to it. + """ + has_author_view = True # Without this flag, studio will use student_view on newly-added blocks :/ + + def render_children(self, context, fragment, can_reorder=True, can_add=False): + """ + Renders the children of the module with HTML appropriate for Studio. If can_reorder is + True, then the children will be rendered to support drag and drop. + """ + contents = [] + + child_context = {'reorderable_items': set()} + if context: + child_context.update(context) + + for child_id in self.children: + child = self.runtime.get_block(child_id) + if can_reorder: + child_context['reorderable_items'].add(child.scope_ids.usage_id) + view_to_render = 'author_view' if hasattr(child, 'author_view') else 'student_view' + rendered_child = child.render(view_to_render, child_context) + fragment.add_fragment_resources(rendered_child) + + contents.append({ + 'id': str(child.scope_ids.usage_id), + 'content': rendered_child.content + }) + + mako_service = self.runtime.service(self, 'mako') + # 'lms.' namespace_prefix is required for rendering in studio + mako_service.namespace_prefix = 'lms.' + fragment.add_content(mako_service.render_template("studio_render_children_view.html", { + 'items': contents, + 'xblock_context': context, + 'can_add': can_add, + 'can_reorder': can_reorder, + })) + + def author_view(self, context): + """ + Display a the studio editor when the user has clicked "View" to see the container view, + otherwise just show the normal 'author_preview_view' or 'student_view' preview. + """ + root_xblock = context.get('root_xblock') + + if root_xblock and root_xblock.location == self.location: + # User has clicked the "View" link. Show an editable preview of this block's children + return self.author_edit_view(context) + return self.author_preview_view(context) + + def author_edit_view(self, context): + """ + Child blocks can override this to control the view shown to authors in Studio when + editing this block's children. + """ + fragment = Fragment() + self.render_children(context, fragment, can_reorder=True, can_add=False) + return fragment + + def author_preview_view(self, context): + """ + Child blocks can override this to add a custom preview shown to authors in Studio when + not editing this block's children. + """ + return self.student_view(context) + + +class NestedXBlockSpec: + """ + Class that allows detailed specification of allowed nested XBlocks. For use with + StudioContainerWithNestedXBlocksMixin.allowed_nested_blocks + """ + + def __init__( + self, block, single_instance=False, disabled=False, disabled_reason=None, boilerplate=None, + category=None, label=None, + ): + self._block = block + self._single_instance = single_instance + self._disabled = disabled + self._disabled_reason = disabled_reason + self._boilerplate = boilerplate + # Some blocks may not be nesting-aware, but can be nested anyway with a bit of help. + # For example, if you wanted to include an XBlock from a different project that didn't + # yet use XBlock utils, you could specify the category and studio label here. + self._category = category + self._label = label + + @property + def category(self): + """ Block category - used as a computer-readable name of an XBlock """ + return self._category or self._block.CATEGORY + + @property + def label(self): + """ Block label - used as human-readable name of an XBlock """ + return self._label or self._block.STUDIO_LABEL + + @property + def single_instance(self): + """ If True, only allow single nested instance of Xblock """ + return self._single_instance + + @property + def disabled(self): + """ + If True, renders add buttons disabled - only use when XBlock can't be added at all (i.e. not available). + To allow single instance of XBlock use single_instance property + """ + return self._disabled + + @property + def disabled_reason(self): + """ + If block is disabled this property is used as add button title, giving some hint about why it is disabled + """ + return self._disabled_reason + + @property + def boilerplate(self): + """ Boilerplate - if not None and not empty used as data-boilerplate attribute value """ + return self._boilerplate + + +class XBlockWithPreviewMixin: + """ + An XBlock mixin providing simple preview view. It is to be used with StudioContainerWithNestedXBlocksMixin to + avoid adding studio wrappers (title, edit button, etc.) to a block when it is rendered as child in parent's + author_preview_view + """ + + def preview_view(self, context): + """ + Preview view - used by StudioContainerWithNestedXBlocksMixin to render nested xblocks in preview context. + Default implementation uses author_view if available, otherwise falls back to student_view + Child classes can override this method to control their presentation in preview context + """ + view_to_render = 'author_view' if hasattr(self, 'author_view') else 'student_view' + renderer = getattr(self, view_to_render) + return renderer(context) + + +class StudioContainerWithNestedXBlocksMixin(StudioContainerXBlockMixin): + """ + An XBlock mixin providing interface for specifying allowed nested blocks and adding/previewing them in Studio. + """ + has_children = True + CHILD_PREVIEW_TEMPLATE = "templates/default_preview_view.html" + + @property + def loader(self): + """ + Loader for loading and rendering assets stored in child XBlock package + """ + return loader + + @property + def allowed_nested_blocks(self): + """ + Returns a list of allowed nested XBlocks. Each item can be either + * An XBlock class + * A NestedXBlockSpec + + If XBlock class is used it is assumed that this XBlock is enabled and allows multiple instances. + NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple + instances + """ + return [] + + def get_nested_blocks_spec(self): + """ + Converts allowed_nested_blocks items to NestedXBlockSpec to provide common interface + """ + return [ + block_spec if isinstance(block_spec, NestedXBlockSpec) else NestedXBlockSpec(block_spec) + for block_spec in self.allowed_nested_blocks + ] + + def author_edit_view(self, context): + """ + View for adding/editing nested blocks + """ + fragment = Fragment() + + if 'wrap_children' in context: + fragment.add_content(context['wrap_children']['head']) + + self.render_children(context, fragment, can_reorder=True, can_add=False) + + if 'wrap_children' in context: + fragment.add_content(context['wrap_children']['tail']) + fragment.add_content( + loader.render_django_template( + 'templates/add_buttons.html', + {'child_blocks': self.get_nested_blocks_spec()} + ) + ) + fragment.add_javascript(loader.load_unicode('public/studio_container.js')) + fragment.initialize_js('StudioContainerXBlockWithNestedXBlocksMixin') + return fragment + + def author_preview_view(self, context): + """ + View for previewing contents in studio. + """ + children_contents = [] + + fragment = Fragment() + for child_id in self.children: + child = self.runtime.get_block(child_id) + child_fragment = self._render_child_fragment(child, context, 'preview_view') + fragment.add_fragment_resources(child_fragment) + children_contents.append(child_fragment.content) + + render_context = { + 'block': self, + 'children_contents': children_contents + } + render_context.update(context) + fragment.add_content(self.loader.render_django_template(self.CHILD_PREVIEW_TEMPLATE, render_context)) + return fragment + + def _render_child_fragment(self, child, context, view='student_view'): + """ + Helper method to overcome html block rendering quirks + """ + try: + child_fragment = child.render(view, context) + except NoSuchViewError: + if child.scope_ids.block_type == 'html' and getattr(self.runtime, 'is_author_mode', False): + # html block doesn't support preview_view, and if we use student_view Studio will wrap + # it in HTML that we don't want in the preview. So just render its HTML directly: + child_fragment = Fragment(child.data) + else: + child_fragment = child.render('student_view', context) + + return child_fragment diff --git a/xblock/utils/templates/add_buttons.html b/xblock/utils/templates/add_buttons.html new file mode 100644 index 000000000..f9b133210 --- /dev/null +++ b/xblock/utils/templates/add_buttons.html @@ -0,0 +1,22 @@ +{% load i18n %} + +
+
+
{% trans "Add New Component" %}
+ +
+
diff --git a/xblock/utils/templates/default_preview_view.html b/xblock/utils/templates/default_preview_view.html new file mode 100644 index 000000000..d18c90359 --- /dev/null +++ b/xblock/utils/templates/default_preview_view.html @@ -0,0 +1,3 @@ +
+ {% for child_content in children_contents %} {{ child_content|safe }} {% endfor %} +
\ No newline at end of file diff --git a/xblock/utils/templates/studio_edit.html b/xblock/utils/templates/studio_edit.html new file mode 100644 index 000000000..d75818218 --- /dev/null +++ b/xblock/utils/templates/studio_edit.html @@ -0,0 +1,113 @@ +{% load i18n %} +
+
+
    + {% for field in fields %} + + {% endfor %} +
+
+ +
diff --git a/xblock/utils/templatetags/__init__.py b/xblock/utils/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/xblock/utils/templatetags/i18n.py b/xblock/utils/templatetags/i18n.py new file mode 100644 index 000000000..0b8634d08 --- /dev/null +++ b/xblock/utils/templatetags/i18n.py @@ -0,0 +1,73 @@ +""" +Template tags for handling i18n translations for xblocks + +Based on: https://github.com/eduNEXT/django-xblock-i18n +""" + +from contextlib import contextmanager + +from django.template import Library, Node +from django.templatetags import i18n +from django.utils.translation import get_language, trans_real + +register = Library() + + +class ProxyTransNode(Node): + """ + This node is a proxy of a django TranslateNode. + """ + def __init__(self, do_translate_node): + """ + Initialize the ProxyTransNode + """ + self.do_translate = do_translate_node + self._translations = {} + + @contextmanager + def merge_translation(self, context): + """ + Context wrapper which modifies the given language's translation catalog using the i18n service, if found. + """ + language = get_language() + i18n_service = context.get('_i18n_service', None) + if i18n_service: + # Cache the original translation object to reduce overhead + if language not in self._translations: + self._translations[language] = trans_real.DjangoTranslation(language) + + translation = trans_real.translation(language) + translation.merge(i18n_service) + + yield + + # Revert to original translation object + if language in self._translations: + trans_real._translations[language] = self._translations[language] # pylint: disable=protected-access + # Re-activate the current language to reset translation caches + trans_real.activate(language) + + def render(self, context): + """ + Renders the translated text using the XBlock i18n service, if available. + """ + with self.merge_translation(context): + django_translated = self.do_translate.render(context) + + return django_translated + + +@register.tag('trans') +def xblock_translate(parser, token): + """ + Proxy implementation of the i18n `trans` tag. + """ + return ProxyTransNode(i18n.do_translate(parser, token)) + + +@register.tag('blocktrans') +def xblock_translate_block(parser, token): + """ + Proxy implementation of the i18n `blocktrans` tag. + """ + return ProxyTransNode(i18n.do_block_translate(parser, token)) From 54f88d36bf5db0475d1e4544405125a51dd5c3ed Mon Sep 17 00:00:00 2001 From: farhan Date: Mon, 25 Sep 2023 20:10:56 +0500 Subject: [PATCH 2/3] docs: Move xblock utils repo docs into this repo --- docs/index.rst | 1 + docs/xblock-utils/Images/Screenshot_1.png | Bin 0 -> 48071 bytes docs/xblock-utils/Images/Screenshot_2.png | Bin 0 -> 85681 bytes docs/xblock-utils/index.rst | 179 ++++++++++++++++++ .../settings-and-theme-support.rst | 157 +++++++++++++++ 5 files changed, 337 insertions(+) create mode 100644 docs/xblock-utils/Images/Screenshot_1.png create mode 100644 docs/xblock-utils/Images/Screenshot_2.png create mode 100644 docs/xblock-utils/index.rst create mode 100644 docs/xblock-utils/settings-and-theme-support.rst 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 0000000000000000000000000000000000000000..663cb001536d38da9c2b10d7eece5aecb2905ffb GIT binary patch literal 48071 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 Dwodzo literal 0 HcmV?d00001 diff --git a/docs/xblock-utils/Images/Screenshot_2.png b/docs/xblock-utils/Images/Screenshot_2.png new file mode 100644 index 0000000000000000000000000000000000000000..ba2cbf46b82022db836babc9ce362d47bba84649 GIT binary patch literal 85681 zcmeFZ2UL?y_bkS+q!gOtz% z3QC920|XL!34{P41PDoP#P|LF-#Op9cinUDJ!h@E)_qnMYgY2i%$}J&d+*tJ$)7yRv{LagMCL2GrxI3!d+xxdCBtN-W@}K>y-(rAGc!=F@V9f zBelGDElDXZscZc~zqMnx(gRV6v==MB{QcJh=U;qSQnQ)%g)aM!Px$ili@f^&;fI2I zmyz)8apH&Sqt}FAsGSmi!TR9nrMD~(7Ml)Vdis5JmAI1Yf+pmTDLR8LDG-U~CH5mm zb-qIe2tU6-|8#IVpMU^2Q=;FGCL(u}=TDJEnER;EUw_KktWzR?N-ryfp@;qyW6r*X z{rzhH>$Q`Ainq;A01x~rYO*(J{QbN0o$aH4io#O=_ZxlIG&Pgc!l&ft*-IMpXV~W# z!V_09w(00FWxnirw_Ff^d+osEE2E=zvku?O7Vq|ZR;u%X!wx2}E+x*7nb%xf0fhd5 zdr#RqQ38E+)uIM{z?ch~3&X6kGF3k*bg^Hii$$~cj-6wqDro1pp^8cTN_-%7s~t<@ zK9*6no=Gb6oAFihwjHl>6Sq>XQByeYw2q|bT>=NzMuqIoLAxJZooKY#BC1}pM-=y~ z*aZm*RxNLPpjh*Z0TPa=AyW$NR|y+Z5noFj>edCz z32ENfK5CG_OIExHteuHX_l56~Qn3ZydJA)k_A5O)BDb z)b8A+#z!yX-0K_ldPos-@)3kJR28dkz@bpI8%nQ66JLteO@H9(7Y{q5%#3ya* z_9#!0?e-2ciFmlR{~N3Be(+4?y5q8P`ul4>7rST^_DAdrOsod($n|!1j7nO;KUW7| znT0U2P67<&f^9cWDTn1F>Bz$!3GeBc6uj#NI@z((;38v0&gxPS?f0&{J)iu1`=$e> z;(Buu1lp?Vz)E_Y)S{ zhYfTVv%_E_oYctluugGl>K3TJ(Rw(@{EBu}*la|7fUHJcDN*TL({`l5Ux1L>_o{Ny zKiG_qNTA=d#V1HBKp^HBxEcfUrlmx2EPtCR6&?mF5b%{zguE9g@qj*%Vo?SdGg>Ah z&CRSPtKVlp^@;|w+t7ZsvdgEo3Q%JcFSKh5d_N%-n&;=>Ltp~=VfR-Q!Ckry=Ng=z zf&Y;x6QE@5pCT3lS6IcC>jTHUOpm*pKLZif0wIYK+24Vow?=D%6H7rqrfds~+GW*T z8D)2rKITSyduMA*eLhdy78%t?UkWpkxlvhiH>LD())~pRcmQ0unlO8~jwikfP^3`Q z3;&g@sZ!N$Y(gz`hXzf4`RS5KO@v|GL0cmo$80P_rd*xxR~Hvr-KnJ17m|8c*b+{; zVBT)>-|0?WbEGD+9&0Z}8Ocmt_S$gR%&fyFqdLqkt0)#c85nQ8&-%-&4-xE=w}a>6txVlOAC`pjp5Ar^s4$xPu!4!wv}t4YF8pYL45RFjn1%#6)!Madkgex z>HRvFtX72d&pqS=QRqtXRWDl0TMT{Rz*V9E#flMbse#26o;N%1;poE|{LFH>2WY8M z(}Ptv)`>oOTp^??t_=~hUGs2uR}OR4b+QWBqHWrMtAoN@6^`iil0=Rk(U%>}mK)L0+7}9K?>$@HdpvAM`XK}3}G zJ8?};L0MjGBWq3B)a{%bp|BkPnt;EAmkx~l?AT}Y_h*Usck5=WxS@EOSA!+ar zGup*oaA*WF*cei6_5-(# zxpTAOwZ_U}Mz?2HckHYfTFvO-xEmThYQ9@wNS<$frS?bs{jb<6czbZ*5xM3L8=iDd z)A_~mnww?8%ULTKwZmsS%A}6W8QNwU5@j)VbNNBGHDuUHyyeO*Bamg`JmMMR6|aKh zTv1{{kM)xCh)|8OUr4Zp$HRKu39E`V4FRGKdGTEv@iq5`S#t{Hcu_)#L{(@>u_)Et zU$tMs800~&5qFc2QN7~G*M-aC_L=wOTecc;uKx<(F>O@r3(6X&+FMr@t6Ow)2G1uw z$D~WkB394S(o#xIAlIJaMdmMm z*x}-uBfi1Wv5mefyC5FBjM@!5%Aoir2~2CR?ndCG+s_+JqnY6q3yWRZgSW6Oeaeod z$oDdO(E(*2Qm)5EoK#*p!)+q+_{ZW?+`}Jhz~Cg*&oPHbn@*NuyS*Y%mX&sZ}$#o?HeBzNS`qXtF7 zE;~wMM~ z9C2C6C0#i{3{X7ScOK=fR_t9>i{;RmMie)2^;Pgh{j0 zyfy9$&+eMk1?2YM((OU?3V5hVXu>G`UxI@+*IGHsiqgBlTxx05bH!_(KaNx6___%- zLg?OV>kz|b5!~FLRj;So2d*|9dD#h}SwO_CmdL4Gdj~{s+sa7-- zsP47Do^H7mliD}_MDkqXIGLx!>VwMHpCziaz~4(6^FWjvnEbRu+_@$ITkTo#`eu0| z3cPgQN!BBw_;DA18@@)B&*Mb^fToE=d{TupgQIt2%#8iULP_NWn9+#%Yn6^K-MG7TS*~q+V+S z!>HR`kDK^EM^j9A7o@I1D;LBnA)_n@%pLwz^tSu4_1kmb6=vj6p*?p6R)XZFDOT{A1d30>3*5 z(RV#^=J5ak~~p+A-7 zlU3=N!56*4(TMAl33d%+GAeoiz-oxo%fkTWhIftk}+3C)W0L4ObXtpU_cm z5=pUO)>Px&EYeVAbUR-xdV=t;;~QQ$-3DO}a8Q z^B7qemd5bkoV??Sdu&0dT)mw$u{s}Qe59w^JOy+(w@`s6gwpt+2fdyfu7{&K#(eQuQDgi@7jc5@s-1nsT5=MgY~pKM%w=)?f&f+*XP<@1qcQQK3t^}c-v zB`*hUI|adqTJoseBY=pf);3>ZVnPC67EkFA z{EmBe<9txdXXgjordK}LCJDr|;{$>6h}cT+b2X6pJ4C4(LI|QZLEL1s(nTic@3#trnBeT;l^Mh?&Tf+9B&(R@3ayErRo^tAmxSQVIRi21XkhQ zE!d#o-D38!P>Q_7nop=zD~N}*YkA6N1i>3=+Pu&`H%yXNRk7bX9*wT-p{wVD33x#N zslJAxl_Bm;OC7D=hH9HaE}KTY3Z4VyPU6Uo5~7>N-6@XWUA3+O4+VUDd5Ws|6E&tt z^nOa68*w42xY2^DDNjFlLGOL%fd?Amh}%;$c@=I}Pu?bA5`trX#*DS8TKfm7X*-Ij zRlNZR6h4R^3@X5O?gD?Q=;9T%0OB*W_^{c2a$)~zAI)l#wa*^*DY{fuWYn9Ru^Yia zrtOh=>a0EhzWxi*#Bm4}r`R%{*9l2uP(w0x>OF%XcPJa*T1An;6yOu%8_%% zu;QY4$lUlZj~f3{vjU1ooGoGgQ8soIUi@+(HNXfb6BgiIKoN}uP>Uec-AgBtWqz?c zyL{-`Q|nt3^~dC!SzXT|k~zKPuO#@QzPMix`>XI6kPi%Mi=UU3Ya_ApV?X~C zP&R#)>=Mxe$9{KTyvx%ij7p8nF_N|+$cElbY0f2C0^0UkOBUUqUzh#S+7F2D?;E;I zkaEoShv?0k9~%o5bo&$s$s>KWitq?x$H9w3%l`r6P3yQ~bE z8~bGtJNh9lX=nyfjP+1h%PQWuv&1m=VegAyfsWo*~{EMagzqNvKeW zTvepi=PHSzC4|Wn{kuov0=3inF+M{_2c`qn%NE4xWF7`YWoKE&x9$?;QMhF=*0q@` zsktl^y-D~@w0xhj>z~R}I$g^6I>t7`w&}=b4No{A^87mJ`$zj69>N?_9+Q?H5}LYJ znYPw9QMcBa?qzF5{CPo{G-?jM3HF4Ic8dCMnHjt}`5C?uk}@w@qII#{TBW3Q%+KdE ze)X6Ecq%1b=Zxd-klWpDv|A0pJ1;!Y_a9po%kQhM(#=|ELMDe$vfefTmo zf87GuRs}u{CL6nNLFw-S_w~OCcVZiYB;UK}`hn^iD=x)KanFqrjkFxWgBesyb6gTA#8G-NC#Y->iEbHJ|3Nn4K5yG>wR0`^t5%>G*xl zI5b9l(DUx_y}R!BL(Un0%UxWS0ApI71ql_KA!(g6yh1KqHevx=bhFA!;8htD|7g%1 zI(BU0{xc zIF;oZiWZ?+5Y4gSqOkB~A_D>j;&FkP|iQE_wdF(y79&yEvugfa+b}-=KfeQ+8?1^NR-;XZvH*$J;r1_G)SuaLf6p zUAIQtA2{HKYOAt}Qo4Lk$)A^;Dw+um6lCqw=Vrk%`Y|hs&xMj{T?DUyak8(OsRJ1D_ zync`Bq3-wVs4g>}9X@lnt>Z2cqTKhV7Wj9qq!>$Q*Myc9b!R8% zIEjN*2ZxiaS2-*bhS%un#M&tf1*_jgpT1UBUr)C?Pt>swn?C9HD%gK_n|=0wh7yga z2!YM0B{Bis18~b;=icF*nH4joWj%>pi`s+3+9m%^I&ws7iTSmK9+YZ8OD(y#1}P!j z->i7x*-f{cd_1g#NnTpNY0vh@a;xbjN>_8JlukM=EW4=E2pl$8RQIMVI4A-7Ew`H@ z_*-0-mrJ7VQ?}G$u1hMA_zKYzz-X?aht|Hn{(&V$sa9ShPiVq3w5T#yoO2ELH%Z># z6krCjh6V@mN;3IAMby!WqOH}N)dn54#F6FfDgn%Fd68Ryqn7RWvM2e~z}J<1HQqtn z$`greKFRMIk_lzKbivsXTv$K^SBNU)@ss+%lVLol(-D&4LuSH zE&1UpdE?TX3(G{4ND{JOC&4l8MdeJ|t5?~te;Qf}-NAfoYPH+CoFD^<=Ko=9++&`< zB3No8wBnL5%wP3uai`iz5b%fK)_i^PmWNP6uEPDrO#Vm4)Rl7&%vh2V*4A~@GW!gc zpM##Mc^GZyfq`(>jZl-V4R#g#zFd`@jVGBCB5u~J=1QZ^p>k*)csV4@es1>M5A6YW z`FjWxCVS{1W5L`UJ^UqwgyS83Fx3xIKH6UX|;-*=|l@)wOt7KAxX ze5*lL##vY*ZB1%Zp}=sMI7$DP<8yE#u_Yk9DYWI$=Jo zWnoAIsLS~j`0Z9bP9lvEO+mLinkkj#kQ4g0*iFuAc@tLKf~2l*AqxHPR|5*?hMiK} zXxBPD_=k>X2vvWo^u58^5jz)QXYZUidKh00w7g2~)S6XfFA6lfXrXH*o1oDMd{$Lu zQQ`h#)$$CTBtBZ>S%ZY?-6^$vA&`i_UPFu9+1lWS^xQipq2{&XJ!I=2o?6dKsIv`) znMqW2e0|RU3DXrjbRX+p@N~UU)$&R3e1`xcj?H@iM8ZE6;3Ii&@e{|8@sz><#`9wT zDc~?Xd6^ygtdzM*`E}CbFOP-XmA!GpF6?SmO4a?lA1{}Y43WRtpU?>h$CAY8QaxV$ zO{u!X!sm|hABTn?8o9e}m#eh|+#`z%ULBzFNg8MtO;bxeeTuarJ5kKXpc5|y0xjN_3m9A zmYnFqRZ`7|`0{S@`K~)VP3Icij$;T+e$~H(ZHdpEFXu*Cu{}3nzJVEMa@?nchT}=3 z?B6sIwQ$VEj&7N$$CkVB)&50?DsKy2aCDp3hT_v4woV$;## zF2P_IQ)nYIXngiIG(e>wyRy<8U#HPO675iz&*GtDw^-^!`$RPrk&==xqn##=3laoj zUV%SBpeiQ}lQJ6dN7av$U$y3hzALoO#0OsXA?urzVNHRm-%Or2dRjJ0-{Sw)rL?6M zyX<@CPH8DlL*x~_9ETsXxzbsA{-Z%uls?imsT4YwA5M|{eayvlSq>axqR>Z{*_147 z^y%1*HBTnpbM!Tk^XT(0dQ-8yqF^)BX8G%Us|m^qC&3t!UC%K3BBnX_hUPO(hkt88iYK9gknI5+937W;OR|-{MkPdtM z@f8U|ls=@SxL#wVOBF^5OKpz^(0kn)N_cXqgsGJ&rE7u%&04szj8zFO`{FZ zFkf(^%c0U!^S6VJ^pR4Xc{qLdoVuGcFOE+lI|; zB(FgqCvD*w&P&CC`WNblwtl=8O3DN0K7j?(XVUG@(LV`?@ZbB6aZ zH?ID9pn7QYn3~S%(`N*0mj1Hnbp>CuBac}4eb(2BPvmbd&^0y*V^~P>L{hd7@>3e_ zu@~=^AlenEN^eev80n=iIkHA;<@glgj?%GAicb~I(OK`_Yh!=>Rb`U~V37|taQu!( znx0y4;zJ`yQXk1{AP3^L@aWaLxmC(evb+`o+mjIf7PcFtT`%yaxz!G+`e$(J7fZ|l zdVWT!WhKpc7au7F)~*)@ImNH$2+P-%^9=IuSR_h zduH`Z{8Q)OfX2up%KWtVY+L1fhb4UNl?7D#NB24jkvn?Xi4J$CvUc`>t^%M*J0S5f zSr*e6oNseBAt9+m;F{H!wT67k5ota3ATlL@D^FS|xI%81K`O_`U! zE4lUD9L`~>=-^@;Y%-N4Hx(HiOiO*BmAb%UE+c|6g2aqkl=|bmE!+Fs z)ZX!7Q7Ne?eWNmyj@qG^&Ckoj{J=_{g2w^*=Us{M{3a>c{xvlK`4#i-T6kp%*rb4_ z=%`wC_xUoPsXglO@!u19TeMeA{*pSVHC*%Q_K1Yag07oWE_gVNySmyeGn)6MRD5}Z zBTyMYU$rWXli&JgB`5g@Tn(?FzH)7J_x@sYD?@y4XS-YM$XW2T+$Ge_4^FcEm;<&vT#S8D$&T@EhjM$<3H2KeSE z_eAk0M!4`VCq>?&^3Q(hi|C&q>2sou)S`!q+FTBtCm}vpa_y$hr}YFiR^1P=TyoPO z)(oh2Lc5_~v*e4E7kA&`u+~hl=4aXBnpl`KsYZ<1V8YKpOjRToBCG^+3$^<`0M1k@ zxkr_a%riVoYW{%< z$F|dyRoeS_n|$F|$lkJUDED)-@_QCv$Y(1!j0<99G@^0u6hwU5MsPo_P+bLJ^j$-K z_K+s`^BYJ()IFx5c@2m86N=ff>d>mNE>glGq?=Tc-=)7W5t9lEe#QnrIvGb1qn9t`k}3$_}lzM)y| z5$sH-O@roIMocznZ<_yF)=B>@z30?SmK|VuE$$%C3cBSkI@M^ybpTlD(t1|$1A z4Z0=MoQ}pXiS|eD_s0_6uZH-j3^i%kD}H$B{%RP5YH0Q)gXh?(gDwN@nJ>v!M5eg{ zjrR{XZdfQ`{A4BrPiIxl+&cl(>8~lC_dba9veQbFdtp|Ze#c4L=AsxliwuU!ULO0q z`S#z<>)o;a^=XB`7(0GEiluD3GT2I}m}e$aE}djJs!@JAdL_l%&>Q z%>Dj)?K^7ari3aN|cRRse}KJij7tY6=2oK#h9xqTl<-JM)moy9DYhIKpekD__;{9 zdGepkk~SxBo`Q4iq^C~nK@qnb2U_1>ABEBiXTz*uSVhP!KIE0=j-p8~%L1dyb8wQP zP@9-ZW5QDjlls%ggdkmwAZ6YUHe2cmj4jturbE?|RnJQj-XG21=_`j?~S!?oY+yrQq^0Z4s(AG+oK&WL%bq0X!*`EV^y zH1$D!Yu4p{(t2l@>_XGQ_L>*CHEc+mFSyHH=$6VN7!^o-OlVU!-ahAd&hG=$F&zCT zEynH!z%V7IOs|G!RPb8k7aM!lrs2x_KUp)6m}vo8W2JTdj7f^u z^GVl$H5Iuv1Nrqyz3|gOzPR>x6;__w)&^S^H4DejLAq#bp)kEl#lM&_#-V>us%)}g z!R2L*Vw;Z6V#-{8u}E1RDs^b=sG#m62Tbg@wJCHNW!I!EmywxSL437`e$)IV)YR`> z#-@tW7&!-8MaaBy?={K^1-+4RVaLME@2Mr$?L7N-*X)14TnUogzyf_nAU>ilZe`sm zTwyAbTQn5y{M?~h0iU@;3G=&%WeA&LgPxb7#^8-V@JMY&5Bw}TH zUhE|7qnVfaeb=71F3~iP@}>;|rz-W`zcM%UBUxzuMGcXZt5*vmaWN7DwS|@ixJ>u+saPi4V-waq z)u#e*wFy|Y(FSs^_V&oPF>EdfMBJ1KYt@|K!_`2&Eoxq17Q9U18LGlUdxYSvBGO=q zRv;#s9%@fvXK2iVX-TR4DTmP#`@(;}k;vo<_)$uto!*KUxxQlkd%tQufH~Reg@eXG$&xjzaL^HcA6PyFR6uaLep-FvShHT!@k5;Q^D^B%@9DJ4%2vRAIsjCB-o5tx+D?yx z%R@h2YB4q|9a$qBoJ9-xYUNc;aQF_H2+i3JW27wHf&=8veM|4ZkP@^h1X%W1u62}Y zM5A3&<;bJdj^?S;%Egks%@IkoaW$#c^CcUi3lKv4Xk90TJsDQn-|AUHe8j9MrO|zmFoH_ zquPH+723fAQ-#{M%I2@A{pg;WM*K=4c*AGs;hZ4S#|ledV*AUoImrO9#23LH~mcpB!g zsEy<0NVMO>vq2Vf*Ec= zW!MKh-nx7)0T4_R+fu%xHXVmFUiC|$hA~{}ljLUEm7r+bGrAgEt=>UR38_QPBNARK z6AOVvKMV6=f{Jf$>zsF)FvAK5S)uuF**Xt4XaM3fsKW!u$zADN4;4MeW3bq+jCcDP zmoLT4@J+3bL87ce^uc3y52PC{rfbfT_o8~9kH1_i4J=nRg{=Pq?O1!O+H9=ua5l^_ zJzeTbqcboL1*m%=Y_ffByQ@QZiclwZKAd~dJVE^c#sgi-n9{0S5p)lCYbNmIrheBZ zvytl+p^wZkTWzcNWp=QN)U=v1ggMub`pwE{a;mODMs99)@-GY^zrLRc(fg%tWH1O5b?w>%rRg!>8LfHFPQkUXZ&w6WG%;I|9qd zC28$9bu8g=MY+XEuaHw&g2_;Ck9@#evM=hvLLly1hLBT;5m@+ZePg5bu6j!obxbU` zYHz%Jr4GVO-*R!nC-5?Ik&i-dHfM>*%04R_R;j~-02u!ONK`59b7qYLs^(>-SAaK0 z&$+ty%8*#W%C#N=U~O~tXZO81jj-%2o9-@vBp^N{okg4ct>Vos6XM+=?CSUS0n&gU z_I1r0JKEm;(^7_MkG5~dy58q{7VOh;%p64a>!|7P-q^yV+#>W6SMO|hQ6kfehY=?U zd)3XY7Tj3DcP{h9`M^`gsO_%bC+bIv|Lp{!jEmO{$R z1j)jxq;Ulwr6PII1l9T5JAfM)LdW{G5u>4;{E?G={k@aK?%(;!EG&Y-S2=`?7W-r_ z!v*bE*M0$3wzzC$!^)M3Y+DHUK@fd`b zY1I4JHt=ZG5JrdVtKrMJRoU!(CPoW_73+r=NIh zH8#1QJN?_`YtTs9)QYUhj1`|}A+X(E>t6$%t4RyD;=Y=0J{>h?$vI-6y3U#veXm(V zUeYv}=%n^M1`S!1qWZ|~`4G!pojhMp&Df&@jsvM*Y zpc*dlcO;7-P01*9Nij3K7pPleE@t{-nRJNKa~%u=Tv;1NWgtK{TPu|FSsOMs7H*F; zXF)TY20v!T5U%#9bb#0P+EZPY8qXPZT`t@G1&!g==K~h>Hp}Hl3S9HK8RvVisrWM` z8AC>WId3qycKxQX*O4dfp|IVpH)_hnavCuF&|B~|g!q%Yf?8juW|Bo23SU?Quz|rk z!(*qvadalV7OF{5YWB5VKOFwUz1W+g|AOlntP&ONwBRs}?s(p;z}R@lO!?h)C>TZs z_O9a>#LU|nA`C(OOJqHo=7#L}ruB2I7yIqxfhrGe<*F7cu5u7w}E z7k5l(sMnMt$6b8PwZ0y+^E5j#MfGLBHaGk6f&)#O{t~2od+L z-_~c4zegD*8x^$jDue$UsF@Rz)}D4Fc)LI0ZD^OS`r|bxCC}-%<2Bx`f1kp{@b;eo zf-@j_qW6?acWqW0Wt8}P`5!Is_)q6BepxeMz=AWBpE7rfXb&z4CqLTG1F2RoWGOn8 zfp4mFeckIn4NNAY9bfoTpXg2lrf#{fM{Q@$jc6u0K08NQdrGMsKizlpOIk{}jf6)P z#C&dhH>6?fxvGJID&-E^xPp{r&Bgb=M96l|BKT- zyn13uU}iKP>$4fI_Js?{$@Y1V`l(}Wy-eZ^-4hE`f>xV8eE4wI-^gIlQGj`Vh>6=s z%6M=;5)0#G&mdPsKt^i(o}FQ2UkBIKp)mJK;)PvWNP&&p(YdtOU`O@3$}bs0Akbv4 zYThQ&1aNQ&;ObN12-FdUR*Wx6Qa0amj5&y(xV6tMzN~l)1H*1aKn^4?5(VO8Yvt1^ z-B*e@7daKh-=r<%%;BY|M5dO6hr$YY(&*MWPXI|yX?XdNhj_i0uH;6PW7(VLVbGx? zVRxvXYS;QyGTV1wwtIb;OaIw&3!9jeNBHU}FeIwM16VLa0cOW~zm1je#Tg8fuEamh zx7t34@}dN!j38tsc_iK(G@*3$^)d;Sh{$1U^(n2B>{{p4lYD_0`7 ztn}T3cVC!1fmA!*=Ng;0%b&cis){>WbxiQjS%+87Rp|NnSGH#9*Y1xl;t6M$kzn2H zb1+AwM9Ra5+1Yl9ZkFYm)^v?>^n`&c{|ZH08oH^oow@g9Re7}52hp|DURXvf<2^h9 zXl(VfZ0y2GLze%+Y6Vu4oLh2aSkvi5Z9FlUI=#HHLpM0qZRw7W zOHW86&aT-rbON@5S^p8pObScuBlP)x#fQjYo;g*KLue3yy9-G39!CXf%B7x^!=J zA7I=<6|^hwO0bypYiILmcBc~qT$>4=_X>1Bb|i2G<9BX3zVmajtn<|#F18Ts?(Qyg zMN7Zd*tZVpPvVXX5pK`(66H=9eV^fUtyg1 z2qa?EKQnO@!5pQj)!f9GwozrH)nA4bGhMN1DmLulbCC5fd^<03?`Lg;q|0KpU!^Cg zxNQl==3n#CjVC6y0f8@W#%mlJXZ8u6!nqj{_ zE_V{u4>kz^zXp)Xzgxc-A1S~fzN002WT|Pe*>4hE8*f$qqGnZxHR7q+n}%l(A2wLE zPU(mWee;19*SKDs+myj+&txv~09<8NK-I$ham6$<*lA2CbtgP{t#-hh_~-|$f(Gqb zC7&B?82))X{Qa1qw8mXJY3kf@AbN@h4Gxar@D==)$f_Ijp$pM3roltI*iySZYnYu| zn8Mdm?-#A=gy_+`xUj&4IiqK>0>p_}Ze(2%dVG958Mzk9>{aD(TdhewDDN=XrvD#WvVH5*TiB<_sZe)cmUGKHL90nw zZ@?p$I|t+xo*93aoNkjWsLJB=a@SKUA^YsB<@dN(YwO1*(@*Zg9E-WmMFS<%TG8OU zO{ip=TBz7`XSztk)0L8J;gLVrs=jY(Y;PZ`#?@d>)327}l3cqq0ThA|H9auTAz(`t z@_ag!-}3Zd_ZLyUe=6LbzW{~a-4yKDVtsihDAn&ucoIDBy4x+GzHCbU->-;XghF3O zMr~?pJC~k;-qc_MQg;gvkIa=~pvXkS)X2V|EZ={g{D;T*e|&fAf60Zg|4%mh=q5D# z%KI;WK6#I>($aZYV))*|&fM4g^BFvCvkQ=65tctHboF2Fi8X9OtXXcmoB-qTAY z5~ZHy<)HAtHJHBLPYn2rCI8O0=HJboxer*+!I2KI=)YNu{kz*M@4?erjSH+S*@ve@ z{^|D&`DbDfik(H1vq|Hhey`xeW&Y0vWF}$L>6FH*Uj-$NbaQoe_3V<8ef0gZLZ6a> z2i`DVqLlv`i|Bcv=qcp_ebsGzs||8tsyWPyK@SXAoBYEyUCmil916^>Wf)R2*AHkK z4I_*9MTvS40t^4R%Oi#WX71lOoYZ^#yJf=(n5aIJ6pjHpa|sPKD&z+T^fOxb)%N8J zJxV4%=tk^C=%JUJNzb=GIpqsMz<~W=K=87UP_ol@-oyRDw}>1QA~XJ`sVA5h5uf1y z)_B=dOrG=+;14ra4@jo4Fn=-Lc}QNX$ndKeS|$d#=|DA*LPYS_8@a1Me? zBobZLix?QXEOR6&`hMkBK$n#|TOksr0jEs!R9J={Nzde03UhUajHBOjXWF0L9N0m09VzpY7 zDJrnt#1MCY=>;pl`Yiv!Ed}_fFy|Dyf|~Tcb0ifD5J{C%4C&% z@E?f)gE=e`Qsz7$s^q)!NdUMyUSpt>0VG%`8tkt|XWLq-M-Q$&L)z%h7t$Dr3Tx|1 zD$fzM1Gbiy%q~ndGU_T+p~6->jC0r)$Jq4Pj(2Xm3yHkGoJ@uLs0R?7_$z}=xWuM$ z)dIEgyy8Av({frh)WCyG;a@fn)gbaNM^^EKR3S{1K@+je=NoXz?Uty;#SA@g-;Tx$ zZ^-g&S6bM{903TipwZIGg2{ZNhMrgVl;&O0yP%z|1wDR#e!17cArJh>Dz1@ev7lG} zy0x}enfKhecT8}pu!6^z{bElxcz}p!UJ0_ZYz#s=+6>aU&z>C%ZuFXa58{X2?Ij7k zu}+cP-z&W5fYEL9Nx0Zxz@T)M2U$ZjlQ*XKb_#TBkNtxq>TRHjPQ5#4XS&o8cralM z&I?Sq|N0?wiJr(B!pfOxdv9{XriY6ge58y`{iRz}-=$31=8tbk@h2EyOhOU~AERZu zDRs>n#M%o=rC@{`h@b4v&@*U$g~4F#aSeeD*iYg`MgWbl&`>B5a%OO$U~+wXeWoSD z=sC|BU_qeNl`Apsn99)&5-Dm@ALpQ4QBlD>{2q#VP5~}fx{W?tS>7UGqCT78bZS{y zqwbVw|4s^ggyi55qS|4!aFrS4MR^N0mEz1{u30}MCKAnXDXmJpv~NB1@3X?hZyH>H z3EmdG`TToT!)37P;yXbmz{8!RM@k_ah#iX<_I5oHXgvYbl9df!AxzYvkFFYG&hYSb z=4d4{b9;t8WDt)+TsS#78z{M$@Y7<@S56Negd_Ku(Xslcq5~Zw8qxmPx&V?x){)$X z?ibFku5x4g!4ul#9z$wDW4iOcS+ak!ee)tKF6fRir5Yl8m^YSI0+5h%=$1Xu#tE43 z<#tVC=FMCfW6t~cd!|3K*m@0}z8;W}cxgBlv&$ITg-H6MS~XF>e#h5y)9LX{;J}_v z&~9DRV|`kXW{CPx?ug<8uo>yzAw%!_jpT;17d~wg?=nDLAFPVg5qENPcPAs^d6Rl% zQA6a#A-ZlN^LoN1cMlH*df{tJ?lp6@(PKiiKQ2w=U~_Ao*Q`xOoN6Z*UG;DReL^)M zNp3-lqPBp-@4)80&^M)0tM-(c!%Yp}!6xw&S@AseQ zeJ>ybtuG5kFj;ym*+?H!%8OXc|N&s_$Z{I3?-Rchzlrr9~uG&q#F@)yN zOwczN5Jz7`UXZ&0HEd4e5>U2d&}Q4?)Kl^oF5!P@YB zG3#;3DYd4PUoyyNIkJ>4#?$3^7h7cN&?w{dJ9G2)2_`QfcTx9-zoDq&ROHeIoqHf? z1i{v9cvJD_O{YC8&cfk?8d`dKv9ty}-bLh=!H;C7+8t#RG9nrwZ1Crjlat*F9KS-C zp0@Gx=YNSq83}`}=RG~~9>1O^6R|M0*!C93%(8tc24fR68p&!(Yd-{gxcRO3qy!Y| zPyDcAid1JS%%!}QbIB`X+Zw!vPf-N%m9TQw7&rG6C&7(W3e#i7^9HLDrSDA5Y;RYW zg7E2gY;aL)o_OZJC-^?txxMd~{m+=2n@7gvXJs`n?%Ygbo_1NHT4M$Y=DgOG2GaM_ zU4d538go5qDlIF+E_Zpk-{+$ZO>^(6llL^_$1M>!B!rS(Uq61-SH|)h(3PMG50uqWFDTlwv3Kt~WQyd+l|+9LEzC9j;b# zz>-nYip{Ohmg>DN)YU(__FD&QR-VgTP`}zJ3wH;BLt*dvnk)?rY!5|HDa1q)ljnSu zrfkijoJ!B-xdmfeG8-i6D$s^JXma*a&QFY?0r8ySx6034%qixHkGJy1-;UT0jL_QkVZ& zKO}#58TRns4gY?Sbeqj|D$8E>hln88fIQT2s*25x_M<`#AZI|HAH-4f4*i!&{S%o9 zvkrQb2j5;f%j7JUh0F6YrDKFIJt&`o^o|1bFe zrWqfoZE0!Y7(g_|%ENHH_Mclov(xFBxwe zh2)wYI!CW=KMa;v5azL1o_k@RV>meUz;&5>iJ#hGmg(wh$8hM_W2dB{Gv4wEtkG7j z8-uJ4-!0WS(-^Q1rvYRC(wUYSX7*ljC203e{WaID@jhJKqbMvJ?h#JM8}v2EjK0U- zLJ#r|x}U5Xk^Gn`7V$njf5AAQ`NMke?I*onxlsDx5&i31DoYQkeoqIF!qtWFB@vrv z_ipqjlmN!oJU3svr8)*y;z%HPg6q_fV5Lf_1M*8LK^&YCNiJg zdAb#GlJY&&xbwi(z5H6~J@=WdV#<4aO^DDxGu830nNnAO3lmCTQcSNo%NF8R^$aMo z`l^(^*!&W1T%cY!t08xa{nm$IY|75)DmjY~S|o*epQy|5wI5=Qs>UNO^vbSJ@+Q$%rT+(Y@7d7Qx~(o8&CL z-A#Q%^6a!NL|RqGX1ZXBc#4X?ZCsN_+CwtF<~DK2r9#zhIZhgQ-IVb^Kymp8#Zgq; zfzPQ6mGh6pry^|=eLYrk(oL}ZBhd#@AIMK+jYhg!rfT%iI5E|W0+*@%r^yrJigLQ? z%>i~HAFxW>(VVcw!vbTEy;_bYmUd}HY!h~F`SH6KWm2Nwf8zIOg#gkccLARK*#;9l<+lW9Nj2jel7U}P}ljg(q8{kRbIVO{< zmm^aFY-{eG(A@-UkIZW16HBiuh`Q2E9gsf8HD6ZFueAwBo>;SBI7ryu4Q2KcM}vVR z<>w`zDqmm)lh&Ut*=OR)(3&aISiwi!NA2BDmi^PFYSDrgvWh|ui4_qEu^-3gJf=w! z;Ki44YCOg8ueW+F(!HWP1_P#WcAIZHLgYZC-=@nD3>;;!vesZiG%AT+S3GV1E1GfRKn0^m6KFSNTqtO*JfAxX$q2sC`WXs_|S|mJ4S*)wyjbxKU%Zgl^v( z)u@pSh42~iBjDU_8%GJZAD!bAy{E}YNj=p_lG@n)%9J4g-pd0QB%!C8qB3ql3yDY- zv^@5wfZ#XG;FCKhyg=e_Zuv72i)s6s5>!cjqUN;t`NWmB!;N~8nY|^4(|gOlnRx{4 zZu_#&QelWMbL+*nsm;O@dH>$L3B~^dnIvT&j%#}6omYnT?X+QvNLm6%?L~73jS&oN zaoYCOzR7KkRHaF+%AtE=gGpump6!PKTxQ4;@Qa~*tSEknQl8{V($sDYB0H}gXtEp=g|Fg+1vWNnNR|LzKoPXwgZkxtNdK{V|{px z2~0aPdI{&X8b}f4-+`k?WQc8O{BuRVzc-+z)Bg?nJUkt}S)VGuU7QRdbV*jh@$?3F zV-%uX1JWC%?l%BoeaEJ>FIDn9|T9>lTe_OjyY@w1l4O|eKe(iFHClbKcz1iuhHJK@f380q!A_d;pS zzW~i)a5t09z<{_7`UJJ-8lBk!MR3!pxK-S$l|2Y|Lh4gK<6X2%YL-_l)6hBw^;Uy@ya_0{ijanK---kRHz6WRA<;1~*Bg9eS z8OOGnuk-fINE+JC+*faiK&=1sbS2Ww5~v0U-0rRlsjp)GkV3H@IX-{YMDSuQQE9a9`zp~aJA{(|2xr0G5EhZEkXz) z#n3wFx8TxQ|61B?!l>zE>$oj9bOSzA`82Vy0cgzaq9_UoV-{N%^FLuB%Yb0QsxhVM zGVe3?{3e!^2b~3m{rRcc%l))Af@U+ZOlvK_6#yf6m2_)!3PEf<|4lkn_x7SGeh=`2 z)QmW&ToN5=(lHf>H~_8vpk%kf_icXVzftA081~}z_EKR~jp)LByOj}gziE!W4Qk+g zEqa>JXC(TD7;{Z`-*Z;0!VPV7<$CLgjuN`LGJH?K&F8b>lKX48cFv$-SOA5qO^}J#n4+0Uk9seLew`X*QS!hn~uTL#i~PeF0OesuK7gk zsrex|^t!KA`pprD!;&LKFqCwRH4WW0aoBxVu@<*?8gsX5b*F($EJ3Cy`bOc5!(dS5 zrY}E|XR_=S14E|y>fsej&HZrVIkLwSo1nK8KjXijL-ZL%CU4M%#nE%GLG;Sm5{@J8 zo%$}d#1g@;{7-UloHTUYevVS_9LP&oalTO(J`H!_(|#_SX*3P1+*?^onn~SwM=Crd zjg(=J1Aq9rRD{nV6u-Q*s~aX^&Q#F{w<@f?{UKRg0jL_X?_)r-xcZwaO=QpFVuuR==kXX15xt(KnmF8Y64a^d%&V1Cx+xyy>>te!6+x zd+Wvz4?Ha*phPw=`Yc6rc;!=v3K@L4qSFxIRrWj)N=^SO1e$KWwJm?<%VZ4?tZ*#F zd>ILc_!W9OyrY`5r7W4QBL29TL&s7DulU|oeKlk-*41a6peR)IWyC%2a(k&O8nl3v zx>%p~WqQ5J=V0Rfij=&I=*vHib4Zk>iDp)Sd3U^n|K1lSNY{L^Wx8j26R%j+nDvM1 zU!im|?%lul_L%3yUh(fVW4{m1Cnt%(B3rx&n%6sVM{1dGrqc6`r)Pu84DQ#WW7+h#cW0i!vNx35`uIt z#mr-YPGPdtZmmSR8%|#?Pw6jT^Q@f6&V*`$F--&YnLze$>m+3>X}wB6HulxX-)`kXwa1I{;S*{pZF#@ELUIxKgWJ}tnOzraKPoY1&s%)q zIwRcW(>7?#pji2RH3@90gZ$#7hMvhcuDp$JC450yNv)`fW4AjN6f6G;oB!0xe}&B> zoNxMrZbawLg_R(M!EwFnjw`ue@PbXxI^`qL_BEv|IYthS2?e`9^PFlH8AtS6FN||M zrFdW5YkKGQjPnS^2ns-7{t_yyD{U6k=@VmIW?AyLHMSgPWQ&7KemhcR+54qnSu2qA z{=TneWDN4eeN8BzAH45<-Mt8eIImG-?;m^ql#kbew=cFWnJc$8`bOZS7C@%zIAVz9 ztO?W=*8?U!yHeJB$@0!4$)saed?H~3yO&D-_|@U0XbG(*w|4RayYTLBels)ZfA$-& z;_NvJh03d`8TU9e()grK9;(n%`VN(pIyF;@K8g-rf7?rorI?>kwT6{R`ROQgE5sj+ zem<(3?CB@;^&gXRgjS~6nh@l<8?@Li{;7p zCaH_|laYXP!!tM2!6$S;pi%pcy@{bTTDq@8Kvwc^nvJMyK_I;VbK( z1$W}2u4_HJ`bot6OKNsz7_T!9zax$@E3e1T$s}Bwu;W$`~iRU0b((NZ-}rTe#Wz%%^_&G&y_lzHM*W$;2a7w1(+# zJi+S)Yc$)R4k|~{Uygi!3t4p69v`;C_v-j%P5dT_3Qbty#)GJ@i1@#Cu72V7kBgkJ z&u?B;+Ag}24wdjmC!9tb`_YP~O@y8xY+?0l@v-D4OR)^o-Becv$^gHkBzcEE4mP^z zJvOs_`*&6Vl5O*PRzzPs?M^cPFU|ZGhG_MCP?w_Cg!6gI+wRjMX+WF%9x;B+Vn_c4Wn0`V8`dg+Mi+iY(05Vg|HtNFQK z+!cK654?LSTCl~D`1$?%rLJc^o+`|&0A}**>=tqEQFtALf?D8PE3lThAKe$d7_la# z&ur&rw;;b~*73f7oc+Fd7H!o1PdxP^tOXw%L+lP}$Mv^+qsfCx|E!m3Qu~%nLa><*qkfi6*LcqLmPeuZC^bk-{uHrTFd927L~wy>pzY+L5yVoa||uY6Kxgm|Tu z#aR|CIG_^Lvy#-qv+I-o7TO;hjJYnf(VmT&P_5s8YPkiM%)oK8Dg|l{7qVI=+3tt^ zX#jdIHMQd{_!RZHF#LGr;DTsa_B0_y(Dl0#xR$HJjelf@5`cE)gU9hH`k3SdXXIK{ zEThdBtaf*6>dvvxsE2LK%4c6Y(jRt>M+`z>`e-~Nw@BpsTIJaiq5dzMZ>ip|R&h^b z{&I)vtxDVU>8$wr*6w1<(%WjIW+y@uDpDGNpXimuIsaU`^hFNy#AZV5GY3T^9{O2kE`k;?bWX6N{MSdy5_&7Zc|i z{RRJeP{N;Yb~G0^gYQs%8D~g`wxx1l;wG}^UcU-lc;!hbO66sH`>`3Ny-dN7J#e7T zoZ&R5w!OKT#KZX}oBoT4#qTU#()zA4(fY4@9Wnd^G#)8#urx{CIFk(?@4+FX#7Bq8 zuczjtDCkE>LVt~AhHH#P0!L`nyeB17|1efdlZ+^TsP(pXdS^2&WM{eN@zb`sWG5QJ zlZT>T7Fbiy7Wly2cUjEpAXiv8GSr*pW+%-Kh3-}IPvx#ji2+}@#^2( z#4q?CFF`fjd-?nJ_#*^y%+8>_wPVcH;B@SX%;_AaHKvPTA#008-FRF%` Rg-#vU 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

(=2c+^{c`ijZNpj<1d2<34!j7t;Ps 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

9*df`+Cg`3#EX7Rz&vwkFj5ilx&G1Il&sO(5qq! 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 F^uL8_Mm+!k literal 0 HcmV?d00001 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 `__: + + - 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 `__. + +``SettingsService`` allows individual XBlocks to access environment and +django settings in an isolated manner: + +- XBlock settings are represented as dictionary stored in `django + settings `__ + 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 `__ +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.``, 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 `__ + 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) From d6b7b2dd0437d524cec647fa19ae7e4b24bc8e17 Mon Sep 17 00:00:00 2001 From: farhan Date: Mon, 25 Sep 2023 20:16:26 +0500 Subject: [PATCH 3/3] chore: Updating Python Requirements --- requirements/base.txt | 6 ++- requirements/dev.txt | 98 ++++++++++++++++++++++++++++++++++++++--- requirements/django.txt | 6 ++- requirements/doc.txt | 3 ++ requirements/test.txt | 79 +++++++++++++++++++++++++++++---- 5 files changed, 174 insertions(+), 18 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 221371a2a..6b14476c8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -10,8 +10,12 @@ fs==2.4.16 # via -r requirements/base.in lxml==4.9.3 # via -r requirements/base.in -markupsafe==2.1.3 +mako==1.2.4 # via -r requirements/base.in +markupsafe==2.1.3 + # via + # -r requirements/base.in + # mako python-dateutil==2.8.2 # via -r requirements/base.in pytz==2023.3.post1 diff --git a/requirements/dev.txt b/requirements/dev.txt index b9bec2950..2badfa7bb 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -12,6 +12,10 @@ appdirs==1.4.4 # via # -r requirements/test.txt # fs +arrow==1.2.3 + # via + # -r requirements/test.txt + # cookiecutter astroid==2.15.7 # via # -r requirements/test.txt @@ -21,6 +25,10 @@ attrs==23.1.0 # via # -r requirements/test.txt # hypothesis +binaryornot==0.4.4 + # via + # -r requirements/test.txt + # cookiecutter boto3==1.28.53 # via # -r requirements/test.txt @@ -34,12 +42,25 @@ build==1.0.3 # via # -r requirements/pip-tools.txt # pip-tools +certifi==2023.7.22 + # via + # -r requirements/test.txt + # requests +chardet==5.2.0 + # via + # -r requirements/test.txt + # binaryornot +charset-normalizer==3.2.0 + # via + # -r requirements/test.txt + # requests click==8.1.7 # via # -r requirements/pip-tools.txt # -r requirements/test.txt # click-log # code-annotations + # cookiecutter # edx-lint # pip-tools click-log==0.4.0 @@ -50,6 +71,10 @@ code-annotations==1.5.0 # via # -r requirements/test.txt # edx-lint +cookiecutter==2.3.1 + # via + # -r requirements/test.txt + # xblock-sdk coverage[toml]==7.3.1 # via # -r requirements/ci.txt @@ -75,6 +100,7 @@ django==2.2.28 # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt # openedx-django-pyfs + # xblock-sdk edx-lint==5.3.4 # via -r requirements/test.txt exceptiongroup==1.1.3 @@ -93,12 +119,18 @@ fs==2.4.16 # -r requirements/test.txt # fs-s3fs # openedx-django-pyfs + # xblock fs-s3fs==1.1.1 # via # -r requirements/test.txt # openedx-django-pyfs -hypothesis==6.86.2 + # xblock-sdk +hypothesis==6.87.0 # via -r requirements/test.txt +idna==3.4 + # via + # -r requirements/test.txt + # requests importlib-metadata==6.8.0 # via # -r requirements/pip-tools.txt @@ -119,6 +151,7 @@ jinja2==3.1.2 # via # -r requirements/test.txt # code-annotations + # cookiecutter # diff-cover # jinja2-pluralize jinja2-pluralize==0.3.0 @@ -137,15 +170,30 @@ lazy-object-proxy==1.9.0 # -r requirements/test.txt # astroid lxml==4.9.3 + # via + # -r requirements/test.txt + # xblock + # xblock-sdk +mako==1.2.4 # via -r requirements/test.txt +markdown-it-py==3.0.0 + # via + # -r requirements/test.txt + # rich markupsafe==2.1.3 # via # -r requirements/test.txt # jinja2 + # mako + # xblock mccabe==0.7.0 # via # -r requirements/test.txt # pylint +mdurl==0.1.2 + # via + # -r requirements/test.txt + # markdown-it-py mock==5.1.0 # via -r requirements/test.txt openedx-django-pyfs==3.4.0 @@ -186,11 +234,11 @@ py==1.11.0 # tox pycodestyle==2.11.0 # via -r requirements/test.txt -pydantic==2.3.0 +pydantic==2.4.0 # via # -r requirements/test.txt # inflect -pydantic-core==2.6.3 +pydantic-core==2.10.0 # via # -r requirements/test.txt # pydantic @@ -198,7 +246,8 @@ pygments==2.16.1 # via # -r requirements/test.txt # diff-cover -pylint==2.17.5 + # rich +pylint==2.17.6 # via # -r requirements/test.txt # edx-lint @@ -218,6 +267,10 @@ pylint-plugin-utils==0.8.2 # -r requirements/test.txt # pylint-celery # pylint-django +pypng==0.20220715.0 + # via + # -r requirements/test.txt + # xblock-sdk pyproject-hooks==1.0.0 # via # -r requirements/pip-tools.txt @@ -234,25 +287,42 @@ pytest-django==4.5.2 python-dateutil==2.8.2 # via # -r requirements/test.txt + # arrow # botocore + # xblock python-slugify==8.0.1 # via # -r requirements/test.txt # code-annotations + # cookiecutter pytz==2023.3.post1 # via # -r requirements/test.txt # django + # xblock pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations + # cookiecutter + # xblock +requests==2.31.0 + # via + # -r requirements/test.txt + # cookiecutter + # xblock-sdk +rich==13.5.3 + # via + # -r requirements/test.txt + # cookiecutter s3transfer==0.6.2 # via # -r requirements/test.txt # boto3 simplejson==3.19.1 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # xblock-sdk six==1.16.0 # via # -r requirements/ci.txt @@ -311,19 +381,27 @@ typing-extensions==4.8.0 # pydantic # pydantic-core # pylint + # rich urllib3==1.26.16 # via # -r requirements/test.txt # botocore + # requests virtualenv==20.24.5 # via # -r requirements/ci.txt # -r requirements/test.txt # tox web-fragments==2.1.0 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # xblock + # xblock-sdk webob==1.8.7 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # xblock + # xblock-sdk wheel==0.41.2 # via # -r requirements/pip-tools.txt @@ -332,6 +410,12 @@ wrapt==1.15.0 # via # -r requirements/test.txt # astroid +xblock==1.7.0 + # via + # -r requirements/test.txt + # xblock-sdk +xblock-sdk==0.7.0 + # via -r requirements/test.txt zipp==3.17.0 # via # -r requirements/pip-tools.txt diff --git a/requirements/django.txt b/requirements/django.txt index ac5424f7f..4d0569b60 100644 --- a/requirements/django.txt +++ b/requirements/django.txt @@ -34,8 +34,12 @@ lazy==1.6 # via -r requirements/django.in lxml==4.9.3 # via -r requirements/base.txt -markupsafe==2.1.3 +mako==1.2.4 # via -r requirements/base.txt +markupsafe==2.1.3 + # via + # -r requirements/base.txt + # mako openedx-django-pyfs==3.4.0 # via -r requirements/django.in python-dateutil==2.8.2 diff --git a/requirements/doc.txt b/requirements/doc.txt index 4a2574f76..6b61dda84 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -66,10 +66,13 @@ lazy==1.6 # via -r requirements/django.txt lxml==4.9.3 # via -r requirements/django.txt +mako==1.2.4 + # via -r requirements/django.txt markupsafe==2.1.3 # via # -r requirements/django.txt # jinja2 + # mako mock==5.1.0 # via -r requirements/doc.in openedx-django-pyfs==3.4.0 diff --git a/requirements/test.txt b/requirements/test.txt index 17b2a9664..5c48c51ad 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -10,6 +10,8 @@ appdirs==1.4.4 # via # -r requirements/django.txt # fs +arrow==1.2.3 + # via cookiecutter astroid==2.15.7 # via # -r requirements/test.in @@ -17,6 +19,8 @@ astroid==2.15.7 # pylint-celery attrs==23.1.0 # via hypothesis +binaryornot==0.4.4 + # via cookiecutter boto3==1.28.53 # via # -r requirements/django.txt @@ -26,15 +30,24 @@ botocore==1.31.53 # -r requirements/django.txt # boto3 # s3transfer +certifi==2023.7.22 + # via requests +chardet==5.2.0 + # via binaryornot +charset-normalizer==3.2.0 + # via requests click==8.1.7 # via # click-log # code-annotations + # cookiecutter # edx-lint click-log==0.4.0 # via edx-lint code-annotations==1.5.0 # via edx-lint +cookiecutter==2.3.1 + # via xblock-sdk coverage[toml]==7.3.1 # via # -r requirements/test.in @@ -53,6 +66,7 @@ distlib==0.3.7 # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/django.txt # openedx-django-pyfs + # xblock-sdk edx-lint==5.3.4 # via -r requirements/test.in exceptiongroup==1.1.3 @@ -68,12 +82,16 @@ fs==2.4.16 # -r requirements/django.txt # fs-s3fs # openedx-django-pyfs + # xblock fs-s3fs==1.1.1 # via # -r requirements/django.txt # openedx-django-pyfs -hypothesis==6.86.2 + # xblock-sdk +hypothesis==6.87.0 # via -r requirements/test.in +idna==3.4 + # via requests inflect==7.0.0 # via jinja2-pluralize iniconfig==2.0.0 @@ -83,6 +101,7 @@ isort==5.12.0 jinja2==3.1.2 # via # code-annotations + # cookiecutter # diff-cover # jinja2-pluralize jinja2-pluralize==0.3.0 @@ -97,13 +116,24 @@ lazy==1.6 lazy-object-proxy==1.9.0 # via astroid lxml==4.9.3 + # via + # -r requirements/django.txt + # xblock + # xblock-sdk +mako==1.2.4 # via -r requirements/django.txt +markdown-it-py==3.0.0 + # via rich markupsafe==2.1.3 # via # -r requirements/django.txt # jinja2 + # mako + # xblock mccabe==0.7.0 # via pylint +mdurl==0.1.2 + # via markdown-it-py mock==5.1.0 # via -r requirements/test.in openedx-django-pyfs==3.4.0 @@ -129,13 +159,15 @@ py==1.11.0 # via tox pycodestyle==2.11.0 # via -r requirements/test.in -pydantic==2.3.0 +pydantic==2.4.0 # via inflect -pydantic-core==2.6.3 +pydantic-core==2.10.0 # via pydantic pygments==2.16.1 - # via diff-cover -pylint==2.17.5 + # via + # diff-cover + # rich +pylint==2.17.6 # via # -r requirements/test.in # edx-lint @@ -150,6 +182,8 @@ pylint-plugin-utils==0.8.2 # via # pylint-celery # pylint-django +pypng==0.20220715.0 + # via xblock-sdk pytest==7.4.2 # via # -r requirements/test.in @@ -162,23 +196,38 @@ pytest-django==4.5.2 python-dateutil==2.8.2 # via # -r requirements/django.txt + # arrow # botocore + # xblock python-slugify==8.0.1 - # via code-annotations + # via + # code-annotations + # cookiecutter pytz==2023.3.post1 # via # -r requirements/django.txt # django + # xblock pyyaml==6.0.1 # via # -r requirements/django.txt # code-annotations + # cookiecutter + # xblock +requests==2.31.0 + # via + # cookiecutter + # xblock-sdk +rich==13.5.3 + # via cookiecutter s3transfer==0.6.2 # via # -r requirements/django.txt # boto3 simplejson==3.19.1 - # via -r requirements/django.txt + # via + # -r requirements/django.txt + # xblock-sdk six==1.16.0 # via # -r requirements/django.txt @@ -217,18 +266,30 @@ typing-extensions==4.8.0 # pydantic # pydantic-core # pylint + # rich urllib3==1.26.16 # via # -r requirements/django.txt # botocore + # requests virtualenv==20.24.5 # via tox web-fragments==2.1.0 - # via -r requirements/django.txt + # via + # -r requirements/django.txt + # xblock + # xblock-sdk webob==1.8.7 - # via -r requirements/django.txt + # via + # -r requirements/django.txt + # xblock + # xblock-sdk wrapt==1.15.0 # via astroid +xblock==1.7.0 + # via xblock-sdk +xblock-sdk==0.7.0 + # via -r requirements/test.in # The following packages are considered to be unsafe in a requirements file: # setuptools