From ea8cf452f772adbdeb8e8c8619d41733e41616d9 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 8 Mar 2024 14:25:28 +1030 Subject: [PATCH] feat: ported SupersetXBlock from platform-plugin-superset --- .../static/html/superset_edit.html | 96 ++++++ .../static/js/install_required.js | 11 + .../static/js/superset_edit.js | 19 ++ platform_plugin_aspects/superset_xblock.py | 299 +++++++++++++++++ platform_plugin_aspects/utils.py | 54 +++ platform_plugin_aspects/xblock.py | 311 ++++++++++++++++++ requirements/base.in | 1 + requirements/base.txt | 50 ++- requirements/ci.txt | 4 +- requirements/dev.txt | 56 +++- requirements/doc.txt | 80 +++-- requirements/pip-tools.txt | 6 +- requirements/pip.txt | 2 +- requirements/quality.txt | 50 ++- requirements/test.txt | 48 ++- setup.py | 3 + 16 files changed, 1008 insertions(+), 82 deletions(-) create mode 100644 platform_plugin_aspects/static/html/superset_edit.html create mode 100644 platform_plugin_aspects/static/js/install_required.js create mode 100644 platform_plugin_aspects/static/js/superset_edit.js create mode 100644 platform_plugin_aspects/superset_xblock.py create mode 100644 platform_plugin_aspects/xblock.py diff --git a/platform_plugin_aspects/static/html/superset_edit.html b/platform_plugin_aspects/static/html/superset_edit.html new file mode 100644 index 0000000..be49e0e --- /dev/null +++ b/platform_plugin_aspects/static/html/superset_edit.html @@ -0,0 +1,96 @@ +{% load i18n %} + +
+ + + +
diff --git a/platform_plugin_aspects/static/js/install_required.js b/platform_plugin_aspects/static/js/install_required.js new file mode 100644 index 0000000..49f8344 --- /dev/null +++ b/platform_plugin_aspects/static/js/install_required.js @@ -0,0 +1,11 @@ +try { + (function (require) { + require.config({ + paths: { + supersetEmbeddedSdk: "https://cdn.jsdelivr.net/npm/@superset-ui/embedded-sdk@0.1.0-alpha.10/bundle/index.min", + }, + }); + }).call(this, require || RequireJS.require); +} catch (e) { + console.log("Unable to load embedded_sdk via requirejs"); +} diff --git a/platform_plugin_aspects/static/js/superset_edit.js b/platform_plugin_aspects/static/js/superset_edit.js new file mode 100644 index 0000000..5dac93c --- /dev/null +++ b/platform_plugin_aspects/static/js/superset_edit.js @@ -0,0 +1,19 @@ +/* Javascript for SupersetXBlock. */ +function SupersetXBlock(runtime, element) { + + $(element).find('.save-button').bind('click', function() { + var handlerUrl = runtime.handlerUrl(element, 'studio_submit'); + var data = { + display_name: $(element).find('input[name=superset_display_name]').val(), + dashboard_uuid: $(element).find('input[name=superset_dashboard_uuid]').val(), + filters: $(element).find('input[name=superset_filters]').val(), + }; + $.post(handlerUrl, JSON.stringify(data)).done(function(response) { + window.location.reload(false); + }); + }); + + $(element).find('.cancel-button').bind('click', function() { + runtime.notify('cancel', {}); + }); +} diff --git a/platform_plugin_aspects/superset_xblock.py b/platform_plugin_aspects/superset_xblock.py new file mode 100644 index 0000000..fd15621 --- /dev/null +++ b/platform_plugin_aspects/superset_xblock.py @@ -0,0 +1,299 @@ +"""XBlock to embed a Superset dashboards in Open edX.""" +from __future__ import annotations + +import logging +from typing import Tuple + +import pkg_resources +from django.conf import settings +from django.utils import translation +from web_fragments.fragment import Fragment +from xblock.core import XBlock +from xblock.fields import List, Scope, String +from xblock.utils.resources import ResourceLoader +from xblock.utils.settings import XBlockWithSettingsMixin + +from .utils import _, update_context + +log = logging.getLogger(__name__) +loader = ResourceLoader(__name__) + + +@XBlock.wants("user") +@XBlock.needs("i18n") +@XBlock.needs("settings") +class SupersetXBlock(XBlockWithSettingsMixin, XBlock): + """ + Superset XBlock provides a way to embed dashboards from Superset in a course. + """ + + block_settings_key = 'SupersetXBlock' + + display_name = String( + display_name=_("Display name"), + help=_("Display name"), + default="Superset Dashboard", + scope=Scope.settings, + ) + + dashboard_uuid = String( + display_name=_("Dashboard UUID"), + help=_( + "The ID of the dashboard to embed. Available in the Superset embed dashboard UI." + ), + default="", + scope=Scope.settings, + ) + + filters = List( + display_name=_("Filters"), + help=_( + """Semicolon separated list of SQL filters to apply to the + dashboard. E.g: org='edX'; country in ('us', 'co'). + The fields used here must be available on every dataset used by the dashboard. + """ + ), + default=[], + scope=Scope.settings, + ) + + def resource_string(self, path): + """Handy helper for getting resources from our kit.""" + data = pkg_resources.resource_string(__name__, path) + return data.decode("utf8") + + def render_template(self, template_path, context=None) -> str: + """ + Render a template with the given context. + + The template is translatedaccording to the user's language. + + args: + template_path: The path to the template + context: The context to render in the template + + returns: + The rendered template + """ + return loader.render_django_template( + template_path, context, i18n_service=self.runtime.service(self, "i18n") + ) + + def user_is_staff(self, user) -> bool: + """ + Check whether the user has course staff permissions for this XBlock. + """ + return user.opt_attrs.get("edx-platform.user_is_staff") + + def is_student(self, user) -> bool: + """ + Check if the user is a student. + """ + return user.opt_attrs.get("edx-platform.user_role") == "Student" + + def anonymous_user_id(self, user) -> str: + """ + Return the anonymous user ID of the user. + """ + return user.opt_attrs.get("edx-platform.anonymous_user_id") + + def get_superset_config(self): + """ + Returns a dict containing Superset connection details. + + Dict will contain the following keys: + + * service_url + * internal_service_url + * username + * password + """ + superset_config = self.get_xblock_settings({}) + cleaned_config = { + "username": superset_config.get("username"), + "password": superset_config.get("password"), + "internal_service_url": superset_config.get("internal_service_url"), + "service_url": superset_config.get("service_url"), + } + + # SupersetClient requires a trailing slash for service URLs. + for key in ('service_url', 'internal_service_url'): + url = cleaned_config.get(key, "http://superset:8088") + if url and url[-1] != '/': + url += '/' + cleaned_config[key] = url + + return cleaned_config + + def student_view(self, context=None): + """ + Render the view shown to users of this XBlock. + """ + user_service = self.runtime.service(self, "user") + user = user_service.get_current_user() + + context.update( + { + "self": self, + "user": user, + "course": self.course_id, + "display_name": self.display_name, + } + ) + + # Hide Superset content from learners + if self.user_is_student(user): + frag = Fragment() + frag.add_content(self.render_template("static/html/superset_student.html", context)) + return frag + + superset_config = self.get_superset_config() + + if self.dashboard_uuid: + context = update_context( + context=context, + superset_config=superset_config, + dashboard_uuid=self.dashboard_uuid, + filters=self.filters, + ) + + context["xblock_id"] = self.scope_ids.usage_id.block_id + + frag = Fragment() + frag.add_content(self.render_template("static/html/superset.html", context)) + frag.add_css(self.resource_string("static/css/superset.css")) + frag.add_javascript(self.resource_string("static/js/install_required.js")) + + # Add i18n js + statici18n_js_url = self._get_statici18n_js_url() + if statici18n_js_url: + frag.add_javascript_url( + self.runtime.local_resource_url(self, statici18n_js_url) + ) + frag.add_javascript(self.resource_string("static/js/embed_dashboard.js")) + frag.add_javascript(self.resource_string("static/js/superset.js")) + frag.initialize_js( + "SupersetXBlock", + json_args=context, + ) + return frag + + def studio_view(self, context=None): + """ + Render the view shown when editing this XBlock. + """ + superset_config = self.get_superset_config() + filters = "; ".join(self.filters) + context = { + "display_name": self.display_name, + "dashboard_uuid": self.dashboard_uuid, + "filters": filters, + "display_name_field": self.fields[ # pylint: disable=unsubscriptable-object + "display_name" + ], + "dashboard_uuid_field": self.fields[ # pylint: disable=unsubscriptable-object + "dashboard_uuid" + ], + "filters_field": self.fields[ # pylint: disable=unsubscriptable-object + "filters" + ], + } + + frag = Fragment() + frag.add_content( + self.render_template("static/html/superset_edit.html", context) + ) + frag.add_css(self.resource_string("static/css/superset.css")) + + # Add i18n js + statici18n_js_url = self._get_statici18n_js_url() + if statici18n_js_url: + frag.add_javascript_url( + self.runtime.local_resource_url(self, statici18n_js_url) + ) + + frag.add_javascript(self.resource_string("static/js/superset_edit.js")) + frag.initialize_js("SupersetXBlock") + return frag + + @XBlock.json_handler + def studio_submit(self, data, suffix=""): # pylint: disable=unused-argument + """ + Save studio updates. + """ + self.display_name = data.get("display_name") + self.dashboard_uuid = data.get("dashboard_uuid") + filters = data.get("filters") + self.filters = [] + if filters: + for rlsf in filters.split(";"): + rlsf = rlsf.strip() + self.filters.append(rlsf) + + @staticmethod + def get_fullname(user) -> Tuple[str, str]: + """ + Return the full name of the user. + + args: + user: The user to get the fullname + + returns: + A tuple containing the first name and last name of the user + """ + first_name, last_name = "", "" + + if user.full_name: + fullname = user.full_name.split(" ", 1) + first_name = fullname[0] + + if fullname[1:]: + last_name = fullname[1] + + return first_name, last_name + + @staticmethod + def workbench_scenarios(): + """Return a canned scenario for display in the workbench.""" + return [ + ( + "SupersetXBlock", + """ + """, + ), + ( + "Multiple SupersetXBlock", + """ + + + + + """, + ), + ] + + @staticmethod + def _get_statici18n_js_url(): + """ + Return the Javascript translation file for the currently selected language, if any. + + Defaults to English if available. + """ + locale_code = translation.get_language() + if locale_code is None: + return None + text_js = "public/js/translations/{locale_code}/text.js" + lang_code = locale_code.split("-")[0] + for code in (locale_code, lang_code, "en"): + if pkg_resources.resource_exists( + loader.module_name, text_js.format(locale_code=code) + ): + return text_js.format(locale_code=code) + return None + + @staticmethod + def get_dummy(): + """ + Return dummy method to generate initial i18n. + """ + return translation.gettext_noop("Dummy") diff --git a/platform_plugin_aspects/utils.py b/platform_plugin_aspects/utils.py index d17c767..9bcd6d6 100644 --- a/platform_plugin_aspects/utils.py +++ b/platform_plugin_aspects/utils.py @@ -16,6 +16,60 @@ os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" +def _(text): + """ + Define a dummy `gettext` replacement to make string extraction tools scrape strings marked for translation. + """ + return text + + +def update_context( # pylint: disable=dangerous-default-value + context, + superset_config={}, + dashboard_uuid="", + filters=[], + user=None, +): + """ + Update context with superset token and dashboard id. + + Args: + context (dict): the context for the instructor dashboard. It must include a course object + superset_config (dict): superset config. + dashboard_uuid (str): superset dashboard uuid. + filters (list): list of filters to apply to the dashboard. + user (User): user object. + """ + course = context["course"] + + if user is None: + user = get_current_user() + superset_token, dashboard_uuid = generate_guest_token( + user=user, + course=course, + superset_config=superset_config, + dashboard_uuid=dashboard_uuid, + filters=filters, + ) + + if superset_token: + context.update( + { + "superset_token": superset_token, + "dashboard_uuid": dashboard_uuid, + "superset_url": superset_config.get("service_url"), + } + ) + else: + context.update( + { + "exception": dashboard_uuid, + } + ) + + return context + + def generate_superset_context( # pylint: disable=dangerous-default-value context, dashboard_uuid="", filters=[] ): diff --git a/platform_plugin_aspects/xblock.py b/platform_plugin_aspects/xblock.py new file mode 100644 index 0000000..4ddd172 --- /dev/null +++ b/platform_plugin_aspects/xblock.py @@ -0,0 +1,311 @@ +"""XBlock to embed a Superset dashboards in Open edX.""" +from __future__ import annotations + +import logging +from typing import Tuple + +import pkg_resources +from django.conf import settings +from django.utils import translation +from web_fragments.fragment import Fragment +from xblock.core import XBlock +from xblock.fields import List, Scope, String +from xblockutils.resources import ResourceLoader + +from .utils import _, update_context + +log = logging.getLogger(__name__) +loader = ResourceLoader(__name__) + + +@XBlock.wants("user") +@XBlock.needs("i18n") +class SupersetXBlock(XBlock): + """ + Superset XBlock provides a way to embed dashboards from Superset in a course. + """ + + display_name = String( + display_name=_("Display name"), + help=_("Display name"), + default="Superset Dashboard", + scope=Scope.settings, + ) + + superset_url = String( + display_name=_("Superset URL"), + help=_("Superset URL to embed the dashboard."), + default="", + scope=Scope.settings, + ) + + superset_username = String( + display_name=_("Superset Username"), + help=_("Superset Username"), + default="", + scope=Scope.settings, + ) + + superset_password = String( + display_name=_("Superset Password"), + help=_("Superset Password"), + default="", + scope=Scope.settings, + ) + + dashboard_uuid = String( + display_name=_("Dashboard UUID"), + help=_( + "The ID of the dashboard to embed. Available in the Superset embed dashboard UI." + ), + default="", + scope=Scope.settings, + ) + + filters = List( + display_name=_("Filters"), + help=_( + """Semicolon separated list of SQL filters to apply to the + dashboard. E.g: org='edX'; country in ('us', 'co'). + The fields used here must be available on every dataset used by the dashboard. + """ + ), + default=[], + scope=Scope.settings, + ) + + def resource_string(self, path): + """Handy helper for getting resources from our kit.""" + data = pkg_resources.resource_string(__name__, path) + return data.decode("utf8") + + def render_template(self, template_path, context=None) -> str: + """ + Render a template with the given context. + + The template is translatedaccording to the user's language. + + args: + template_path: The path to the template + context: The context to render in the template + + returns: + The rendered template + """ + return loader.render_django_template( + template_path, context, i18n_service=self.runtime.service(self, "i18n") + ) + + def user_is_staff(self, user) -> bool: + """ + Check whether the user has course staff permissions for this XBlock. + """ + return user.opt_attrs.get("edx-platform.user_is_staff") + + def is_student(self, user) -> bool: + """ + Check if the user is a student. + """ + return user.opt_attrs.get("edx-platform.user_role") == "student" + + def anonymous_user_id(self, user) -> str: + """ + Return the anonymous user ID of the user. + """ + return user.opt_attrs.get("edx-platform.anonymous_user_id") + + def student_view(self, context=None): + """ + Render the view shown to students. + """ + user_service = self.runtime.service(self, "user") + user = user_service.get_current_user() + + context.update( + { + "self": self, + "user": user, + "course": self.course_id, + "display_name": self.display_name, + } + ) + + superset_config = getattr(settings, "SUPERSET_CONFIG", {}) + + xblock_superset_config = { + "username": self.superset_username or superset_config.get("username"), + "password": self.superset_password or superset_config.get("password"), + } + + if self.superset_url: + xblock_superset_config["service_url"] = self.superset_url + + if self.dashboard_uuid: + context = update_context( + context=context, + superset_config=xblock_superset_config, + dashboard_uuid=self.dashboard_uuid, + filters=self.filters, + ) + + context["xblock_id"] = self.scope_ids.usage_id.block_id + + frag = Fragment() + frag.add_content(self.render_template("static/html/superset.html", context)) + frag.add_css(self.resource_string("static/css/superset.css")) + frag.add_javascript(self.resource_string("static/js/install_required.js")) + + # Add i18n js + statici18n_js_url = self._get_statici18n_js_url() + if statici18n_js_url: + frag.add_javascript_url( + self.runtime.local_resource_url(self, statici18n_js_url) + ) + frag.add_javascript(self.resource_string("static/js/embed_dashboard.js")) + frag.add_javascript(self.resource_string("static/js/superset.js")) + frag.initialize_js( + "SupersetXBlock", + json_args={ + "superset_url": self.superset_url or superset_config.get("host"), + "superset_username": self.superset_username, + "superset_password": self.superset_password, + "dashboard_uuid": self.dashboard_uuid, + "superset_token": context.get("superset_token"), + "xblock_id": self.scope_ids.usage_id.block_id, + }, + ) + return frag + + def studio_view(self, context=None): + """ + Render the view shown to course authors. + """ + filters = "; ".join(self.filters) + context = { + "display_name": self.display_name, + "superset_url": self.superset_url, + "superset_username": self.superset_username, + "superset_password": self.superset_password, + "dashboard_uuid": self.dashboard_uuid, + "filters": filters, + "display_name_field": self.fields[ # pylint: disable=unsubscriptable-object + "display_name" + ], + "superset_url_field": self.fields[ # pylint: disable=unsubscriptable-object + "superset_url" + ], + "superset_username_field": self.fields[ # pylint: disable=unsubscriptable-object + "superset_username" + ], + "superset_password_field": self.fields[ # pylint: disable=unsubscriptable-object + "superset_password" + ], + "dashboard_uuid_field": self.fields[ # pylint: disable=unsubscriptable-object + "dashboard_uuid" + ], + "filters_field": self.fields[ # pylint: disable=unsubscriptable-object + "filters" + ], + } + + frag = Fragment() + frag.add_content( + self.render_template("static/html/superset_edit.html", context) + ) + frag.add_css(self.resource_string("static/css/superset.css")) + + # Add i18n js + statici18n_js_url = self._get_statici18n_js_url() + if statici18n_js_url: + frag.add_javascript_url( + self.runtime.local_resource_url(self, statici18n_js_url) + ) + + frag.add_javascript(self.resource_string("static/js/superset_edit.js")) + frag.initialize_js("SupersetXBlock") + return frag + + @XBlock.json_handler + def studio_submit(self, data, suffix=""): # pylint: disable=unused-argument + """ + Save studio updates. + """ + self.display_name = data.get("display_name") + self.superset_url = data.get("superset_url") + self.superset_username = data.get("superset_username") + self.superset_password = data.get("superset_password") + self.dashboard_uuid = data.get("dashboard_uuid") + filters = data.get("filters") + self.filters = [] + if filters: + for rlsf in filters.split(";"): + rlsf = rlsf.strip() + self.filters.append(rlsf) + + @staticmethod + def get_fullname(user) -> Tuple[str, str]: + """ + Return the full name of the user. + + args: + user: The user to get the fullname + + returns: + A tuple containing the first name and last name of the user + """ + first_name, last_name = "", "" + + if user.full_name: + fullname = user.full_name.split(" ", 1) + first_name = fullname[0] + + if fullname[1:]: + last_name = fullname[1] + + return first_name, last_name + + @staticmethod + def workbench_scenarios(): + """Return a canned scenario for display in the workbench.""" + return [ + ( + "SupersetXBlock", + """ + """, + ), + ( + "Multiple SupersetXBlock", + """ + + + + + """, + ), + ] + + @staticmethod + def _get_statici18n_js_url(): + """ + Return the Javascript translation file for the currently selected language, if any. + + Defaults to English if available. + """ + locale_code = translation.get_language() + if locale_code is None: + return None + text_js = "public/js/translations/{locale_code}/text.js" + lang_code = locale_code.split("-")[0] + for code in (locale_code, lang_code, "en"): + if pkg_resources.resource_exists( + loader.module_name, text_js.format(locale_code=code) + ): + return text_js.format(locale_code=code) + return None + + @staticmethod + def get_dummy(): + """ + Return dummy method to generate initial i18n. + """ + return translation.gettext_noop("Dummy") diff --git a/requirements/base.in b/requirements/base.in index 0e0bac7..f0bb4ae 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -14,3 +14,4 @@ edx-django-utils # Django utilities, we use caching and monitoring edx-opaque-keys # Parsing library for course and usage keys django-rest-framework # REST API framework edx-toggles +XBlock diff --git a/requirements/base.txt b/requirements/base.txt index 335de9d..c6d6eba 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,18 +1,15 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade # amqp==5.2.0 # via kombu +appdirs==1.4.4 + # via fs asgiref==3.7.2 # via django -backports-zoneinfo[tzdata]==0.2.1 - # via - # celery - # django - # kombu billiard==4.2.0 # via celery celery==5.3.6 @@ -62,7 +59,7 @@ django-waffle==4.1.0 # edx-toggles djangorestframework==3.14.0 # via django-rest-framework -edx-django-utils==5.10.1 +edx-django-utils==5.11.0 # via # -r requirements/base.in # edx-toggles @@ -70,14 +67,23 @@ edx-opaque-keys==2.5.1 # via -r requirements/base.in edx-toggles==5.1.1 # via -r requirements/base.in +fs==2.4.16 + # via xblock idna==3.6 # via requests jinja2==3.1.3 # via code-annotations kombu==5.3.5 # via celery +lxml==5.1.0 + # via xblock +mako==1.3.2 + # via xblock markupsafe==2.1.5 - # via jinja2 + # via + # jinja2 + # mako + # xblock newrelic==9.7.0 # via edx-django-utils oauthlib==3.2.2 @@ -99,15 +105,20 @@ pymongo==3.13.0 pynacl==1.5.0 # via edx-django-utils python-dateutil==2.9.0.post0 - # via celery + # via + # celery + # xblock python-slugify==8.0.4 # via code-annotations pytz==2024.1 - # via djangorestframework + # via + # djangorestframework + # xblock pyyaml==6.0.1 # via # code-annotations # superset-api-client + # xblock requests==2.31.0 # via # -r requirements/base.in @@ -115,8 +126,12 @@ requests==2.31.0 # superset-api-client requests-oauthlib==1.3.1 # via superset-api-client +simplejson==3.19.2 + # via xblock six==1.16.0 - # via python-dateutil + # via + # fs + # python-dateutil sqlparse==0.4.4 # via django stevedore==5.2.0 @@ -134,9 +149,7 @@ typing-extensions==4.10.0 # edx-opaque-keys # kombu tzdata==2024.1 - # via - # backports-zoneinfo - # celery + # via celery urllib3==2.2.1 # via requests vine==5.1.0 @@ -147,4 +160,13 @@ vine==5.1.0 wcwidth==0.2.13 # via prompt-toolkit web-fragments==2.1.0 + # via + # -r requirements/base.in + # xblock +webob==1.8.7 + # via xblock +xblock==2.0.0 # via -r requirements/base.in + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/ci.txt b/requirements/ci.txt index 88a39ff..8315b53 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade @@ -32,7 +32,7 @@ tomli==2.0.1 # via # pyproject-api # tox -tox==4.13.0 +tox==4.14.1 # via -r requirements/ci.in virtualenv==20.25.1 # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index 78d779c..6220219 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade @@ -8,6 +8,10 @@ amqp==5.2.0 # via # -r requirements/quality.txt # kombu +appdirs==1.4.4 + # via + # -r requirements/quality.txt + # fs asgiref==3.7.2 # via # -r requirements/quality.txt @@ -17,12 +21,6 @@ astroid==3.1.0 # -r requirements/quality.txt # pylint # pylint-celery -backports-zoneinfo[tzdata]==0.2.1 - # via - # -r requirements/quality.txt - # celery - # django - # kombu billiard==4.2.0 # via # -r requirements/quality.txt @@ -145,7 +143,7 @@ djangorestframework==3.14.0 # -r requirements/quality.txt # django-mock-queries # django-rest-framework -edx-django-utils==5.10.1 +edx-django-utils==5.11.0 # via # -r requirements/quality.txt # edx-toggles @@ -166,11 +164,15 @@ filelock==3.13.1 # -r requirements/ci.txt # tox # virtualenv +fs==2.4.16 + # via + # -r requirements/quality.txt + # xblock idna==3.6 # via # -r requirements/quality.txt # requests -importlib-metadata==7.0.1 +importlib-metadata==7.0.2 # via # -r requirements/pip-tools.txt # build @@ -192,11 +194,20 @@ kombu==5.3.5 # -r requirements/quality.txt # celery lxml==5.1.0 - # via edx-i18n-tools + # via + # -r requirements/quality.txt + # edx-i18n-tools + # xblock +mako==1.3.2 + # via + # -r requirements/quality.txt + # xblock markupsafe==2.1.5 # via # -r requirements/quality.txt # jinja2 + # mako + # xblock mccabe==0.7.0 # via # -r requirements/quality.txt @@ -241,7 +252,7 @@ pbr==6.0.0 # via # -r requirements/quality.txt # stevedore -pip-tools==7.4.0 +pip-tools==7.4.1 # via -r requirements/pip-tools.txt platformdirs==4.2.0 # via @@ -315,7 +326,7 @@ pyproject-hooks==1.0.0 # -r requirements/pip-tools.txt # build # pip-tools -pytest==8.1.0 +pytest==8.1.1 # via # -r requirements/quality.txt # pytest-cov @@ -328,6 +339,7 @@ python-dateutil==2.9.0.post0 # via # -r requirements/quality.txt # celery + # xblock python-slugify==8.0.4 # via # -r requirements/quality.txt @@ -336,6 +348,7 @@ pytz==2024.1 # via # -r requirements/quality.txt # djangorestframework + # xblock pyyaml==6.0.1 # via # -r requirements/quality.txt @@ -343,6 +356,7 @@ pyyaml==6.0.1 # edx-i18n-tools # responses # superset-api-client + # xblock requests==2.31.0 # via # -r requirements/quality.txt @@ -355,10 +369,15 @@ requests-oauthlib==1.3.1 # superset-api-client responses==0.25.0 # via -r requirements/quality.txt +simplejson==3.19.2 + # via + # -r requirements/quality.txt + # xblock six==1.16.0 # via # -r requirements/quality.txt # edx-lint + # fs # python-dateutil snowballstemmer==2.2.0 # via @@ -398,7 +417,7 @@ tomlkit==0.12.4 # via # -r requirements/quality.txt # pylint -tox==4.13.0 +tox==4.14.1 # via -r requirements/ci.txt typing-extensions==4.10.0 # via @@ -412,7 +431,6 @@ typing-extensions==4.10.0 tzdata==2024.1 # via # -r requirements/quality.txt - # backports-zoneinfo # celery urllib3==2.2.1 # via @@ -434,11 +452,19 @@ wcwidth==0.2.13 # -r requirements/quality.txt # prompt-toolkit web-fragments==2.1.0 - # via -r requirements/quality.txt + # via + # -r requirements/quality.txt + # xblock +webob==1.8.7 + # via + # -r requirements/quality.txt + # xblock wheel==0.42.0 # via # -r requirements/pip-tools.txt # pip-tools +xblock==2.0.0 + # via -r requirements/quality.txt zipp==3.17.0 # via # -r requirements/pip-tools.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index a1a6928..33b9a8e 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -1,17 +1,21 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade # accessible-pygments==0.0.4 # via pydata-sphinx-theme -alabaster==0.7.13 +alabaster==0.7.16 # via sphinx amqp==5.2.0 # via # -r requirements/test.txt # kombu +appdirs==1.4.4 + # via + # -r requirements/test.txt + # fs asgiref==3.7.2 # via # -r requirements/test.txt @@ -20,12 +24,6 @@ babel==2.14.0 # via # pydata-sphinx-theme # sphinx -backports-zoneinfo[tzdata]==0.2.1 - # via - # -r requirements/test.txt - # celery - # django - # kombu beautifulsoup4==4.12.3 # via pydata-sphinx-theme billiard==4.2.0 @@ -115,14 +113,14 @@ djangorestframework==3.14.0 # django-rest-framework doc8==1.1.1 # via -r requirements/doc.in -docutils==0.19 +docutils==0.20.1 # via # doc8 # pydata-sphinx-theme # readme-renderer # restructuredtext-lint # sphinx -edx-django-utils==5.10.1 +edx-django-utils==5.11.0 # via # -r requirements/test.txt # edx-toggles @@ -134,20 +132,22 @@ exceptiongroup==1.2.0 # via # -r requirements/test.txt # pytest +fs==2.4.16 + # via + # -r requirements/test.txt + # xblock idna==3.6 # via # -r requirements/test.txt # requests imagesize==1.4.1 # via sphinx -importlib-metadata==7.0.1 +importlib-metadata==7.0.2 # via # build # keyring # sphinx # twine -importlib-resources==6.1.2 - # via keyring iniconfig==2.0.0 # via # -r requirements/test.txt @@ -169,12 +169,22 @@ kombu==5.3.5 # via # -r requirements/test.txt # celery +lxml==5.1.0 + # via + # -r requirements/test.txt + # xblock +mako==1.3.2 + # via + # -r requirements/test.txt + # xblock markdown-it-py==3.0.0 # via rich markupsafe==2.1.5 # via # -r requirements/test.txt # jinja2 + # mako + # xblock mdurl==0.1.2 # via markdown-it-py model-bakery==1.17.0 @@ -226,7 +236,7 @@ pycparser==2.21 # via # -r requirements/test.txt # cffi -pydata-sphinx-theme==0.14.4 +pydata-sphinx-theme==0.15.2 # via sphinx-book-theme pygments==2.17.2 # via @@ -246,7 +256,7 @@ pynacl==1.5.0 # edx-django-utils pyproject-hooks==1.0.0 # via build -pytest==8.1.0 +pytest==8.1.1 # via # -r requirements/test.txt # pytest-cov @@ -259,6 +269,7 @@ python-dateutil==2.9.0.post0 # via # -r requirements/test.txt # celery + # xblock python-slugify==8.0.4 # via # -r requirements/test.txt @@ -266,14 +277,15 @@ python-slugify==8.0.4 pytz==2024.1 # via # -r requirements/test.txt - # babel # djangorestframework + # xblock pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations # responses # superset-api-client + # xblock readme-renderer==43.0 # via twine requests==2.31.0 @@ -301,32 +313,37 @@ rich==13.7.1 # via twine secretstorage==3.3.3 # via keyring +simplejson==3.19.2 + # via + # -r requirements/test.txt + # xblock six==1.16.0 # via # -r requirements/test.txt + # fs # python-dateutil snowballstemmer==2.2.0 # via sphinx soupsieve==2.5 # via beautifulsoup4 -sphinx==6.2.1 +sphinx==7.2.6 # via # -r requirements/doc.in # pydata-sphinx-theme # sphinx-book-theme -sphinx-book-theme==1.0.1 +sphinx-book-theme==1.1.2 # via -r requirements/doc.in -sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-applehelp==1.0.8 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==1.0.6 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.0.5 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==1.0.7 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.10 # via sphinx sqlparse==0.4.4 # via @@ -362,11 +379,9 @@ typing-extensions==4.10.0 # edx-opaque-keys # kombu # pydata-sphinx-theme - # rich tzdata==2024.1 # via # -r requirements/test.txt - # backports-zoneinfo # celery urllib3==2.2.1 # via @@ -385,8 +400,17 @@ wcwidth==0.2.13 # -r requirements/test.txt # prompt-toolkit web-fragments==2.1.0 + # via + # -r requirements/test.txt + # xblock +webob==1.8.7 + # via + # -r requirements/test.txt + # xblock +xblock==2.0.0 # via -r requirements/test.txt zipp==3.17.0 - # via - # importlib-metadata - # importlib-resources + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index 8528adb..d758a9d 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade @@ -8,11 +8,11 @@ build==1.1.1 # via pip-tools click==8.1.7 # via pip-tools -importlib-metadata==7.0.1 +importlib-metadata==7.0.2 # via build packaging==23.2 # via build -pip-tools==7.4.0 +pip-tools==7.4.1 # via -r requirements/pip-tools.in pyproject-hooks==1.0.0 # via diff --git a/requirements/pip.txt b/requirements/pip.txt index 6665603..02bceaf 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade diff --git a/requirements/quality.txt b/requirements/quality.txt index 478f824..e581779 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade @@ -8,6 +8,10 @@ amqp==5.2.0 # via # -r requirements/test.txt # kombu +appdirs==1.4.4 + # via + # -r requirements/test.txt + # fs asgiref==3.7.2 # via # -r requirements/test.txt @@ -16,12 +20,6 @@ astroid==3.1.0 # via # pylint # pylint-celery -backports-zoneinfo[tzdata]==0.2.1 - # via - # -r requirements/test.txt - # celery - # django - # kombu billiard==4.2.0 # via # -r requirements/test.txt @@ -112,7 +110,7 @@ djangorestframework==3.14.0 # -r requirements/test.txt # django-mock-queries # django-rest-framework -edx-django-utils==5.10.1 +edx-django-utils==5.11.0 # via # -r requirements/test.txt # edx-toggles @@ -126,6 +124,10 @@ exceptiongroup==1.2.0 # via # -r requirements/test.txt # pytest +fs==2.4.16 + # via + # -r requirements/test.txt + # xblock idna==3.6 # via # -r requirements/test.txt @@ -146,10 +148,20 @@ kombu==5.3.5 # via # -r requirements/test.txt # celery +lxml==5.1.0 + # via + # -r requirements/test.txt + # xblock +mako==1.3.2 + # via + # -r requirements/test.txt + # xblock markupsafe==2.1.5 # via # -r requirements/test.txt # jinja2 + # mako + # xblock mccabe==0.7.0 # via pylint model-bakery==1.17.0 @@ -227,7 +239,7 @@ pynacl==1.5.0 # via # -r requirements/test.txt # edx-django-utils -pytest==8.1.0 +pytest==8.1.1 # via # -r requirements/test.txt # pytest-cov @@ -240,6 +252,7 @@ python-dateutil==2.9.0.post0 # via # -r requirements/test.txt # celery + # xblock python-slugify==8.0.4 # via # -r requirements/test.txt @@ -248,12 +261,14 @@ pytz==2024.1 # via # -r requirements/test.txt # djangorestframework + # xblock pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations # responses # superset-api-client + # xblock requests==2.31.0 # via # -r requirements/test.txt @@ -266,10 +281,15 @@ requests-oauthlib==1.3.1 # superset-api-client responses==0.25.0 # via -r requirements/test.txt +simplejson==3.19.2 + # via + # -r requirements/test.txt + # xblock six==1.16.0 # via # -r requirements/test.txt # edx-lint + # fs # python-dateutil snowballstemmer==2.2.0 # via pydocstyle @@ -310,7 +330,6 @@ typing-extensions==4.10.0 tzdata==2024.1 # via # -r requirements/test.txt - # backports-zoneinfo # celery urllib3==2.2.1 # via @@ -328,4 +347,15 @@ wcwidth==0.2.13 # -r requirements/test.txt # prompt-toolkit web-fragments==2.1.0 + # via + # -r requirements/test.txt + # xblock +webob==1.8.7 + # via + # -r requirements/test.txt + # xblock +xblock==2.0.0 # via -r requirements/test.txt + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/requirements/test.txt b/requirements/test.txt index 7e75b72..383402a 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade @@ -8,16 +8,14 @@ amqp==5.2.0 # via # -r requirements/base.txt # kombu -asgiref==3.7.2 +appdirs==1.4.4 # via # -r requirements/base.txt - # django -backports-zoneinfo[tzdata]==0.2.1 + # fs +asgiref==3.7.2 # via # -r requirements/base.txt - # celery # django - # kombu billiard==4.2.0 # via # -r requirements/base.txt @@ -96,7 +94,7 @@ djangorestframework==3.14.0 # -r requirements/base.txt # django-mock-queries # django-rest-framework -edx-django-utils==5.10.1 +edx-django-utils==5.11.0 # via # -r requirements/base.txt # edx-toggles @@ -106,6 +104,10 @@ edx-toggles==5.1.1 # via -r requirements/base.txt exceptiongroup==1.2.0 # via pytest +fs==2.4.16 + # via + # -r requirements/base.txt + # xblock idna==3.6 # via # -r requirements/base.txt @@ -120,10 +122,20 @@ kombu==5.3.5 # via # -r requirements/base.txt # celery +lxml==5.1.0 + # via + # -r requirements/base.txt + # xblock +mako==1.3.2 + # via + # -r requirements/base.txt + # xblock markupsafe==2.1.5 # via # -r requirements/base.txt # jinja2 + # mako + # xblock model-bakery==1.17.0 # via django-mock-queries newrelic==9.7.0 @@ -166,7 +178,7 @@ pynacl==1.5.0 # via # -r requirements/base.txt # edx-django-utils -pytest==8.1.0 +pytest==8.1.1 # via # pytest-cov # pytest-django @@ -178,6 +190,7 @@ python-dateutil==2.9.0.post0 # via # -r requirements/base.txt # celery + # xblock python-slugify==8.0.4 # via # -r requirements/base.txt @@ -186,12 +199,14 @@ pytz==2024.1 # via # -r requirements/base.txt # djangorestframework + # xblock pyyaml==6.0.1 # via # -r requirements/base.txt # code-annotations # responses # superset-api-client + # xblock requests==2.31.0 # via # -r requirements/base.txt @@ -204,9 +219,14 @@ requests-oauthlib==1.3.1 # superset-api-client responses==0.25.0 # via -r requirements/test.in +simplejson==3.19.2 + # via + # -r requirements/base.txt + # xblock six==1.16.0 # via # -r requirements/base.txt + # fs # python-dateutil sqlparse==0.4.4 # via @@ -237,7 +257,6 @@ typing-extensions==4.10.0 tzdata==2024.1 # via # -r requirements/base.txt - # backports-zoneinfo # celery urllib3==2.2.1 # via @@ -255,4 +274,15 @@ wcwidth==0.2.13 # -r requirements/base.txt # prompt-toolkit web-fragments==2.1.0 + # via + # -r requirements/base.txt + # xblock +webob==1.8.7 + # via + # -r requirements/base.txt + # xblock +xblock==2.0.0 # via -r requirements/base.txt + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/setup.py b/setup.py index 76bf498..9f99e4f 100755 --- a/setup.py +++ b/setup.py @@ -181,5 +181,8 @@ def is_requirement(line): "cms.djangoapp": [ "platform_plugin_aspects = platform_plugin_aspects.apps:PlatformPluginAspectsConfig", ], + "xblock.v1": [ + "superset = platform_plugin_aspects.xblock:SupersetXBlock", + ], }, )