From c7f68c311bf6f2835f1a37e25ecbf3f4a5e78e82 Mon Sep 17 00:00:00 2001 From: Muhammad Farhan Khan Date: Tue, 26 Sep 2023 10:58:36 +0500 Subject: [PATCH 1/7] chore: mention repo deprecation in the readme --- README.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.rst b/README.rst index 76fda07..c8e439e 100644 --- a/README.rst +++ b/README.rst @@ -10,6 +10,29 @@ Purpose These are a collection of utility functions, base test classes and documentation that are useful for any XBlocks. + +⚠️ Deprecation Notice ⚠️ +********************** + +**Effective Date:** September 26, 2023 + +**Repository Migration:** +This `xblock-utils` repository has been deprecated as of September 26, 2023, and the code and documentation have been migrated to the `Xblock `_ repository. + +This decision was made to streamline and consolidate our codebase. + +The migration process was completed through this Pull Request: `PR #669 `_ + +What to Expect: +=============== + +**Archival**: We are going to archive the `xblock-utils` repository. This means that it will become read-only, and no further updates or changes will be accepted. + +We appreciate your understanding and cooperation during this transition. If you have any questions or concerns, please don't hesitate to reach out to us through the `XBlock` repository's issue tracker. + +Thank you for your continued support and contributions to the Open edX community. + + Getting Started *************** From d3bbca881ecea34ca7a4feface8a13b4aa0a7ba0 Mon Sep 17 00:00:00 2001 From: Muhammad Farhan Khan Date: Tue, 26 Sep 2023 11:31:56 +0500 Subject: [PATCH 2/7] chore: update --- README.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c8e439e..178f45f 100644 --- a/README.rst +++ b/README.rst @@ -17,15 +17,12 @@ documentation that are useful for any XBlocks. **Effective Date:** September 26, 2023 **Repository Migration:** -This `xblock-utils` repository has been deprecated as of September 26, 2023, and the code and documentation have been migrated to the `Xblock `_ repository. +This `xblock-utils` repository has been `deprecated `_ as of September 26, 2023, and the code and documentation have been migrated to the `Xblock `_ repository. This decision was made to streamline and consolidate our codebase. The migration process was completed through this Pull Request: `PR #669 `_ -What to Expect: -=============== - **Archival**: We are going to archive the `xblock-utils` repository. This means that it will become read-only, and no further updates or changes will be accepted. We appreciate your understanding and cooperation during this transition. If you have any questions or concerns, please don't hesitate to reach out to us through the `XBlock` repository's issue tracker. From ca4dd442c9c45970fd24f3852e149800425b1cfa Mon Sep 17 00:00:00 2001 From: farhan Date: Tue, 26 Sep 2023 20:05:06 +0500 Subject: [PATCH 3/7] fix: fix quality error --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 178f45f..5a47036 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ documentation that are useful for any XBlocks. ⚠️ Deprecation Notice ⚠️ -********************** +************************ **Effective Date:** September 26, 2023 From 497dfe8df5969a137279fb91886707c29b15790a Mon Sep 17 00:00:00 2001 From: farhan Date: Wed, 27 Sep 2023 12:20:59 +0500 Subject: [PATCH 4/7] chore: add deprecation warnings on use of deprecated package import --- xblockutils/__init__.py | 2 +- xblockutils/deprecation/__init__.py | 0 xblockutils/deprecation/warn.py | 34 +++++++++++++++++++++++++++++ xblockutils/helpers.py | 7 ++++++ xblockutils/publish_event.py | 7 ++++++ xblockutils/resources.py | 11 +++++++--- xblockutils/settings.py | 6 +++++ xblockutils/studio_editable.py | 14 ++++++++---- xblockutils/templatetags/i18n.py | 6 +++++ 9 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 xblockutils/deprecation/__init__.py create mode 100644 xblockutils/deprecation/warn.py diff --git a/xblockutils/__init__.py b/xblockutils/__init__.py index ac57d0f..97599e3 100644 --- a/xblockutils/__init__.py +++ b/xblockutils/__init__.py @@ -2,4 +2,4 @@ Useful classes and functionality for building and testing XBlocks """ -__version__ = '3.4.1' +__version__ = '4.0.0' diff --git a/xblockutils/deprecation/__init__.py b/xblockutils/deprecation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xblockutils/deprecation/warn.py b/xblockutils/deprecation/warn.py new file mode 100644 index 0000000..1475602 --- /dev/null +++ b/xblockutils/deprecation/warn.py @@ -0,0 +1,34 @@ +""" +Utilities for warning about the use of deprecated package + +See https://github.com/openedx/xblock-utils/issues/197 for details. +""" + +import warnings + + +class DeprecatedPackageWarning(DeprecationWarning): + """ + A warning that a deprecated package is being used. + """ + + def __init__(self, old_import, new_import): + super().__init__() + self.old_import = old_import + self.new_import = new_import + + def __str__(self): + return ( + "Please use import {self.new_import} instead of {self.old_import} as " + "'xblock-utils' package has been deprecated and migrated within 'xblock' package. " + ).format(self=self) + + +def warn_deprecated_package(old_import, new_import): + """ + Warn that a package has been deprecated + """ + warnings.warn( + DeprecatedPackageWarning(old_import, new_import), + stacklevel=3, # Should surface the line that is doing the importing. + ) diff --git a/xblockutils/helpers.py b/xblockutils/helpers.py index 941daa3..b31773f 100644 --- a/xblockutils/helpers.py +++ b/xblockutils/helpers.py @@ -2,6 +2,13 @@ Useful helper methods """ +from xblockutils.deprecation.warn import warn_deprecated_package + +warn_deprecated_package( + 'xblockutils.helpers', + 'xblock.utils.helpers' +) + def child_isinstance(block, child_id, block_class_or_mixin): """ diff --git a/xblockutils/publish_event.py b/xblockutils/publish_event.py index d366f9c..00783de 100644 --- a/xblockutils/publish_event.py +++ b/xblockutils/publish_event.py @@ -22,6 +22,13 @@ from xblock.core import XBlock +from xblockutils.deprecation.warn import warn_deprecated_package + +warn_deprecated_package( + 'xblockutils.publish_event', + 'xblock.utils.publish_event' +) + class PublishEventMixin: """ diff --git a/xblockutils/resources.py b/xblockutils/resources.py index 2f6b268..8e9986c 100644 --- a/xblockutils/resources.py +++ b/xblockutils/resources.py @@ -25,12 +25,17 @@ import warnings import pkg_resources - from django.template import Context, Template, Engine from django.template.backends.django import get_installed_libraries - -from mako.template import Template as MakoTemplate from mako.lookup import TemplateLookup as MakoTemplateLookup +from mako.template import Template as MakoTemplate + +from xblockutils.deprecation.warn import warn_deprecated_package + +warn_deprecated_package( + 'xblockutils.resources', + 'xblock.utils.resources' +) class ResourceLoader: diff --git a/xblockutils/settings.py b/xblockutils/settings.py index 26552fe..8b5302c 100644 --- a/xblockutils/settings.py +++ b/xblockutils/settings.py @@ -5,8 +5,14 @@ This module contains a mixins that allows third party XBlocks to access Settings Service in edX LMS. """ +from xblockutils.deprecation.warn import warn_deprecated_package from xblockutils.resources import ResourceLoader +warn_deprecated_package( + 'xblockutils.settings', + 'xblock.utils.settings' +) + class XBlockWithSettingsMixin: """ diff --git a/xblockutils/studio_editable.py b/xblockutils/studio_editable.py index 7100681..5841baa 100644 --- a/xblockutils/studio_editable.py +++ b/xblockutils/studio_editable.py @@ -11,23 +11,29 @@ import logging -import simplejson as json +import simplejson as json +from web_fragments.fragment import Fragment from xblock.core import XBlock, XBlockMixin -from xblock.fields import Scope, JSONField, List, Integer, Float, Boolean, String, DateTime from xblock.exceptions import JsonHandlerError, NoSuchViewError -from web_fragments.fragment import Fragment +from xblock.fields import Scope, JSONField, List, Integer, Float, Boolean, String, DateTime from xblock.validation import Validation +from xblockutils.deprecation.warn import warn_deprecated_package from xblockutils.resources import ResourceLoader +warn_deprecated_package( + 'xblockutils.studio_editable', + 'xblock.utils.studio_editable' +) + # Globals ########################################################### log = logging.getLogger(__name__) loader = ResourceLoader(__name__) -# Classes ########################################################### +# Classes ########################################################### class FutureFields: """ diff --git a/xblockutils/templatetags/i18n.py b/xblockutils/templatetags/i18n.py index 90ab8a8..2867e50 100644 --- a/xblockutils/templatetags/i18n.py +++ b/xblockutils/templatetags/i18n.py @@ -10,6 +10,12 @@ from django.templatetags import i18n from django.utils.translation import get_language, trans_real +from xblockutils.deprecation.warn import warn_deprecated_package + +warn_deprecated_package( + 'xblockutils.templatetags.i18n', + 'xblock.utils.templatetags.i18n' +) register = Library() From 0294eae69de552a7eb00a7485edc6afb46e1cf57 Mon Sep 17 00:00:00 2001 From: farhan Date: Mon, 2 Oct 2023 11:01:12 +0500 Subject: [PATCH 5/7] chore: remove code and import from xblock respository --- setup.py | 2 +- xblockutils/helpers.py | 28 +- xblockutils/publish_event.py | 59 +--- xblockutils/resources.py | 122 +------- xblockutils/settings.py | 93 +----- xblockutils/studio_editable.py | 512 +------------------------------ xblockutils/templatetags/i18n.py | 76 +---- 7 files changed, 25 insertions(+), 867 deletions(-) diff --git a/setup.py b/setup.py index 4f8a5fe..f81b656 100644 --- a/setup.py +++ b/setup.py @@ -137,7 +137,7 @@ def get_version(*file_paths): 'xblockutils', ], install_requires=load_requirements('requirements/base.in'), - package_data=package_data("xblockutils", ["public", "templates", "templatetags"]), + package_data=package_data("xblockutils", ["deprecation", "public", "templates", "templatetags"]), url='https://github.com/openedx/xblock-utils', classifiers=[ 'Framework :: Django :: 3.2', diff --git a/xblockutils/helpers.py b/xblockutils/helpers.py index b31773f..8d7fec4 100644 --- a/xblockutils/helpers.py +++ b/xblockutils/helpers.py @@ -1,6 +1,6 @@ -""" -Useful helper methods -""" +"""Deprecated package support.""" +# pylint: disable=useless-suppression,line-too-long,redefined-builtin,wildcard-import, +# pylint: disable=wrong-import-position,wrong-import-order from xblockutils.deprecation.warn import warn_deprecated_package @@ -9,24 +9,4 @@ 'xblock.utils.helpers' ) - -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) +from xblock.utils.helpers import * diff --git a/xblockutils/publish_event.py b/xblockutils/publish_event.py index 00783de..cd79e0f 100644 --- a/xblockutils/publish_event.py +++ b/xblockutils/publish_event.py @@ -1,26 +1,6 @@ -# -# Copyright (C) 2014-2015 edX -# -# This software's license gives you freedom; you can copy, convey, -# propagate, redistribute and/or modify this program under the terms of -# the GNU Affero General Public License (AGPL) as published by the Free -# Software Foundation (FSF), either version 3 of the License, or (at your -# option) any later version of the AGPL published by the FSF. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero -# General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program in a file in the toplevel directory called -# "AGPLv3". If not, see . -# -""" -PublishEventMixin: A mixin for publishing events from an XBlock -""" - -from xblock.core import XBlock +"""Deprecated package support.""" +# pylint: disable=useless-suppression,line-too-long,redefined-builtin,wildcard-import, +# pylint: disable=wrong-import-position,wrong-import-order from xblockutils.deprecation.warn import warn_deprecated_package @@ -29,35 +9,4 @@ 'xblock.utils.publish_event' ) - -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'} +from xblock.utils.publish_event import * diff --git a/xblockutils/resources.py b/xblockutils/resources.py index 8e9986c..c220f55 100644 --- a/xblockutils/resources.py +++ b/xblockutils/resources.py @@ -1,34 +1,6 @@ -# -# Copyright (C) 2014-2015 Harvard, edX, OpenCraft -# -# This software's license gives you freedom; you can copy, convey, -# propagate, redistribute and/or modify this program under the terms of -# the GNU Affero General Public License (AGPL) as published by the Free -# Software Foundation (FSF), either version 3 of the License, or (at your -# option) any later version of the AGPL published by the FSF. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero -# General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program in a file in the toplevel directory called -# "AGPLv3". If not, see . -# -""" -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 +"""Deprecated package support.""" +# pylint: disable=useless-suppression,line-too-long,redefined-builtin,wildcard-import, +# pylint: disable=wrong-import-position,wrong-import-order from xblockutils.deprecation.warn import warn_deprecated_package @@ -37,90 +9,4 @@ 'xblock.utils.resources' ) - -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': 'xblockutils.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 - """ - 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 +from xblock.utils.resources import * diff --git a/xblockutils/settings.py b/xblockutils/settings.py index 8b5302c..156481d 100644 --- a/xblockutils/settings.py +++ b/xblockutils/settings.py @@ -1,97 +1,12 @@ -# -# Copyright (C) 2015 OpenCraft -# License: AGPLv3 -""" -This module contains a mixins that allows third party XBlocks to access Settings Service in edX LMS. -""" +"""Deprecated package support.""" +# pylint: disable=useless-suppression,line-too-long,redefined-builtin,wildcard-import, +# pylint: disable=wrong-import-position,wrong-import-order from xblockutils.deprecation.warn import warn_deprecated_package -from xblockutils.resources import ResourceLoader warn_deprecated_package( 'xblockutils.settings', 'xblock.utils.settings' ) - -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 settigns 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)) +from xblock.utils.settings import * diff --git a/xblockutils/studio_editable.py b/xblockutils/studio_editable.py index 5841baa..3d8945b 100644 --- a/xblockutils/studio_editable.py +++ b/xblockutils/studio_editable.py @@ -1,516 +1,12 @@ -# -# Copyright (C) 2015 OpenCraft -# License: AGPLv3 -""" -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.validation import Validation +"""Deprecated package support.""" +# pylint: disable=useless-suppression,line-too-long,redefined-builtin,wildcard-import, +# pylint: disable=wrong-import-position,wrong-import-order from xblockutils.deprecation.warn import warn_deprecated_package -from xblockutils.resources import ResourceLoader warn_deprecated_package( 'xblockutils.studio_editable', 'xblock.utils.studio_editable' ) -# 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 +from xblock.utils.studio_editable import * diff --git a/xblockutils/templatetags/i18n.py b/xblockutils/templatetags/i18n.py index 2867e50..c24fbc8 100644 --- a/xblockutils/templatetags/i18n.py +++ b/xblockutils/templatetags/i18n.py @@ -1,14 +1,6 @@ -""" -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 +"""Deprecated package support.""" +# pylint: disable=useless-suppression,line-too-long,redefined-builtin,wildcard-import, +# pylint: disable=wrong-import-position,wrong-import-order from xblockutils.deprecation.warn import warn_deprecated_package @@ -17,64 +9,4 @@ 'xblock.utils.templatetags.i18n' ) -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 xblock.utils.templatetags.i18n import * From fd0723f5122ab43f2273097df05a04f2b76aa979 Mon Sep 17 00:00:00 2001 From: farhan Date: Mon, 2 Oct 2023 19:36:25 +0500 Subject: [PATCH 6/7] fix: fix test cases --- tests/unit/test_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index 4c2caa9..80df76f 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -117,7 +117,7 @@ def test_theme_files_are_loaded_from_correct_package(self, xblock_class): 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("xblockutils.settings.ResourceLoader") as patched_resource_loader: + with patch("xblock.utils.settings.ResourceLoader") as patched_resource_loader: xblock.include_theme_files(fragment) patched_resource_loader.assert_called_with(package_name) @@ -133,7 +133,7 @@ def test_theme_files_are_added_to_fragment(self, package_name, locations): 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("xblockutils.settings.ResourceLoader.load_unicode") as patched_load_unicode: + 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) @@ -146,6 +146,6 @@ def test_invalid_default_theme_config(self, theme_config): xblock.default_theme_config = theme_config self.service_mock.get_settings_bucket = Mock(return_value={}) fragment = MagicMock() - with patch("xblockutils.settings.ResourceLoader.load_unicode") as patched_load_unicode: + with patch("xblock.utils.settings.ResourceLoader.load_unicode") as patched_load_unicode: xblock.include_theme_files(fragment) patched_load_unicode.assert_not_called() From 8fe1a755381e6841af46de94e154f70c55e7f7a9 Mon Sep 17 00:00:00 2001 From: Muhammad Farhan Khan Date: Wed, 4 Oct 2023 09:37:56 +0500 Subject: [PATCH 7/7] chore: Update xblockutils/deprecation/warn.py Co-authored-by: Feanil Patel --- xblockutils/deprecation/warn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xblockutils/deprecation/warn.py b/xblockutils/deprecation/warn.py index 1475602..01fd662 100644 --- a/xblockutils/deprecation/warn.py +++ b/xblockutils/deprecation/warn.py @@ -19,8 +19,8 @@ def __init__(self, old_import, new_import): def __str__(self): return ( - "Please use import {self.new_import} instead of {self.old_import} as " - "'xblock-utils' package has been deprecated and migrated within 'xblock' package. " + "Please use import {self.new_import} instead of {self.old_import} because " + "the 'xblock-utils' package has been deprecated and migrated to within 'xblock' package. " ).format(self=self)