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__))) 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/django.mo b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 00000000..d695284d Binary files /dev/null and b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.mo differ 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..a8a893b2 --- /dev/null +++ b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,60 @@ +# 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 +msgid "" +"Superset is not configured properly. Please contact your system " +"administrator." +msgstr "" + +#: static/html/superset_student.html:4 +msgid "" +"Superset is only visible to course staff and instructors. Please contact " +"your system administrator." +msgstr "" + +#: xblock.py:31 xblock.py:32 +msgid "Display name" +msgstr "" + +#: xblock.py:33 +msgid "Superset Dashboard" +msgstr "" + +#: xblock.py:38 +msgid "Dashboard UUID" +msgstr "" + +#: xblock.py:40 +msgid "" +"The ID of the dashboard to embed. Available in the Superset embed dashboard " +"UI." +msgstr "" + +#: 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" +" The fields used here must be available on every dataset used by the dashboard.\n" +" " +msgstr "" 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 00000000..bceb9a73 Binary files /dev/null and b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/mako.mo differ diff --git a/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/mako.po b/platform_plugin_aspects/conf/locale/en/LC_MESSAGES/mako.po new file mode 100644 index 00000000..26884f8f --- /dev/null +++ b/platform_plugin_aspects/conf/locale/en/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. +# +#, 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/django.mo b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.mo new file mode 100644 index 00000000..3f6a630b Binary files /dev/null and b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.mo differ diff --git a/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.po b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.po new file mode 100644 index 00000000..b25099b0 --- /dev/null +++ b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/django.po @@ -0,0 +1,70 @@ +# 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: 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 +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_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 xblock.py +msgid "Display name" +msgstr "Dïspläý nämé Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" + +#: xblock.py +msgid "Superset Dashboard" +msgstr "Süpérsét Däshßöärd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт#" + +#: xblock.py +msgid "Dashboard UUID" +msgstr "Däshßöärd ÛÛÌD Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт#" + +#: 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 "Filters" +msgstr "Fïltérs Ⱡ'σяєм ιρѕυм #" + +#: 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/conf/locale/eo/LC_MESSAGES/mako.mo b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/mako.mo new file mode 100644 index 00000000..4a4d4710 Binary files /dev/null and b/platform_plugin_aspects/conf/locale/eo/LC_MESSAGES/mako.mo differ 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 60b7959f..64474060 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,9 @@ class AddSupersetTab(PipelineStep): - """Add superset tab to instructor dashboard.""" + """ + Add superset tab to instructor dashboard. + """ def run_filter( self, context, template_name @@ -37,7 +40,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)) @@ -49,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/settings/common.py b/platform_plugin_aspects/settings/common.py index b8b84e2e..d76fbb4e 100644 --- a/platform_plugin_aspects/settings/common.py +++ b/platform_plugin_aspects/settings/common.py @@ -16,7 +16,7 @@ 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", "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..e821f235 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.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) @@ -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_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/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/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())}
diff --git a/platform_plugin_aspects/tests/test_utils.py b/platform_plugin_aspects/tests/test_utils.py index 01cd44d4..8356757e 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..7c17bd94 --- /dev/null +++ b/platform_plugin_aspects/tests/test_xblock.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +""" +Test basic SupersetXBlock display function +""" +from unittest import TestCase +from unittest.mock import Mock, patch + +from opaque_keys.edx.locator import CourseLocator +from xblock.field_data import DictFieldData +from xblock.reference.user_service import XBlockUser + +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( + spec=XBlockUser, + opt_attrs={ + "edx-platform.username": user_role, + "edx-platform.user_role": user_role, + }, + ) + + 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)) + # 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.SupersetClient") + def test_render_instructor(self, mock_superset_client): + """ + Ensure staff can see the Superset dashboard. + """ + 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) + + 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) + + @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/utils.py b/platform_plugin_aspects/utils.py index d17c7670..d9310934 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__) @@ -16,47 +18,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 generate_superset_context( # pylint: disable=dangerous-default-value - context, dashboard_uuid="", filters=[] + context, + user, + dashboard_uuid=None, + filters=[], ): """ 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. """ course = context["course"] - user = get_current_user() + superset_config = settings.SUPERSET_CONFIG - superset_token, dashboard_uuid = generate_guest_token( + superset_token, dashboard_uuid = _generate_guest_token( user=user, course=course, + superset_config=superset_config, dashboard_uuid=dashboard_uuid, filters=filters, ) 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": 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. @@ -68,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("service_url", superset_config.get("internal_service_url")) + ) superset_username = superset_config.get("username") superset_password = superset_config.get("password") @@ -86,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], } @@ -116,6 +125,41 @@ 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 + else: + assert isinstance(user, XBlockUser) + username = user.opt_attrs.get("edx-platform.username") + + 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 new file mode 100644 index 00000000..55006c19 --- /dev/null +++ b/platform_plugin_aspects/xblock.py @@ -0,0 +1,173 @@ +"""XBlock to embed Superset dashboards in Open edX.""" + +from __future__ import annotations + +import logging + +import pkg_resources +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.studio_editable import StudioEditableXBlockMixin + +from .utils import _, generate_superset_context + +log = logging.getLogger(__name__) +loader = ResourceLoader(__name__) + + +@XBlock.needs("user") +@XBlock.needs("i18n") +class SupersetXBlock(StudioEditableXBlockMixin, XBlock): + """ + 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"), + 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=_( + """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 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_student(self, user) -> bool: + """ + Check if the user is a student. + """ + return not user or user.opt_attrs.get("edx-platform.user_role") == "student" + + 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 = context or {} + context.update( + { + "course": self.runtime.course_id, + "display_name": self.display_name, + } + ) + + # 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 = 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(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() + if statici18n_js_url: + frag.add_javascript_url( + self.runtime.local_resource_url(self, statici18n_js_url) + ) + 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={ + "dashboard_uuid": context.get("dashboard_uuid"), + "superset_url": context.get("superset_url"), + "superset_token": context.get("superset_token"), + "xblock_id": context.get("xblock_id"), + }, + ) + return frag + + @staticmethod + def workbench_scenarios(): # pragma: no cover + """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 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..cb15b7b8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,6 +6,8 @@ # amqp==5.2.0 # via kombu +appdirs==1.4.4 + # via fs asgiref==3.7.2 # via django backports-zoneinfo[tzdata]==0.2.1 @@ -62,7 +64,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,15 +72,24 @@ 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 -newrelic==9.7.0 + # via + # jinja2 + # mako + # xblock +newrelic==9.7.1 # via edx-django-utils oauthlib==3.2.2 # via requests-oauthlib @@ -99,24 +110,33 @@ 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 # 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 six==1.16.0 - # via python-dateutil + # via + # fs + # python-dateutil sqlparse==0.4.4 # via django stevedore==5.2.0 @@ -147,4 +167,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..9f335db1 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -16,7 +16,7 @@ filelock==3.13.1 # via # tox # virtualenv -packaging==23.2 +packaging==24.0 # via # pyproject-api # tox @@ -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..22124d89 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -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 @@ -103,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 @@ -145,7 +149,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 +170,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 +200,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 @@ -209,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 @@ -221,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 @@ -241,7 +258,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 +332,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 +345,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 +354,7 @@ pytz==2024.1 # via # -r requirements/quality.txt # djangorestframework + # xblock pyyaml==6.0.1 # via # -r requirements/quality.txt @@ -343,22 +362,28 @@ pyyaml==6.0.1 # edx-i18n-tools # responses # superset-api-client + # xblock requests==2.31.0 # via # -r requirements/quality.txt # requests-oauthlib # responses # superset-api-client -requests-oauthlib==1.3.1 +requests-oauthlib==1.4.0 # via # -r requirements/quality.txt # 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 +423,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 @@ -434,11 +459,19 @@ wcwidth==0.2.13 # -r requirements/quality.txt # prompt-toolkit web-fragments==2.1.0 - # via -r requirements/quality.txt -wheel==0.42.0 + # via + # -r requirements/quality.txt + # xblock +webob==1.8.7 + # via + # -r requirements/quality.txt + # xblock +wheel==0.43.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..6fe401b1 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -12,6 +12,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 @@ -122,7 +126,7 @@ docutils==0.19 # 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,19 +138,23 @@ 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 +importlib-resources==6.1.3 # via keyring iniconfig==2.0.0 # via @@ -169,12 +177,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 @@ -183,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 @@ -197,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 @@ -246,7 +264,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 +277,7 @@ python-dateutil==2.9.0.post0 # via # -r requirements/test.txt # celery + # xblock python-slugify==8.0.4 # via # -r requirements/test.txt @@ -268,12 +287,14 @@ pytz==2024.1 # -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 @@ -285,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 @@ -301,9 +322,14 @@ 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 @@ -385,8 +411,19 @@ 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 + +# 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..b876b7b0 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -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 +packaging==24.0 # via build -pip-tools==7.4.0 +pip-tools==7.4.1 # via -r requirements/pip-tools.in pyproject-hooks==1.0.0 # via @@ -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 66656035..0094cc68 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -4,7 +4,7 @@ # # 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 478f8241..db037f2a 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -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 @@ -112,7 +116,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 +130,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 +154,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 @@ -158,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 @@ -170,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 @@ -227,7 +245,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 +258,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,28 +267,35 @@ 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 # requests-oauthlib # responses # superset-api-client -requests-oauthlib==1.3.1 +requests-oauthlib==1.4.0 # via # -r requirements/test.txt # 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 @@ -328,4 +354,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..9cff756d 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -8,6 +8,10 @@ amqp==5.2.0 # via # -r requirements/base.txt # kombu +appdirs==1.4.4 + # via + # -r requirements/base.txt + # fs asgiref==3.7.2 # via # -r requirements/base.txt @@ -96,7 +100,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 +110,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,13 +128,23 @@ 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 +newrelic==9.7.1 # via # -r requirements/base.txt # edx-django-utils @@ -138,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 @@ -166,7 +184,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 +196,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,27 +205,34 @@ 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 # requests-oauthlib # responses # superset-api-client -requests-oauthlib==1.3.1 +requests-oauthlib==1.4.0 # via # -r requirements/base.txt # 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 @@ -255,4 +281,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", + ], }, ) 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 = []