From ea8cf452f772adbdeb8e8c8619d41733e41616d9 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 8 Mar 2024 14:25:28 +1030 Subject: [PATCH 01/11] 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 00000000..be49e0ef --- /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 00000000..49f83447 --- /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 00000000..5dac93cb --- /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 00000000..fd15621d --- /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 d17c7670..9bcd6d60 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 00000000..4ddd172e --- /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 0e0bac76..f0bb4ae6 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 335de9dc..c6d6ebac 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 88a39ff7..8315b530 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 78d779c2..6220219b 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 a1a6928d..33b9a8e8 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 8528adba..d758a9d4 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 66656035..02bceaf6 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 478f8241..e5817797 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 7e75b722..383402a2 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 76bf498d..9f99e4f3 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", + ], }, ) From 4d1de01366c1fdbf0b7292a52492da99b81f7ce5 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 7 Mar 2024 15:33:50 +1030 Subject: [PATCH 02/11] refactor: use XBlock best practices * use StudioEditableXBlockMixin * removes sensitive Superset fields from XBlock -- use SUPERSET_CONFIG * SUPERSET_CONFIG: distinguished between service_url and internal_service_url * lets XBlock default to the instructor dashboard UUID, if configured * extracts translated text * denies access to Superset for non course staff * removes unused code and files * adds tests --- .../conf/locale/en/LC_MESSAGES/django.mo | Bin 0 -> 420 bytes .../conf/locale/en/LC_MESSAGES/django.po | 64 ++++ .../conf/locale/eo/LC_MESSAGES/django.mo | Bin 0 -> 2799 bytes .../conf/locale/eo/LC_MESSAGES/django.po | 74 +++++ platform_plugin_aspects/extensions/filters.py | 23 +- platform_plugin_aspects/settings/common.py | 3 +- .../settings/tests/test_settings.py | 11 +- .../static/html/superset_edit.html | 96 ------ .../static/html/superset_student.html | 5 + .../static/js/superset_edit.js | 19 -- platform_plugin_aspects/superset_xblock.py | 299 ------------------ platform_plugin_aspects/tests/test_utils.py | 49 ++- platform_plugin_aspects/tests/test_xblock.py | 92 ++++++ platform_plugin_aspects/utils.py | 122 ++++--- platform_plugin_aspects/xblock.py | 216 +++---------- test_settings.py | 7 +- 16 files changed, 400 insertions(+), 680 deletions(-) create mode 100644 platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.mo create mode 100644 platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.po create mode 100644 platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.mo create mode 100644 platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.po delete mode 100644 platform_plugin_aspects/static/html/superset_edit.html create mode 100644 platform_plugin_aspects/static/html/superset_student.html delete mode 100644 platform_plugin_aspects/static/js/superset_edit.js delete mode 100644 platform_plugin_aspects/superset_xblock.py create mode 100644 platform_plugin_aspects/tests/test_xblock.py diff --git a/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.mo b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..d695284d174e273d4336e6e3aca2f6a10e3a591b GIT binary patch literal 420 zcma)%%}N6?6oo6g%F?xq2rd-8HPeE$sUxDcP_S6a=swO|M96-V$ZN?r&Njv(WN21@PLr4asc^MP(h{ARO=sin_mZv>x8x!^ zp8j5Qy@>00URS1cBbmP%A26nm*#rh#U)%BpJ&avWMd_@TgXc1edDVDR!G!!OXzA+j UV@P`=`Lj)rojgosaxxN~FJbq7u>b%7 literal 0 HcmV?d00001 diff --git a/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.po b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..7537bef2 --- /dev/null +++ b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,64 @@ +# edX translation file. +# Copyright (C) 2024 EdX +# This file is distributed under the GNU AFFERO GENERAL PUBLIC LICENSE. +# EdX Team , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: 0.1a\n" +"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" +"POT-Creation-Date: 2023-06-13 08:00+0000\n" +"PO-Revision-Date: 2023-06-13 09:00+0000\n" +"Last-Translator: \n" +"Language-Team: openedx-translation \n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/html/superset.html:9 templates/xblock/superset.html:8 +msgid "" +"Superset is not configured properly. Please contact your system " +"administrator." +msgstr "" + +#: static/html/superset_edit.html:7 xblock.py:36 xblock.py:37 +msgid "Display name" +msgstr "" + +#: static/html/superset_edit.html:20 xblock.py:43 +msgid "Dashboard UUID" +msgstr "" + +#: static/html/superset_edit.html:33 xblock.py:52 +msgid "Filters" +msgstr "" + +#: static/html/superset_edit.html:50 +msgid "Save" +msgstr "" + +#: static/html/superset_edit.html:53 +msgid "Cancel" +msgstr "" + +#: templates/xblock/superset_student.html:4 +msgid "" +"Superset is only visible to course staff and instructors. Please contact " +"your system administrator." +msgstr "" + +#: xblock.py:45 +msgid "" +"The ID of the dashboard to embed. Available in the Superset embed dashboard " +"UI." +msgstr "" + +#: xblock.py:54 +msgid "" +"List of SQL filters to apply to the\n" +" dashboard. E.g: [\"org='edX'\", \"country in ('us', 'co')\"]\n" +" The fields used here must be available on every dataset used by the dashboard.\n" +" " +msgstr "" diff --git a/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.mo b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..d38ccfbc298fb49380e4e205960486523f6cd174 GIT binary patch literal 2799 zcmcgt&x;#%6d$!(4eCJzFFm~O9@5gCG223EQ&+4_7gn}*?e2;o9%eJ&Or}g`;>;vA z2f=0uRV*yX=H#JL0woti1hWRRg8fo&PIiu7JoGQniwJ`Fc{Ayz+1*M7ahC7C$#>q5 z&-;Aed*5GPI@DC~q_B=)*;p@N4Y2t673&$S->_c9`UC5Etiw-WLB0(6GvpD-Ly*5i zz76>&WFGRZCl%!~-<1$g_|?K~6&+ep*pJeo9eJK)!pB3f#r`lnc6(7q*hq{o?&zwsOvK%^=uK4L0O6T45@u?5?`dKLogcP>nQ^gks6@@3L zQ~{nfL3&lUY<)7U=naYHr0Nzvc&`M7mM398l!xf$M1#?py~9RW09QkYEaV zBNF+6UMP_67$5+4l{^gne-ndgS;+C3OeDx5L}^VJ8lB(e1{|}}ZkDmam=s!F(3B<5 zy(;p7x?rg9f(y%avXs$g_0$zncD+EoJw=(b*-Yj{22W~H_XBlh{3QY$)vR45H1bK0|kJ57)!3s2OeEy&ctgZ`i;YhFw^3G2ke1 zoOf@B{cy{qX!9O+*^Vusk{mqtq1%X>JA>gm@3JSnUytj7P1!;05&wn_1gsPE>jZb* zoo|xzgBR?u!l;s>P1+fRT{GMYyFkRDKZJ2tOKcHzE9`U8GFVncpk!k^`NnC^5ZfRN z3bzoa31}O`1{*}1!y5>>!Pp|;!0Iy@@P4lz0~;_q4`Cd+VG;Hd5Mx*%D|X3bbYot3 z1|vS;WXyA%LR8n{Oh?GNOa{`0XMp2ge3%Dp0~PP0Ik`{c^pFChXY7eWxE&MMfKr?d z`-5h3VoYw64-H7dKIb=VO!h54AwYwE{9@QJ5fOYaWg{*KrO@SQyg#iha}n=rXx<-` zxpCwA?cgSpJrpwf#X(<~a)4Ig`%Ly&3oXjmjcmsSpQxeu, 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: 0.1a\n" +"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n" +"POT-Creation-Date: 2023-06-13 08:00+0000\n" +"PO-Revision-Date: 2023-06-13 09:00+0000\n" +"Last-Translator: \n" +"Language-Team: openedx-translation \n" +"Language: eo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/html/superset.html templates/xblock/superset.html +msgid "" +"Superset is not configured properly. Please contact your system " +"administrator." +msgstr "" +"Süpérsét ïs nöt çönfïgüréd pröpérlý. Pléäsé çöntäçt ýöür sýstém " +"ädmïnïsträtör. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" + +#: static/html/superset_edit.html xblock.py xblock.py +msgid "Display name" +msgstr "Dïspläý nämé Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" + +#: static/html/superset_edit.html xblock.py +msgid "Dashboard UUID" +msgstr "Däshßöärd ÛÛÌD Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" + +#: static/html/superset_edit.html xblock.py +msgid "Filters" +msgstr "Fïltérs Ⱡ'σяєм ιρѕυм #" + +#: static/html/superset_edit.html +msgid "Save" +msgstr "Sävé Ⱡ'σяєм ι#" + +#: static/html/superset_edit.html +msgid "Cancel" +msgstr "Çänçél Ⱡ'σяєм ιρѕυ#" + +#: templates/xblock/superset_student.html +msgid "" +"Superset is only visible to course staff and instructors. Please contact " +"your system administrator." +msgstr "" +"Süpérsét ïs önlý vïsïßlé tö çöürsé stäff änd ïnstrüçtörs. Pléäsé çöntäçt " +"ýöür sýstém ädmïnïsträtör. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" + +#: xblock.py +msgid "" +"The ID of the dashboard to embed. Available in the Superset embed dashboard " +"UI." +msgstr "" +"Thé ÌD öf thé däshßöärd tö émßéd. Àväïläßlé ïn thé Süpérsét émßéd däshßöärd " +"ÛÌ. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" + +#: xblock.py +msgid "" +"List of SQL filters to apply to the\n" +" dashboard. E.g: [\"org='edX'\", \"country in ('us', 'co')\"]\n" +" The fields used here must be available on every dataset used by the dashboard.\n" +" " +msgstr "" +"Lïst öf SQL fïltérs tö äpplý tö thé\n" +" däshßöärd. É.g: [\"örg='édX'\", \"çöüntrý ïn ('üs', 'çö')\"]\n" +" Thé fïélds üséd héré müst ßé äväïläßlé ön évérý dätäsét üséd ßý thé däshßöärd.\n" +" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє ¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαт#" diff --git a/platform_plugin_aspects/extensions/filters.py b/platform_plugin_aspects/extensions/filters.py index 60b7959f..e6ab4ec3 100644 --- a/platform_plugin_aspects/extensions/filters.py +++ b/platform_plugin_aspects/extensions/filters.py @@ -3,6 +3,7 @@ """ import pkg_resources +from crum import get_current_user from django.conf import settings from django.template import Context, Template from openedx_filters import PipelineStep @@ -21,7 +22,18 @@ class AddSupersetTab(PipelineStep): - """Add superset tab to instructor dashboard.""" + """ + Add superset tab to instructor dashboard. + + Enable in the LMS by adding this stanza to OPEN_EDX_FILTERS_CONFIG: + + "org.openedx.learning.instructor.dashboard.render.started.v1": { + "fail_silently": False, + "pipeline": [ + "platform_plugin_aspects.extensions.filters.AddSupersetTab", + ] + } + """ def run_filter( self, context, template_name @@ -37,7 +49,14 @@ def run_filter( filters = ASPECTS_SECURITY_FILTERS_FORMAT + extra_filters_format - context = generate_superset_context(context, dashboard_uuid, filters) + user = get_current_user() + + context = generate_superset_context( + context, + user, + dashboard_uuid=dashboard_uuid, + filters=filters, + ) template = Template(self.resource_string("static/html/superset.html")) html = template.render(Context(context)) diff --git a/platform_plugin_aspects/settings/common.py b/platform_plugin_aspects/settings/common.py index b8b84e2e..85453426 100644 --- a/platform_plugin_aspects/settings/common.py +++ b/platform_plugin_aspects/settings/common.py @@ -16,7 +16,8 @@ def plugin_settings(settings): """ settings.MAKO_TEMPLATE_DIRS_BASE.append(ROOT_DIRECTORY / "templates") settings.SUPERSET_CONFIG = { - "url": "http://superset.local.overhang.io:8088", + "internal_service_url": "http://superset:8088", + "service_url": "http://superset.local.edly.io:8088", "username": "superset", "password": "superset", } diff --git a/platform_plugin_aspects/settings/tests/test_settings.py b/platform_plugin_aspects/settings/tests/test_settings.py index 5b937bdf..9719f895 100644 --- a/platform_plugin_aspects/settings/tests/test_settings.py +++ b/platform_plugin_aspects/settings/tests/test_settings.py @@ -21,7 +21,8 @@ def test_common_settings(self): settings.MAKO_TEMPLATE_DIRS_BASE = [] common_settings.plugin_settings(settings) self.assertIn("MAKO_TEMPLATE_DIRS_BASE", settings.__dict__) - self.assertIn("url", settings.SUPERSET_CONFIG) + self.assertIn("internal_service_url", settings.SUPERSET_CONFIG) + self.assertIn("service_url", settings.SUPERSET_CONFIG) self.assertIn("username", settings.SUPERSET_CONFIG) self.assertIn("password", settings.SUPERSET_CONFIG) self.assertIsNotNone(settings.ASPECTS_INSTRUCTOR_DASHBOARD_UUID) @@ -40,14 +41,12 @@ def test_production_settings(self): test_timeout = 1 settings.ENV_TOKENS = { "SUPERSET_CONFIG": { - "url": "http://superset.local.overhang.io:8088", + "internal_service_url": "http://superset:8088", + "service_url": "http://superset.local.overhang.io", "username": "superset", "password": "superset", }, - "ASPECTS_INSTRUCTOR_DASHBOARD_UUID": { - "dashboard_slug": "instructor-dashboard", - "dashboard_uuid": "1d6bf904-f53f-47fd-b1c9-6cd7e284d286", - }, + "ASPECTS_INSTRUCTOR_DASHBOARD_UUID": "test-settings-dashboard-uuid", "SUPERSET_EXTRA_FILTERS_FORMAT": [], "EVENT_SINK_CLICKHOUSE_BACKEND_CONFIG": { "url": test_url, diff --git a/platform_plugin_aspects/static/html/superset_edit.html b/platform_plugin_aspects/static/html/superset_edit.html deleted file mode 100644 index be49e0ef..00000000 --- a/platform_plugin_aspects/static/html/superset_edit.html +++ /dev/null @@ -1,96 +0,0 @@ -{% load i18n %} - -
-
    -
  • -
    - - -
    - {% trans display_name_field.help %} -
  • -
  • -
    - - -
    - {% trans superset_url_field.help %} -
  • -
  • -
    - - -
    - {% trans superset_username_field.help %} -
  • -
  • -
    - - -
    - {% trans dashboard_uuid_field.help %} -
  • -
  • -
    - - -
    - {% trans dashboard_uuid_field.help %} -
  • -
  • -
    - - -
    - {% trans filters_field.help %} -
  • - -
- - -
diff --git a/platform_plugin_aspects/static/html/superset_student.html b/platform_plugin_aspects/static/html/superset_student.html new file mode 100644 index 00000000..9a73cf99 --- /dev/null +++ b/platform_plugin_aspects/static/html/superset_student.html @@ -0,0 +1,5 @@ +{% load i18n %} + diff --git a/platform_plugin_aspects/static/js/superset_edit.js b/platform_plugin_aspects/static/js/superset_edit.js deleted file mode 100644 index 5dac93cb..00000000 --- a/platform_plugin_aspects/static/js/superset_edit.js +++ /dev/null @@ -1,19 +0,0 @@ -/* 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 deleted file mode 100644 index fd15621d..00000000 --- a/platform_plugin_aspects/superset_xblock.py +++ /dev/null @@ -1,299 +0,0 @@ -"""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/tests/test_utils.py b/platform_plugin_aspects/tests/test_utils.py index 01cd44d4..607fc5a8 100644 --- a/platform_plugin_aspects/tests/test_utils.py +++ b/platform_plugin_aspects/tests/test_utils.py @@ -94,25 +94,37 @@ def test_get_ccx_courses_feature_disabled(self): self.assertEqual(list(courses), []) - @patch("platform_plugin_aspects.utils.generate_guest_token") + @patch.object( + settings, + "SUPERSET_CONFIG", + { + "internal_service_url": "http://superset:8088", + "service_url": "http://superset-dummy-url", + "username": "superset", + "password": "superset", + }, + ) + @patch("platform_plugin_aspects.utils._generate_guest_token") def test_generate_superset_context(self, mock_generate_guest_token): """ Test generate_superset_context """ course_mock = Mock() filter_mock = Mock() + user_mock = Mock() context = {"course": course_mock} mock_generate_guest_token.return_value = ("test-token", "test-dashboard-uuid") context = generate_superset_context( context, + user_mock, dashboard_uuid="test-dashboard-uuid", filters=[filter_mock], ) self.assertEqual(context["superset_token"], "test-token") self.assertEqual(context["dashboard_uuid"], "test-dashboard-uuid") - self.assertEqual(context["superset_url"], settings.SUPERSET_CONFIG.get("host")) + self.assertEqual(context["superset_url"], "http://superset-dummy-url/") self.assertNotIn("exception", context) @patch("platform_plugin_aspects.utils.SupersetClient") @@ -124,57 +136,68 @@ def test_generate_superset_context_with_superset_client_exception( """ course_mock = Mock() filter_mock = Mock() + user_mock = Mock() context = {"course": course_mock} mock_superset_client.side_effect = Exception("test-exception") context = generate_superset_context( context, + user_mock, dashboard_uuid="test-dashboard-uuid", filters=[filter_mock], ) self.assertIn("exception", context) + @patch.object( + settings, + "SUPERSET_CONFIG", + { + "internal_service_url": "http://superset:8088", + "service_url": "http://dummy-superset-url", + "username": "superset", + "password": "superset", + }, + ) @patch("platform_plugin_aspects.utils.SupersetClient") - @patch("platform_plugin_aspects.utils.get_current_user") - def test_generate_superset_context_succesful( - self, mock_get_current_user, mock_superset_client - ): + def test_generate_superset_context_succesful(self, mock_superset_client): """ Test generate_superset_context """ course_mock = Mock() filter_mock = Mock() + user_mock = Mock() + user_mock.username = "test-user" context = {"course": course_mock} response_mock = Mock(status_code=200) mock_superset_client.return_value.session.post.return_value = response_mock response_mock.json.return_value = { "token": "test-token", } - mock_get_current_user.return_value = User(username="test-user") context = generate_superset_context( context, - dashboard_uuid="test-dashboard-uuid", + user_mock, filters=[filter_mock], ) self.assertEqual(context["superset_token"], "test-token") - self.assertEqual(context["dashboard_uuid"], "test-dashboard-uuid") - self.assertEqual(context["superset_url"], settings.SUPERSET_CONFIG.get("host")) + self.assertEqual(context["dashboard_uuid"], "test-settings-dashboard-uuid") + self.assertEqual(context["superset_url"], "http://dummy-superset-url/") - @patch("platform_plugin_aspects.utils.get_current_user") - def test_generate_superset_context_with_exception(self, mock_get_current_user): + def test_generate_superset_context_with_exception(self): """ Test generate_superset_context """ course_mock = Mock() filter_mock = Mock() - mock_get_current_user.return_value = User(username="test-user") + user_mock = Mock() + user_mock.username = "test-user" context = {"course": course_mock} context = generate_superset_context( context, + user_mock, dashboard_uuid="test-dashboard-uuid", filters=[filter_mock], ) diff --git a/platform_plugin_aspects/tests/test_xblock.py b/platform_plugin_aspects/tests/test_xblock.py new file mode 100644 index 00000000..a4882e69 --- /dev/null +++ b/platform_plugin_aspects/tests/test_xblock.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +""" +Test basic SupersetXBlock display function +""" +from unittest import TestCase +from unittest.mock import Mock, patch + +from django.conf import settings +from opaque_keys.edx.locator import CourseLocator +from xblock.field_data import DictFieldData + +from ..xblock import SupersetXBlock + + +def make_an_xblock(user_role, **kwargs): + """ + Helper method that creates a new SupersetXBlock + """ + course_id = CourseLocator("foo", "bar", "baz") + mock_user = Mock( + opt_attrs={"edx-platform.user_role": user_role}, + ) + + def service(block, service): + # Mock the user service + if service == "user": + return Mock(get_current_user=Mock(return_value=mock_user)) + # Mock the i18n service + return Mock(_catalog={}) + + def local_resource_url(_self, url): + return url + + runtime = Mock( + course_id=course_id, + service=service, + local_resource_url=Mock(side_effect=local_resource_url), + ) + scope_ids = Mock() + field_data = DictFieldData(kwargs) + xblock = SupersetXBlock(runtime, field_data, scope_ids) + xblock.xmodule_runtime = runtime + return xblock + + +class TestRender(TestCase): + """ + Test the HTML rendering of the XBlock + """ + + @patch("platform_plugin_aspects.utils._generate_guest_token") + def test_render_instructor(self, mock_generate_guest_token): + """ + Ensure staff can see the Superset dashboard. + """ + mock_generate_guest_token.return_value = ("test-token", "test-dashboard-uuid") + xblock = make_an_xblock("instructor") + student_view = xblock.student_view() + html = student_view.content + self.assertIsNotNone(html) + self.assertIn("superset-embedded-container", html) + + def test_render_student(self): + """ + Ensure students see a warning message, not Superset. + """ + xblock = make_an_xblock("student") + student_view = xblock.student_view() + html = student_view.content + self.assertIsNotNone(html) + self.assertNotIn("superset-embedded-container", html) + self.assertIn("Superset is only visible to course staff and instructors", html) + + @patch("platform_plugin_aspects.xblock.pkg_resources.resource_exists") + @patch("platform_plugin_aspects.xblock.translation.get_language") + @patch("platform_plugin_aspects.utils._generate_guest_token") + def test_render_translations( + self, mock_generate_guest_token, mock_get_language, mock_resource_exists + ): + """ + Ensure translated javascript is served. + """ + mock_generate_guest_token.return_value = ("test-token", "test-dashboard-uuid") + mock_get_language.return_value = "eo" + mock_resource_exists.return_value = True + xblock = make_an_xblock("instructor") + student_view = xblock.student_view() + for resource in student_view.resources: + if resource.kind == "url": + url_resource = resource + self.assertIsNotNone(url_resource, "No 'url' resource found in fragment") + self.assertIn("eo/text.js", url_resource.data) diff --git a/platform_plugin_aspects/utils.py b/platform_plugin_aspects/utils.py index 9bcd6d60..91bef831 100644 --- a/platform_plugin_aspects/utils.py +++ b/platform_plugin_aspects/utils.py @@ -2,13 +2,15 @@ Utilities for the Aspects app. """ +from __future__ import annotations + import logging import os from importlib import import_module -from crum import get_current_user from django.conf import settings from supersetapiclient.client import SupersetClient +from xblock.reference.user_service import XBlockUser logger = logging.getLogger(__name__) @@ -23,28 +25,26 @@ def _(text): return text -def update_context( # pylint: disable=dangerous-default-value +def generate_superset_context( # pylint: disable=dangerous-default-value context, - superset_config={}, - dashboard_uuid="", + user, + dashboard_uuid=None, 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 + user (XBlockUser or User): the current user. 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"] + superset_config = settings.SUPERSET_CONFIG - if user is None: - user = get_current_user() - superset_token, dashboard_uuid = generate_guest_token( + superset_token, dashboard_uuid = _generate_guest_token( user=user, course=course, superset_config=superset_config, @@ -53,64 +53,25 @@ def update_context( # pylint: disable=dangerous-default-value ) if superset_token: + superset_url = _fix_service_url(superset_config.get("service_url")) 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=[] -): - """ - 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. - """ - course = context["course"] - user = get_current_user() - - superset_token, dashboard_uuid = generate_guest_token( - user=user, - course=course, - dashboard_uuid=dashboard_uuid, - filters=filters, - ) - - if superset_token: - context.update( - { - "superset_token": superset_token, - "dashboard_uuid": dashboard_uuid, - "superset_url": settings.SUPERSET_CONFIG.get("host"), + "superset_url": superset_url, } ) else: context.update( { - "exception": dashboard_uuid, + "exception": str(dashboard_uuid), } ) return context -def generate_guest_token(user, course, dashboard_uuid, filters): +def _generate_guest_token(user, course, superset_config, dashboard_uuid, filters): """ Generate a Superset guest token for the user. @@ -122,9 +83,9 @@ def generate_guest_token(user, course, dashboard_uuid, filters): tuple: Superset guest token and dashboard id. or None, exception if Superset is missconfigured or cannot generate guest token. """ - superset_config = settings.SUPERSET_CONFIG - - superset_internal_host = superset_config.get("service_url") + superset_internal_host = _fix_service_url( + superset_config.get("internal_service_url", superset_config.get("service_url")) + ) superset_username = superset_config.get("username") superset_password = superset_config.get("password") @@ -140,17 +101,11 @@ def generate_guest_token(user, course, dashboard_uuid, filters): formatted_filters = [filter.format(course=course, user=user) for filter in filters] + if not dashboard_uuid: + dashboard_uuid = settings.ASPECTS_INSTRUCTOR_DASHBOARD_UUID + data = { - "user": { - "username": user.username, - # We can send more info about the user to superset - # but Open edX only provides the full name. For now is not needed - # and doesn't add any value so we don't send it. - # { - # "first_name": "John", - # "last_name": "Doe", - # } - }, + "user": _superset_user_data(user), "resources": [{"type": "dashboard", "id": dashboard_uuid}], "rls": [{"clause": filter} for filter in formatted_filters], } @@ -170,6 +125,43 @@ def generate_guest_token(user, course, dashboard_uuid, filters): return None, exc +def _fix_service_url(url: str) -> str: + """ + Append a trailing slash to the given url, if missing. + + SupersetClient requires a trailing slash for service URLs. + """ + if url and url[-1] != "/": + url += "/" + return url + + +def _superset_user_data(user: XBlockUser) -> dict: + """ + Return the user properties sent to the Superset API. + """ + # We can send more info about the user to superset + # but Open edX only provides the full name. For now is not needed + # and doesn't add any value so we don't send it. + # { + # "first_name": "John", + # "last_name": "Doe", + # } + username = None + # Django User + if hasattr(user, "username"): + username = user.username + # XBlockUser + elif hasattr(user, "opt_attrs"): + username = user.opt_attrs.get("edx-platform.username") + else: + raise NotImplementedError(f"Unsupported user type {user}") + + return { + "username": username, + } + + def get_model(model_setting): """Load a model from a setting.""" MODEL_CONFIG = getattr(settings, "EVENT_SINK_CLICKHOUSE_MODEL_CONFIG", {}) diff --git a/platform_plugin_aspects/xblock.py b/platform_plugin_aspects/xblock.py index 4ddd172e..08ebdd62 100644 --- a/platform_plugin_aspects/xblock.py +++ b/platform_plugin_aspects/xblock.py @@ -1,30 +1,32 @@ -"""XBlock to embed a Superset dashboards in Open edX.""" +"""XBlock to embed 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 xblock.utils.resources import ResourceLoader +from xblock.utils.studio_editable import StudioEditableXBlockMixin -from .utils import _, update_context +from .utils import _, generate_superset_context log = logging.getLogger(__name__) loader = ResourceLoader(__name__) -@XBlock.wants("user") +@XBlock.needs("user") @XBlock.needs("i18n") -class SupersetXBlock(XBlock): +class SupersetXBlock(StudioEditableXBlockMixin, XBlock): """ - Superset XBlock provides a way to embed dashboards from Superset in a course. + XBlock provides a way to embed dashboards from Superset in a course. """ + editable_fields = ("display_name", "dashboard_uuid", "filters") + display_name = String( display_name=_("Display name"), help=_("Display name"), @@ -32,27 +34,6 @@ class SupersetXBlock(XBlock): 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=_( @@ -65,8 +46,8 @@ class SupersetXBlock(XBlock): 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'). + """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. """ ), @@ -74,11 +55,6 @@ class SupersetXBlock(XBlock): 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. @@ -96,64 +72,49 @@ def render_template(self, template_path, context=None) -> str: 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: + def user_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") + return not user or user.opt_attrs.get("edx-platform.user_role") == "student" def student_view(self, context=None): """ - Render the view shown to students. + Render the view shown to users of this XBlock. """ user_service = self.runtime.service(self, "user") user = user_service.get_current_user() + context = context or {} context.update( { - "self": self, - "user": user, - "course": self.course_id, + "course": self.runtime.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, + # Hide Superset content from non-course staff. + if self.user_is_student(user): + frag = Fragment() + frag.add_content( + self.render_template("static/html/superset_student.html", context) ) + return frag - context["xblock_id"] = self.scope_ids.usage_id.block_id + context = generate_superset_context( + context=context, + user=user, + dashboard_uuid=self.dashboard_uuid, + filters=self.filters, + ) + context["xblock_id"] = str(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")) + frag.add_content( + self.render_template("static/html/superset.html", context) + ) + frag.add_css(loader.load_unicode("static/css/superset.css")) + frag.add_javascript(loader.load_unicode("static/js/install_required.js")) # Add i18n js statici18n_js_url = self._get_statici18n_js_url() @@ -161,109 +122,19 @@ def student_view(self, context=None): 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.add_javascript(loader.load_unicode("static/js/embed_dashboard.js")) + frag.add_javascript(loader.load_unicode("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, + "dashboard_uuid": context.get("dashboard_uuid"), + "superset_url": context.get("superset_url"), "superset_token": context.get("superset_token"), - "xblock_id": self.scope_ids.usage_id.block_id, + "xblock_id": context.get("xblock_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.""" @@ -302,10 +173,3 @@ def _get_statici18n_js_url(): ): 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/test_settings.py b/test_settings.py index 9fb85b1c..f24796d6 100644 --- a/test_settings.py +++ b/test_settings.py @@ -61,10 +61,11 @@ ASPECTS_INSTRUCTOR_DASHBOARD_UUID = "test-dashboard-uuid" +SUPERSET_EXTRA_FILTERS_FORMAT = [] + SUPERSET_CONFIG = { - "url": "http://dummy-superset-url:8088", + "internal_service_url": "http://superset:8088", + "service_url": "http://dummy-superset-url", "username": "superset", "password": "superset", } - -SUPERSET_EXTRA_FILTERS_FORMAT = [] From c4766d52f51fba2a2e50004d8450dda27f2ff2bf Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Sun, 10 Mar 2024 14:59:21 +1030 Subject: [PATCH 03/11] chore: bumps version --- CHANGELOG.rst | 7 +++++++ platform_plugin_aspects/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b7d51b66..110ba82c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,13 @@ Unreleased * +0.3.0 – 2024-03-10 +****************** +Added +===== + +* Imported XBlock code from platform-plugin-superset + 0.2.0 – 2024-03-05 ****************** Added diff --git a/platform_plugin_aspects/__init__.py b/platform_plugin_aspects/__init__.py index 430734f1..37a09e39 100644 --- a/platform_plugin_aspects/__init__.py +++ b/platform_plugin_aspects/__init__.py @@ -5,6 +5,6 @@ import os from pathlib import Path -__version__ = "0.2.0" +__version__ = "0.3.0" ROOT_DIRECTORY = Path(os.path.dirname(os.path.abspath(__file__))) From fc0ce5e0e842f342de17314568114b39cefcd91c Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Tue, 12 Mar 2024 09:50:18 +1030 Subject: [PATCH 04/11] fix: rebuilt requirements with python3.8 --- requirements/base.txt | 15 +++++++++---- requirements/ci.txt | 4 ++-- requirements/dev.txt | 19 +++++++++++------ requirements/doc.txt | 43 +++++++++++++++++++++++++------------- requirements/pip-tools.txt | 6 +++--- requirements/pip.txt | 4 ++-- requirements/quality.txt | 15 +++++++++---- requirements/test.txt | 15 +++++++++---- 8 files changed, 81 insertions(+), 40 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index c6d6ebac..cb15b7b8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # make upgrade @@ -10,6 +10,11 @@ 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 @@ -84,7 +89,7 @@ markupsafe==2.1.5 # jinja2 # mako # xblock -newrelic==9.7.0 +newrelic==9.7.1 # via edx-django-utils oauthlib==3.2.2 # via requests-oauthlib @@ -124,7 +129,7 @@ requests==2.31.0 # -r requirements/base.in # requests-oauthlib # superset-api-client -requests-oauthlib==1.3.1 +requests-oauthlib==1.4.0 # via superset-api-client simplejson==3.19.2 # via xblock @@ -149,7 +154,9 @@ typing-extensions==4.10.0 # edx-opaque-keys # kombu tzdata==2024.1 - # via celery + # via + # backports-zoneinfo + # celery urllib3==2.2.1 # via requests vine==5.1.0 diff --git a/requirements/ci.txt b/requirements/ci.txt index 8315b530..9f335db1 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # make upgrade @@ -16,7 +16,7 @@ filelock==3.13.1 # via # tox # virtualenv -packaging==23.2 +packaging==24.0 # via # pyproject-api # tox diff --git a/requirements/dev.txt b/requirements/dev.txt index 6220219b..22124d89 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # make upgrade @@ -21,6 +21,12 @@ 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 @@ -101,7 +107,7 @@ coverage[toml]==7.4.3 # pytest-cov ddt==1.7.2 # via -r requirements/quality.txt -diff-cover==8.0.3 +diff-cover==7.7.0 # via -r requirements/dev.in dill==0.3.8 # via @@ -220,7 +226,7 @@ mypy-extensions==1.0.0 # via # -r requirements/quality.txt # black -newrelic==9.7.0 +newrelic==9.7.1 # via # -r requirements/quality.txt # edx-django-utils @@ -232,7 +238,7 @@ openedx-atlas==0.6.0 # via -r requirements/quality.txt openedx-filters==1.6.0 # via -r requirements/quality.txt -packaging==23.2 +packaging==24.0 # via # -r requirements/ci.txt # -r requirements/pip-tools.txt @@ -363,7 +369,7 @@ requests==2.31.0 # requests-oauthlib # responses # superset-api-client -requests-oauthlib==1.3.1 +requests-oauthlib==1.4.0 # via # -r requirements/quality.txt # superset-api-client @@ -431,6 +437,7 @@ typing-extensions==4.10.0 tzdata==2024.1 # via # -r requirements/quality.txt + # backports-zoneinfo # celery urllib3==2.2.1 # via @@ -459,7 +466,7 @@ webob==1.8.7 # via # -r requirements/quality.txt # xblock -wheel==0.42.0 +wheel==0.43.0 # via # -r requirements/pip-tools.txt # pip-tools diff --git a/requirements/doc.txt b/requirements/doc.txt index 33b9a8e8..6fe401b1 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -1,12 +1,12 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # make upgrade # accessible-pygments==0.0.4 # via pydata-sphinx-theme -alabaster==0.7.16 +alabaster==0.7.13 # via sphinx amqp==5.2.0 # via @@ -24,6 +24,12 @@ 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 @@ -113,7 +119,7 @@ djangorestframework==3.14.0 # django-rest-framework doc8==1.1.1 # via -r requirements/doc.in -docutils==0.20.1 +docutils==0.19 # via # doc8 # pydata-sphinx-theme @@ -148,6 +154,8 @@ importlib-metadata==7.0.2 # keyring # sphinx # twine +importlib-resources==6.1.3 + # via keyring iniconfig==2.0.0 # via # -r requirements/test.txt @@ -193,7 +201,7 @@ model-bakery==1.17.0 # django-mock-queries more-itertools==10.2.0 # via jaraco-classes -newrelic==9.7.0 +newrelic==9.7.1 # via # -r requirements/test.txt # edx-django-utils @@ -207,7 +215,7 @@ openedx-atlas==0.6.0 # via -r requirements/test.txt openedx-filters==1.6.0 # via -r requirements/test.txt -packaging==23.2 +packaging==24.0 # via # -r requirements/test.txt # build @@ -236,7 +244,7 @@ pycparser==2.21 # via # -r requirements/test.txt # cffi -pydata-sphinx-theme==0.15.2 +pydata-sphinx-theme==0.14.4 # via sphinx-book-theme pygments==2.17.2 # via @@ -277,6 +285,7 @@ python-slugify==8.0.4 pytz==2024.1 # via # -r requirements/test.txt + # babel # djangorestframework # xblock pyyaml==6.0.1 @@ -297,7 +306,7 @@ requests==2.31.0 # sphinx # superset-api-client # twine -requests-oauthlib==1.3.1 +requests-oauthlib==1.4.0 # via # -r requirements/test.txt # superset-api-client @@ -326,24 +335,24 @@ snowballstemmer==2.2.0 # via sphinx soupsieve==2.5 # via beautifulsoup4 -sphinx==7.2.6 +sphinx==6.2.1 # via # -r requirements/doc.in # pydata-sphinx-theme # sphinx-book-theme -sphinx-book-theme==1.1.2 +sphinx-book-theme==1.0.1 # via -r requirements/doc.in -sphinxcontrib-applehelp==1.0.8 +sphinxcontrib-applehelp==1.0.4 # via sphinx -sphinxcontrib-devhelp==1.0.6 +sphinxcontrib-devhelp==1.0.2 # via sphinx -sphinxcontrib-htmlhelp==2.0.5 +sphinxcontrib-htmlhelp==2.0.1 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.7 +sphinxcontrib-qthelp==1.0.3 # via sphinx -sphinxcontrib-serializinghtml==1.1.10 +sphinxcontrib-serializinghtml==1.1.5 # via sphinx sqlparse==0.4.4 # via @@ -379,9 +388,11 @@ 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 @@ -410,7 +421,9 @@ webob==1.8.7 xblock==2.0.0 # via -r requirements/test.txt zipp==3.17.0 - # via importlib-metadata + # via + # importlib-metadata + # importlib-resources # 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 d758a9d4..b876b7b0 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.9 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # make upgrade @@ -10,7 +10,7 @@ click==8.1.7 # via pip-tools importlib-metadata==7.0.2 # via build -packaging==23.2 +packaging==24.0 # via build pip-tools==7.4.1 # via -r requirements/pip-tools.in @@ -23,7 +23,7 @@ tomli==2.0.1 # build # pip-tools # pyproject-hooks -wheel==0.42.0 +wheel==0.43.0 # via pip-tools zipp==3.17.0 # via importlib-metadata diff --git a/requirements/pip.txt b/requirements/pip.txt index 02bceaf6..0094cc68 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1,10 +1,10 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # make upgrade # -wheel==0.42.0 +wheel==0.43.0 # via -r requirements/pip.in # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/quality.txt b/requirements/quality.txt index e5817797..db037f2a 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # make upgrade @@ -20,6 +20,12 @@ 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 @@ -170,7 +176,7 @@ model-bakery==1.17.0 # django-mock-queries mypy-extensions==1.0.0 # via black -newrelic==9.7.0 +newrelic==9.7.1 # via # -r requirements/test.txt # edx-django-utils @@ -182,7 +188,7 @@ openedx-atlas==0.6.0 # via -r requirements/test.txt openedx-filters==1.6.0 # via -r requirements/test.txt -packaging==23.2 +packaging==24.0 # via # -r requirements/test.txt # black @@ -275,7 +281,7 @@ requests==2.31.0 # requests-oauthlib # responses # superset-api-client -requests-oauthlib==1.3.1 +requests-oauthlib==1.4.0 # via # -r requirements/test.txt # superset-api-client @@ -330,6 +336,7 @@ typing-extensions==4.10.0 tzdata==2024.1 # via # -r requirements/test.txt + # backports-zoneinfo # celery urllib3==2.2.1 # via diff --git a/requirements/test.txt b/requirements/test.txt index 383402a2..9cff756d 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # make upgrade @@ -16,6 +16,12 @@ asgiref==3.7.2 # via # -r requirements/base.txt # django +backports-zoneinfo[tzdata]==0.2.1 + # via + # -r requirements/base.txt + # celery + # django + # kombu billiard==4.2.0 # via # -r requirements/base.txt @@ -138,7 +144,7 @@ markupsafe==2.1.5 # xblock model-bakery==1.17.0 # via django-mock-queries -newrelic==9.7.0 +newrelic==9.7.1 # via # -r requirements/base.txt # edx-django-utils @@ -150,7 +156,7 @@ openedx-atlas==0.6.0 # via -r requirements/base.txt openedx-filters==1.6.0 # via -r requirements/base.txt -packaging==23.2 +packaging==24.0 # via pytest pbr==6.0.0 # via @@ -213,7 +219,7 @@ requests==2.31.0 # requests-oauthlib # responses # superset-api-client -requests-oauthlib==1.3.1 +requests-oauthlib==1.4.0 # via # -r requirements/base.txt # superset-api-client @@ -257,6 +263,7 @@ typing-extensions==4.10.0 tzdata==2024.1 # via # -r requirements/base.txt + # backports-zoneinfo # celery urllib3==2.2.1 # via From e41daae72d6c0eebe97ca1454729a338d16f17b1 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Tue, 12 Mar 2024 09:51:54 +1030 Subject: [PATCH 05/11] fix: extracted display_name default for translation --- .../conf/locale/en/LC_MESSAGES/django.po | 38 ++++++++--------- .../conf/locale/eo/LC_MESSAGES/django.mo | Bin 2799 -> 2811 bytes .../conf/locale/eo/LC_MESSAGES/django.po | 40 ++++++++---------- platform_plugin_aspects/xblock.py | 2 +- 4 files changed, 36 insertions(+), 44 deletions(-) diff --git a/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.po b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.po index 7537bef2..a8a893b2 100644 --- a/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.po +++ b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.po @@ -17,45 +17,41 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: static/html/superset.html:9 templates/xblock/superset.html:8 +#: static/html/superset.html:9 msgid "" "Superset is not configured properly. Please contact your system " "administrator." msgstr "" -#: static/html/superset_edit.html:7 xblock.py:36 xblock.py:37 -msgid "Display name" -msgstr "" - -#: static/html/superset_edit.html:20 xblock.py:43 -msgid "Dashboard UUID" -msgstr "" - -#: static/html/superset_edit.html:33 xblock.py:52 -msgid "Filters" +#: static/html/superset_student.html:4 +msgid "" +"Superset is only visible to course staff and instructors. Please contact " +"your system administrator." msgstr "" -#: static/html/superset_edit.html:50 -msgid "Save" +#: xblock.py:31 xblock.py:32 +msgid "Display name" msgstr "" -#: static/html/superset_edit.html:53 -msgid "Cancel" +#: xblock.py:33 +msgid "Superset Dashboard" msgstr "" -#: templates/xblock/superset_student.html:4 -msgid "" -"Superset is only visible to course staff and instructors. Please contact " -"your system administrator." +#: xblock.py:38 +msgid "Dashboard UUID" msgstr "" -#: xblock.py:45 +#: xblock.py:40 msgid "" "The ID of the dashboard to embed. Available in the Superset embed dashboard " "UI." msgstr "" -#: xblock.py:54 +#: xblock.py:47 +msgid "Filters" +msgstr "" + +#: xblock.py:49 msgid "" "List of SQL filters to apply to the\n" " dashboard. E.g: [\"org='edX'\", \"country in ('us', 'co')\"]\n" diff --git a/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.mo b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.mo index d38ccfbc298fb49380e4e205960486523f6cd174..3f6a630b8e6ff9595b5dd652902e48b30b6a6b2e 100644 GIT binary patch delta 297 zcmaDa`dhUAo)F7a1|Z-BVi_P#0b*VtUIWA+@BoPUfcPO0^8oP&AZ7>Rzd(Ewh*=pK z7=(cIeIV@zq(1@aWFYOx#K7PWq?ZEeETGs|Angd`C$KOuSOV!qKw6oBA(7z@kWtGF z6k&xJP{+Z*uocKX3#9h}>1LqIb^vK7E(QjmIbZ-(%D@I?1GO^%%?3LPs0Hj1s1yUp z*oEI21xpK3i;7cA6kHOEGm`QXi&8fKV$5gS+|8oEBEF~K@XDg%!z+QZhnEy*9G-u8 W+uXOnKU*4 delta 320 zcmew@`d+mDo)F7a1|Z-9Vi_RL0b*Vt-UGxS@BxU~fcPU2^8xV>Am#yLc19ou(jq|m zCXki^(kwvw8<6$`(!YUpGLZISVqowG((8b97EtUTkX8ipGl7a785rsrx`7N!pukxm ztqi2uSs55=nHd2gj6h8;lK4CrE@d0+r?APfM_X8>vk zI}WH6>@lEL2B2|Z2QoM(<|U`*Ft{WZXC&oMeEyR;u`G4-N5*_6uEXtzm*gE@et2ch rWI;ADM&-@%EQ%~5OUe$fRCu&;f%^I8i~Sd;T-c* Date: Tue, 12 Mar 2024 09:59:11 +1030 Subject: [PATCH 06/11] fix: improve test coverage % and fix formatting --- platform_plugin_aspects/tests/test_utils.py | 2 +- platform_plugin_aspects/tests/test_xblock.py | 17 +++++++++++++++++ platform_plugin_aspects/xblock.py | 4 +--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/platform_plugin_aspects/tests/test_utils.py b/platform_plugin_aspects/tests/test_utils.py index 607fc5a8..8356757e 100644 --- a/platform_plugin_aspects/tests/test_utils.py +++ b/platform_plugin_aspects/tests/test_utils.py @@ -99,7 +99,7 @@ def test_get_ccx_courses_feature_disabled(self): "SUPERSET_CONFIG", { "internal_service_url": "http://superset:8088", - "service_url": "http://superset-dummy-url", + "service_url": "http://superset-dummy-url/", "username": "superset", "password": "superset", }, diff --git a/platform_plugin_aspects/tests/test_xblock.py b/platform_plugin_aspects/tests/test_xblock.py index a4882e69..e38c1a17 100644 --- a/platform_plugin_aspects/tests/test_xblock.py +++ b/platform_plugin_aspects/tests/test_xblock.py @@ -90,3 +90,20 @@ def test_render_translations( url_resource = resource self.assertIsNotNone(url_resource, "No 'url' resource found in fragment") self.assertIn("eo/text.js", url_resource.data) + + @patch("platform_plugin_aspects.xblock.translation.get_language") + @patch("platform_plugin_aspects.utils._generate_guest_token") + def test_render_no_translations( + self, + mock_generate_guest_token, + mock_get_language, + ): + """ + Ensure translated javascript is served. + """ + mock_generate_guest_token.return_value = ("test-token", "test-dashboard-uuid") + mock_get_language.return_value = None + xblock = make_an_xblock("instructor") + student_view = xblock.student_view() + for resource in student_view.resources: + assert resource.kind != "url" diff --git a/platform_plugin_aspects/xblock.py b/platform_plugin_aspects/xblock.py index fa560ece..73e334c1 100644 --- a/platform_plugin_aspects/xblock.py +++ b/platform_plugin_aspects/xblock.py @@ -110,9 +110,7 @@ def student_view(self, context=None): context["xblock_id"] = str(self.scope_ids.usage_id.block_id) frag = Fragment() - frag.add_content( - self.render_template("static/html/superset.html", context) - ) + frag.add_content(self.render_template("static/html/superset.html", context)) frag.add_css(loader.load_unicode("static/css/superset.css")) frag.add_javascript(loader.load_unicode("static/js/install_required.js")) From c09d158eb0d794c0fbe2b339fc5d985765fc196d Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Tue, 12 Mar 2024 10:13:16 +1030 Subject: [PATCH 07/11] fix: docs lint --- platform_plugin_aspects/extensions/filters.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/platform_plugin_aspects/extensions/filters.py b/platform_plugin_aspects/extensions/filters.py index e6ab4ec3..8187601d 100644 --- a/platform_plugin_aspects/extensions/filters.py +++ b/platform_plugin_aspects/extensions/filters.py @@ -27,12 +27,12 @@ class AddSupersetTab(PipelineStep): Enable in the LMS by adding this stanza to OPEN_EDX_FILTERS_CONFIG: - "org.openedx.learning.instructor.dashboard.render.started.v1": { - "fail_silently": False, - "pipeline": [ - "platform_plugin_aspects.extensions.filters.AddSupersetTab", - ] - } + "org.openedx.learning.instructor.dashboard.render.started.v1": { + "fail_silently": False, + "pipeline": [ + "platform_plugin_aspects.extensions.filters.AddSupersetTab", + ] + } """ def run_filter( From a82d845f81e5fa08f4fd2f7cc098a700563b094f Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Tue, 12 Mar 2024 10:16:46 +1030 Subject: [PATCH 08/11] fix: linting Couldn't find an acceptable indentation level for this comment, so just removed it. --- platform_plugin_aspects/extensions/filters.py | 9 --------- platform_plugin_aspects/tests/test_xblock.py | 3 +-- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/platform_plugin_aspects/extensions/filters.py b/platform_plugin_aspects/extensions/filters.py index 8187601d..3e819767 100644 --- a/platform_plugin_aspects/extensions/filters.py +++ b/platform_plugin_aspects/extensions/filters.py @@ -24,15 +24,6 @@ class AddSupersetTab(PipelineStep): """ Add superset tab to instructor dashboard. - - Enable in the LMS by adding this stanza to OPEN_EDX_FILTERS_CONFIG: - - "org.openedx.learning.instructor.dashboard.render.started.v1": { - "fail_silently": False, - "pipeline": [ - "platform_plugin_aspects.extensions.filters.AddSupersetTab", - ] - } """ def run_filter( diff --git a/platform_plugin_aspects/tests/test_xblock.py b/platform_plugin_aspects/tests/test_xblock.py index e38c1a17..a4673e0e 100644 --- a/platform_plugin_aspects/tests/test_xblock.py +++ b/platform_plugin_aspects/tests/test_xblock.py @@ -5,7 +5,6 @@ from unittest import TestCase from unittest.mock import Mock, patch -from django.conf import settings from opaque_keys.edx.locator import CourseLocator from xblock.field_data import DictFieldData @@ -21,7 +20,7 @@ def make_an_xblock(user_role, **kwargs): opt_attrs={"edx-platform.user_role": user_role}, ) - def service(block, service): + def service(block, service): # pylint: disable=unused-argument # Mock the user service if service == "user": return Mock(get_current_user=Mock(return_value=mock_user)) From c1c4e11eee143e54bf1cf9c1d033c3c50a5ff105 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Tue, 12 Mar 2024 10:51:19 +1030 Subject: [PATCH 09/11] fix: improve test coverage --- platform_plugin_aspects/tests/test_xblock.py | 20 ++++++++++++++++---- platform_plugin_aspects/utils.py | 6 ++---- platform_plugin_aspects/xblock.py | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/platform_plugin_aspects/tests/test_xblock.py b/platform_plugin_aspects/tests/test_xblock.py index a4673e0e..7c17bd94 100644 --- a/platform_plugin_aspects/tests/test_xblock.py +++ b/platform_plugin_aspects/tests/test_xblock.py @@ -7,6 +7,7 @@ from opaque_keys.edx.locator import CourseLocator from xblock.field_data import DictFieldData +from xblock.reference.user_service import XBlockUser from ..xblock import SupersetXBlock @@ -17,7 +18,11 @@ def make_an_xblock(user_role, **kwargs): """ course_id = CourseLocator("foo", "bar", "baz") mock_user = Mock( - opt_attrs={"edx-platform.user_role": user_role}, + spec=XBlockUser, + opt_attrs={ + "edx-platform.username": user_role, + "edx-platform.user_role": user_role, + }, ) def service(block, service): # pylint: disable=unused-argument @@ -47,14 +52,21 @@ class TestRender(TestCase): Test the HTML rendering of the XBlock """ - @patch("platform_plugin_aspects.utils._generate_guest_token") - def test_render_instructor(self, mock_generate_guest_token): + @patch("platform_plugin_aspects.utils.SupersetClient") + def test_render_instructor(self, mock_superset_client): """ Ensure staff can see the Superset dashboard. """ - mock_generate_guest_token.return_value = ("test-token", "test-dashboard-uuid") + mock_superset_client.return_value = Mock( + session=Mock( + post=Mock( + return_value=Mock(json=Mock(return_value={"token": "test_token"})) + ) + ) + ) xblock = make_an_xblock("instructor") student_view = xblock.student_view() + mock_superset_client.assert_called_once() html = student_view.content self.assertIsNotNone(html) self.assertIn("superset-embedded-container", html) diff --git a/platform_plugin_aspects/utils.py b/platform_plugin_aspects/utils.py index 91bef831..b31e9ff9 100644 --- a/platform_plugin_aspects/utils.py +++ b/platform_plugin_aspects/utils.py @@ -151,11 +151,9 @@ def _superset_user_data(user: XBlockUser) -> dict: # Django User if hasattr(user, "username"): username = user.username - # XBlockUser - elif hasattr(user, "opt_attrs"): - username = user.opt_attrs.get("edx-platform.username") else: - raise NotImplementedError(f"Unsupported user type {user}") + assert isinstance(user, XBlockUser) + username = user.opt_attrs.get("edx-platform.username") return { "username": username, diff --git a/platform_plugin_aspects/xblock.py b/platform_plugin_aspects/xblock.py index 73e334c1..55006c19 100644 --- a/platform_plugin_aspects/xblock.py +++ b/platform_plugin_aspects/xblock.py @@ -134,7 +134,7 @@ def student_view(self, context=None): return frag @staticmethod - def workbench_scenarios(): + def workbench_scenarios(): # pragma: no cover """Return a canned scenario for display in the workbench.""" return [ ( From 4e40f21054b4fa86764dcef6d8bbd51451ae0408 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Wed, 13 Mar 2024 10:52:04 +1030 Subject: [PATCH 10/11] feat: adds "Go to Superset" link to Instructor Dashboard plugin Because the plugin's template is a mako template, also had to configure i18n_tools to extract translations from mako templates. --- .../conf/locale/babel_mako.cfg | 10 ++++++++ .../conf/locale/en/LC_MESSAGES/mako.mo | Bin 0 -> 366 bytes .../conf/locale/en/LC_MESSAGES/mako.po | 23 ++++++++++++++++++ .../conf/locale/eo/LC_MESSAGES/mako.mo | Bin 0 -> 525 bytes .../conf/locale/eo/LC_MESSAGES/mako.po | 23 ++++++++++++++++++ platform_plugin_aspects/extensions/filters.py | 1 + .../extensions/tests/test_filters.py | 2 ++ .../instructor_dashboard/aspects.html | 15 +++++++++++- 8 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 platform_plugin_aspects/conf/locale/babel_mako.cfg create mode 100644 platform_plugin_aspects/conf/locale/en/LC_MESSAGES/mako.mo create mode 100644 platform_plugin_aspects/conf/locale/en/LC_MESSAGES/mako.po create mode 100644 platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/mako.mo create mode 100644 platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/mako.po diff --git a/platform_plugin_aspects/conf/locale/babel_mako.cfg b/platform_plugin_aspects/conf/locale/babel_mako.cfg new file mode 100644 index 00000000..0b6c2d67 --- /dev/null +++ b/platform_plugin_aspects/conf/locale/babel_mako.cfg @@ -0,0 +1,10 @@ +# Extraction from Mako templates. +# Mako files can appear in a number of different places. Some we want to +# extract strings from, some we don't. +# +# Extract from these directory trees: +# +# templates/instructor_dashboard/ +# +[mako: templates/**/*.html] +input_encoding = utf-8 diff --git a/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/mako.mo b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/mako.mo new file mode 100644 index 0000000000000000000000000000000000000000..bceb9a73e7a56ff2d429409a7968b87973e6d932 GIT binary patch literal 366 zcmY+9F;BxV5QU3i%E-*%felQD?+bsH0c?4(N5O0c=@H5diEa$H1z2!D^?!eyc2 zNuO@Gr@QxlPEWo&s3YVIIY*9>OQh8R>ErSiXY2XX)Vp_B1)8qu0(@hfWn^09&tg)N zg(zleo<#+0oexwsm08pAu4-uM7!iu6a>V1f5VKh{&1nIjraeGo9e|M+!3(N#8gufL zGnHPv=9!43ZUQa6woR>r^Nc*sr6d_wg53YRI7F+hcDe#ug8pDc@+_J21btGDYSVST z8b&Hr;t;&oy^bc%2C(>Jzr_#%zQWbE*2dnE)tmMWgoj<&(80miX4!zJ!mixf*ou+C a(u61h3m%JL<2@r|y@Z, 2024. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-03-13 11:48+1030\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: templates/instructor_dashboard/aspects.html:17 +msgid "Go to Superset" +msgstr "" + diff --git a/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/mako.mo b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/mako.mo new file mode 100644 index 0000000000000000000000000000000000000000..4a4d47105c2194d30dd2a55e7f662c43f1385be1 GIT binary patch literal 525 zcmY+Au}<7T5Qa?zjf)hiQw*X=BC?kpN`dyc1j8-Hk$pDyfs|BP?iOQYcCGa~3S~f% z($VDlw3KNgA<>;u!r>Q4!ILmfiW9%~)6A}Z&Cbl{Q`p+|eds#@gS7`0-=bBf6;A~j2!2e#clz$G^*Ym8UbUO1; z5Xug`fX$|8w_e+uN*W>Y!;=C#GUAJQRpdOA9T*<+X8vwv4#?`v*EDt@G+}b6Mwv`0 ztnO#Vj`B+LoNOt5TvCw5ziZiD>A?TA`&`0e7uqj6wkxF2xB%SL355;5FLGFEwclVj zmOJ?k21zDY#pFVllM4fPzkWWOp4I2|kJ~G_xtxBj$J1|^-hDrvp8Y}UbEwBRm-XqA Fbq|V=qjmrQ literal 0 HcmV?d00001 diff --git a/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/mako.po b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/mako.po new file mode 100644 index 00000000..069b06fa --- /dev/null +++ b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/mako.po @@ -0,0 +1,23 @@ +# Translations template for PROJECT. +# Copyright (C) 2024 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-03-13 11:48+1030\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: eo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Generated-By: Babel 2.14.0\n" + +#: templates/instructor_dashboard/aspects.html +msgid "Go to Superset" +msgstr "Gö tö Süpérsét Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" diff --git a/platform_plugin_aspects/extensions/filters.py b/platform_plugin_aspects/extensions/filters.py index 3e819767..64474060 100644 --- a/platform_plugin_aspects/extensions/filters.py +++ b/platform_plugin_aspects/extensions/filters.py @@ -59,6 +59,7 @@ def run_filter( "section_key": BLOCK_CATEGORY, "section_display_name": BLOCK_CATEGORY.title(), "course_id": str(course.id), + "superset_url": str(context.get("superset_url")), "template_path_prefix": TEMPLATE_ABSOLUTE_PATH, } context["sections"].append(section_data) diff --git a/platform_plugin_aspects/extensions/tests/test_filters.py b/platform_plugin_aspects/extensions/tests/test_filters.py index 1359e860..10fef067 100644 --- a/platform_plugin_aspects/extensions/tests/test_filters.py +++ b/platform_plugin_aspects/extensions/tests/test_filters.py @@ -31,6 +31,7 @@ def test_run_filter(self, mock_generate_superset_context): """ mock_generate_superset_context.return_value = { "sections": [], + "superset_url": "http://superset.testing", } context = self.filter.run_filter(self.context, self.template_name) @@ -40,6 +41,7 @@ def test_run_filter(self, mock_generate_superset_context): "course_id": str(self.context["course"].id), "section_key": BLOCK_CATEGORY, "section_display_name": BLOCK_CATEGORY.title(), + "superset_url": "http://superset.testing", "template_path_prefix": "/instructor_dashboard/", }, context["context"]["sections"][0], diff --git a/platform_plugin_aspects/templates/instructor_dashboard/aspects.html b/platform_plugin_aspects/templates/instructor_dashboard/aspects.html index 1e8020e9..50da207c 100644 --- a/platform_plugin_aspects/templates/instructor_dashboard/aspects.html +++ b/platform_plugin_aspects/templates/instructor_dashboard/aspects.html @@ -1,8 +1,21 @@ +## mako + <%page args="section_data" expression_filter="h"/> -<%! from openedx.core.djangolib.markup import HTML %> +<%! +from django.utils.translation import gettext as _ +from openedx.core.djangolib.markup import HTML +%> <%include file="/courseware/xqa_interface.html/"/> +<% +superset_url = section_data.get("superset_url") +%>
+ % if superset_url: + + % endif ${HTML(section_data['fragment'].body_html())}
From 8498b8823c878b2a92582bea9c7cf17380b856c4 Mon Sep 17 00:00:00 2001 From: Cristhian Garcia Date: Wed, 13 Mar 2024 14:30:57 -0500 Subject: [PATCH 11/11] fix: prefer service_url over internal_service_url --- platform_plugin_aspects/settings/common.py | 1 - platform_plugin_aspects/settings/tests/test_settings.py | 2 +- platform_plugin_aspects/utils.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/platform_plugin_aspects/settings/common.py b/platform_plugin_aspects/settings/common.py index 85453426..d76fbb4e 100644 --- a/platform_plugin_aspects/settings/common.py +++ b/platform_plugin_aspects/settings/common.py @@ -17,7 +17,6 @@ def plugin_settings(settings): settings.MAKO_TEMPLATE_DIRS_BASE.append(ROOT_DIRECTORY / "templates") settings.SUPERSET_CONFIG = { "internal_service_url": "http://superset:8088", - "service_url": "http://superset.local.edly.io:8088", "username": "superset", "password": "superset", } diff --git a/platform_plugin_aspects/settings/tests/test_settings.py b/platform_plugin_aspects/settings/tests/test_settings.py index 9719f895..e821f235 100644 --- a/platform_plugin_aspects/settings/tests/test_settings.py +++ b/platform_plugin_aspects/settings/tests/test_settings.py @@ -22,7 +22,7 @@ def test_common_settings(self): common_settings.plugin_settings(settings) self.assertIn("MAKO_TEMPLATE_DIRS_BASE", settings.__dict__) self.assertIn("internal_service_url", settings.SUPERSET_CONFIG) - self.assertIn("service_url", settings.SUPERSET_CONFIG) + self.assertNotIn("service_url", settings.SUPERSET_CONFIG) self.assertIn("username", settings.SUPERSET_CONFIG) self.assertIn("password", settings.SUPERSET_CONFIG) self.assertIsNotNone(settings.ASPECTS_INSTRUCTOR_DASHBOARD_UUID) diff --git a/platform_plugin_aspects/utils.py b/platform_plugin_aspects/utils.py index b31e9ff9..d9310934 100644 --- a/platform_plugin_aspects/utils.py +++ b/platform_plugin_aspects/utils.py @@ -84,7 +84,7 @@ def _generate_guest_token(user, course, superset_config, dashboard_uuid, filters or None, exception if Superset is missconfigured or cannot generate guest token. """ superset_internal_host = _fix_service_url( - superset_config.get("internal_service_url", superset_config.get("service_url")) + superset_config.get("service_url", superset_config.get("internal_service_url")) ) superset_username = superset_config.get("username") superset_password = superset_config.get("password")