diff --git a/aspects/__init__.py b/aspects/__init__.py index 1ba0e25..ca6a6cb 100644 --- a/aspects/__init__.py +++ b/aspects/__init__.py @@ -2,4 +2,9 @@ One-line description for README and other doc files. """ -__version__ = '0.1.0' +import os +from pathlib import Path + +__version__ = "0.1.0" + +ROOT_DIRECTORY = Path(os.path.dirname(os.path.abspath(__file__))) diff --git a/aspects/apps.py b/aspects/apps.py index 051e25b..327a8f0 100644 --- a/aspects/apps.py +++ b/aspects/apps.py @@ -10,4 +10,21 @@ class AspectsConfig(AppConfig): Configuration for the aspects Django application. """ - name = 'aspects' + name = "aspects" + + plugin_app = { + "settings_config": { + "lms.djangoapp": { + "common": {"relative_path": "settings.common"}, + "production": {"relative_path": "settings.production"}, + }, + "cms.djangoapp": { + "common": {"relative_path": "settings.common"}, + "production": {"relative_path": "settings.production"}, + }, + }, + } + + def ready(self): + """Load modules of Aspects.""" + from aspects.extensions import filters # pylint: disable=unused-import, import-outside-toplevel diff --git a/aspects/extensions/__init__.py b/aspects/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aspects/extensions/filters.py b/aspects/extensions/filters.py new file mode 100644 index 0000000..aa85afe --- /dev/null +++ b/aspects/extensions/filters.py @@ -0,0 +1,64 @@ +""" +Open edX Filters needed for Aspects integration. +""" + +import pkg_resources +from django.conf import settings +from django.template import Context, Template +from openedx_filters import PipelineStep +from web_fragments.fragment import Fragment + +from aspects.utils import generate_superset_context + +TEMPLATE_ABSOLUTE_PATH = "/instructor_dashboard/" +BLOCK_CATEGORY = "aspects" + +ASPECTS_SECURITY_FILTERS_FORMAT = [ + "org = '{course.org}'", + "course_name = '{course.display_name}'", + "course_run = '{course.id.run}'", +] + + +class AddSupersetTab(PipelineStep): + """Add superset tab to instructor dashboard.""" + + def run_filter( + self, context, template_name + ): # pylint: disable=arguments-differ, unused-argument + """Execute filter that modifies the instructor dashboard context. + Args: + context (dict): the context for the instructor dashboard. + _ (str): instructor dashboard template name. + """ + course = context["course"] + dashboard_uuid = settings.ASPECTS_INSTRUCTOR_DASHBOARD_UUID + extra_filters_format = settings.SUPERSET_EXTRA_FILTERS_FORMAT + + filters = ASPECTS_SECURITY_FILTERS_FORMAT + extra_filters_format + + context = generate_superset_context( + context, dashboard_uuid, filters + ) + + template = Template(self.resource_string("static/html/superset.html")) + html = template.render(Context(context)) + frag = Fragment(html) + frag.add_css(self.resource_string("static/css/superset.css")) + frag.add_javascript(self.resource_string("static/js/embed_dashboard.js")) + section_data = { + "fragment": frag, + "section_key": BLOCK_CATEGORY, + "section_display_name": BLOCK_CATEGORY.title(), + "course_id": str(course.id), + "template_path_prefix": TEMPLATE_ABSOLUTE_PATH, + } + context["sections"].append(section_data) + return { + "context": context, + } + + def resource_string(self, path): + """Handy helper for getting resources from our kit.""" + data = pkg_resources.resource_string("aspects", path) + return data.decode("utf8") diff --git a/aspects/settings/__init__.py b/aspects/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aspects/settings/common.py b/aspects/settings/common.py new file mode 100644 index 0000000..5cbb44c --- /dev/null +++ b/aspects/settings/common.py @@ -0,0 +1,23 @@ +""" +Common Django settings for eox_hooks project. +For more information on this file, see +https://docs.djangoproject.com/en/2.22/topics/settings/ +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.22/ref/settings/ +""" +from aspects import ROOT_DIRECTORY + + +def plugin_settings(settings): + """ + Set of plugin settings used by the Open Edx platform. + More info: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst + """ + settings.MAKO_TEMPLATE_DIRS_BASE.append(ROOT_DIRECTORY / "templates") + settings.SUPERSET_CONFIG = { + "url": "http://superset.local.overhang.io:8088", + "username": "superset", + "password": "superset", + } + settings.ASPECTS_INSTRUCTOR_DASHBOARD_UUID = "1d6bf904-f53f-47fd-b1c9-6cd7e284d286" + settings.SUPERSET_EXTRA_FILTERS_FORMAT = [] diff --git a/aspects/settings/production.py b/aspects/settings/production.py new file mode 100644 index 0000000..25a72a9 --- /dev/null +++ b/aspects/settings/production.py @@ -0,0 +1,19 @@ +""" +Production Django settings for Aspects project. +""" + + +def plugin_settings(settings): + """ + Set of plugin settings used by the Open Edx platform. + More info: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst + """ + settings.SUPERSET_CONFIG = getattr(settings, "ENV_TOKENS", {}).get( + "SUPERSET_CONFIG", settings.SUPERSET_CONFIG + ) + settings.ASPECTS_INSTRUCTOR_DASHBOARD_UUID = getattr(settings, "ENV_TOKENS", {}).get( + "ASPECTS_INSTRUCTOR_DASHBOARD_UUID", settings.ASPECTS_INSTRUCTOR_DASHBOARD_UUID + ) + settings.SUPERSET_EXTRA_FILTERS_FORMAT = getattr(settings, "ENV_TOKENS", {}).get( + "SUPERSET_EXTRA_FILTERS_FORMAT", settings.SUPERSET_EXTRA_FILTERS_FORMAT + ) diff --git a/aspects/static/css/superset.css b/aspects/static/css/superset.css new file mode 100644 index 0000000..e549ea4 --- /dev/null +++ b/aspects/static/css/superset.css @@ -0,0 +1,5 @@ +.superset-embedded-container > iframe { + height: 720px; + width: 100%; + display: block; +} diff --git a/aspects/static/html/superset.html b/aspects/static/html/superset.html new file mode 100644 index 0000000..6b4837f --- /dev/null +++ b/aspects/static/html/superset.html @@ -0,0 +1,25 @@ +{% load i18n %} + + + +
+

{{display_name}}

+ + {% if exception %} +

{% trans 'Superset is not configured properly. Please contact your system administrator.'%}

+

+ {{exception}} +

+ {% elif not dashboard_uuid %} +

+ Dashboard UUID is not set. Please set the dashboard UUID in the Studio. {{dashboard_uuid}} +

+ {% elif superset_url and superset_token %} +
+ + {% endif %} +
diff --git a/aspects/static/js/embed_dashboard.js b/aspects/static/js/embed_dashboard.js new file mode 100644 index 0000000..08c42f7 --- /dev/null +++ b/aspects/static/js/embed_dashboard.js @@ -0,0 +1,30 @@ +function embedDashboard(dashboard_uuid, superset_url, superset_token, xblock_id) { + xblock_id = xblock_id || ""; + window.supersetEmbeddedSdk + .embedDashboard({ + id: dashboard_uuid, // given by the Superset embedding UI + supersetDomain: superset_url, // your Superset instance + mountPoint: document.getElementById(`superset-embedded-container-${xblock_id}`), // any html element that can contain an iframe + fetchGuestToken: () => superset_token, // function that returns a Promise with the guest token + dashboardUiConfig: { + // dashboard UI config: hideTitle, hideTab, hideChartControls, filters.visible, filters.expanded (optional) + hideTitle: true, + filters: { + expanded: false, + }, + hideTab: true, + hideChartControls: false, + hideFilters: true, + }, + }) + .then((dashboard) => { + mountPoint = document.getElementById("superset-embedded-container"); + /* + Perform extra operations on the dashboard object or the container + when the dashboard is loaded + */ + }); +} +if (window.dashboard_uuid !== undefined) { + embedDashboard(window.dashboard_uuid, window.superset_url, window.superset_token, window.xblock_id); +} diff --git a/aspects/static/js/superset.js b/aspects/static/js/superset.js new file mode 100644 index 0000000..14ae644 --- /dev/null +++ b/aspects/static/js/superset.js @@ -0,0 +1,36 @@ +/* Javascript for SupersetXBlock. */ +function SupersetXBlock(runtime, element, context) { + const dashboard_uuid = context.dashboard_uuid; + const superset_url = context.superset_url; + const superset_token = context.superset_token; + const xblock_id = context.xblock_id + + function initSuperset(supersetEmbeddedSdk) { + embedDashboard(dashboard_uuid, superset_url, superset_token, xblock_id); + } + + if (typeof require === "function") { + require(["supersetEmbeddedSdk"], function (supersetEmbeddedSdk) { + window.supersetEmbeddedSdk = supersetEmbeddedSdk; + initSuperset(); + }); + } else { + loadJS(function () { + initSuperset(); + }); + } +} + +function loadJS(callback) { + if (window.supersetEmbeddedSdk) { + callback(); + } else { + $.getScript("https://cdn.jsdelivr.net/npm/@superset-ui/embedded-sdk@0.1.0-alpha.10/bundle/index.min.js") + .done(function () { + callback(); + }) + .fail(function () { + console.error("Error loading supersetEmbeddedSdk."); + }); + } +} diff --git a/aspects/templates/aspects/base.html b/aspects/templates/aspects/base.html deleted file mode 100644 index daf311a..0000000 --- a/aspects/templates/aspects/base.html +++ /dev/null @@ -1,26 +0,0 @@ - - -{% load i18n %} -{% trans "Dummy text to generate a translation (.po) source file. It is safe to delete this line. It is also safe to delete (load i18n) above if there are no other (trans) tags in the file" %} - -{% comment %} -As the developer of this package, don't place anything here if you can help it -since this allows developers to have interoperability between your template -structure and their own. - -Example: Developer melding the 2SoD pattern to fit inside with another pattern:: - - {% extends "base.html" %} - {% load static %} - - - {% block extra_js %} - - - {% block javascript %} - - {% endblock javascript %} - - {% endblock extra_js %} -{% endcomment %} - diff --git a/aspects/templates/instructor_dashboard/aspects.html b/aspects/templates/instructor_dashboard/aspects.html new file mode 100644 index 0000000..1e8020e --- /dev/null +++ b/aspects/templates/instructor_dashboard/aspects.html @@ -0,0 +1,8 @@ +<%page args="section_data" expression_filter="h"/> +<%! from openedx.core.djangolib.markup import HTML %> + +<%include file="/courseware/xqa_interface.html/"/> + +
+ ${HTML(section_data['fragment'].body_html())} +
diff --git a/aspects/tests/__init__.py b/aspects/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aspects/tests/test_filters.py b/aspects/tests/test_filters.py new file mode 100644 index 0000000..4bdee4a --- /dev/null +++ b/aspects/tests/test_filters.py @@ -0,0 +1,46 @@ +""" +Tests for the filters module. +""" + +from unittest import TestCase +from unittest.mock import Mock, patch + +from aspects.extensions.filters import BLOCK_CATEGORY, AddSupersetTab + + +class TestFilters(TestCase): + """ + Test suite for the LimeSurveyXBlock filters. + """ + + def setUp(self) -> None: + """ + Set up the test suite. + """ + self.filter = AddSupersetTab(filter_type=Mock(), running_pipeline=Mock()) + self.template_name = "test-template-name" + self.context = {"course": Mock()} + + @patch("aspects.extensions.filters.generate_superset_context") + def test_run_filter(self, mock_generate_superset_context): + """ + Check the filter is not executed when there are no LimeSurvey blocks in the course. + + Expected result: + - The context is returned without modifications. + """ + mock_generate_superset_context.return_value = { + "sections": [], + } + + context = self.filter.run_filter(self.context, self.template_name) + + self.assertDictContainsSubset( + { + "course_id": str(self.context["course"].id), + "section_key": BLOCK_CATEGORY, + "section_display_name": BLOCK_CATEGORY.title(), + "template_path_prefix": "/instructor_dashboard/", + }, + context["context"]["sections"][0], + ) diff --git a/aspects/tests/test_settings.py b/aspects/tests/test_settings.py new file mode 100644 index 0000000..728ab99 --- /dev/null +++ b/aspects/tests/test_settings.py @@ -0,0 +1,57 @@ +""" +Test plugin settings for commond, devstack and production environments +""" + +from django.conf import settings +from django.test import TestCase + +from aspects.settings import common as common_settings +from aspects.settings import production as production_setttings + + +class TestPluginSettings(TestCase): + """ + Tests plugin settings + """ + + def test_common_settings(self): + """ + Test common settings + """ + 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("username", settings.SUPERSET_CONFIG) + self.assertIn("password", settings.SUPERSET_CONFIG) + self.assertIsNotNone(settings.ASPECTS_INSTRUCTOR_DASHBOARD_UUID) + self.assertIsNotNone(settings.SUPERSET_EXTRA_FILTERS_FORMAT) + + def test_production_settings(self): + """ + Test production settings + """ + settings.ENV_TOKENS = { + "SUPERSET_CONFIG": { + "url": "http://superset.local.overhang.io:8088", + "username": "superset", + "password": "superset", + }, + "ASPECTS_INSTRUCTOR_DASHBOARD_UUID": { + "dashboard_slug": "instructor-dashboard", + "dashboard_uuid": "1d6bf904-f53f-47fd-b1c9-6cd7e284d286", + }, + "SUPERSET_EXTRA_FILTERS_FORMAT": [], + } + production_setttings.plugin_settings(settings) + self.assertEqual( + settings.SUPERSET_CONFIG, settings.ENV_TOKENS["SUPERSET_CONFIG"] + ) + self.assertEqual( + settings.ASPECTS_INSTRUCTOR_DASHBOARD_UUID, + settings.ENV_TOKENS["ASPECTS_INSTRUCTOR_DASHBOARD_UUID"], + ) + self.assertEqual( + settings.SUPERSET_EXTRA_FILTERS_FORMAT, + settings.ENV_TOKENS["SUPERSET_EXTRA_FILTERS_FORMAT"], + ) diff --git a/aspects/tests/test_utils.py b/aspects/tests/test_utils.py new file mode 100644 index 0000000..c93974e --- /dev/null +++ b/aspects/tests/test_utils.py @@ -0,0 +1,102 @@ +""" +Tests for the utils module. +""" + +from collections import namedtuple +from unittest import TestCase +from unittest.mock import Mock, patch + +from django.conf import settings + +from aspects.utils import generate_superset_context + +User = namedtuple("User", ["username"]) + + +class TestUtils(TestCase): + """ + Test utils module + """ + + @patch("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() + context = {"course": course_mock} + mock_generate_guest_token.return_value = ("test-token", "test-dashboard-uuid") + + context = generate_superset_context( + context, + 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.assertNotIn("exception", context) + + @patch("aspects.utils.SupersetClient") + def test_generate_superset_context_with_superset_client_exception(self, mock_superset_client): + """ + Test generate_superset_context + """ + course_mock = Mock() + filter_mock = Mock() + context = {"course": course_mock} + mock_superset_client.side_effect = Exception("test-exception") + + context = generate_superset_context( + context, + dashboard_uuid="test-dashboard-uuid", + filters=[filter_mock], + ) + + self.assertIn("exception", context) + + @patch("aspects.utils.SupersetClient") + @patch("aspects.utils.get_current_user") + def test_generate_superset_context_succesful(self, mock_get_current_user, mock_superset_client): + """ + Test generate_superset_context + """ + course_mock = Mock() + filter_mock = Mock() + 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", + 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")) + + @patch("aspects.utils.get_current_user") + def test_generate_superset_context_with_exception(self, mock_get_current_user): + """ + Test generate_superset_context + """ + course_mock = Mock() + filter_mock = Mock() + mock_get_current_user.return_value = User(username="test-user") + context = {"course": course_mock} + + context = generate_superset_context( + context, + dashboard_uuid="test-dashboard-uuid", + filters=[filter_mock], + ) + + self.assertIn("exception", context) diff --git a/aspects/utils.py b/aspects/utils.py new file mode 100644 index 0000000..ce09db1 --- /dev/null +++ b/aspects/utils.py @@ -0,0 +1,117 @@ +""" +Utilities for the Aspects app. +""" + +import logging +import os + +from crum import get_current_user +from django.conf import settings +from supersetapiclient.client import SupersetClient + +logger = logging.getLogger(__name__) + +if settings.DEBUG: + os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" + + +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"), + } + ) + else: + context.update( + { + "exception": dashboard_uuid, + } + ) + + return context + + +def generate_guest_token(user, course, dashboard_uuid, filters): + """ + Generate a Superset guest token for the user. + + Args: + user: User object. + course: Course object. + + Returns: + 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_username = superset_config.get("username") + superset_password = superset_config.get("password") + + try: + client = SupersetClient( + host=superset_internal_host, + username=superset_username, + password=superset_password, + ) + except Exception as exc: # pylint: disable=broad-except + logger.error(exc) + return None, exc + + formatted_filters = [filter.format(course=course, user=user) for filter in filters] + + 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", + # } + }, + "resources": [{"type": "dashboard", "id": dashboard_uuid}], + "rls": [{"clause": filter} for filter in formatted_filters], + } + + try: + response = client.session.post( + url=f"{superset_internal_host}api/v1/security/guest_token/", + json=data, + headers={"Content-Type": "application/json"}, + ) + response.raise_for_status() + token = response.json()["token"] + + return token, dashboard_uuid + except Exception as exc: # pylint: disable=broad-except + logger.error(exc) + return None, exc diff --git a/requirements/base.in b/requirements/base.in index 31859da..da5221d 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -2,3 +2,8 @@ -c constraints.txt Django # Web application framework +openedx-filters +web_fragments +superset-api-client +web_fragments +django_crum diff --git a/requirements/base.txt b/requirements/base.txt index 7a694e7..691da2d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,13 +6,41 @@ # asgiref==3.7.2 # via django +certifi==2024.2.2 + # via requests +charset-normalizer==3.3.2 + # via requests django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in + # django-crum + # openedx-filters +django-crum==0.7.9 + # via -r requirements/base.in +idna==3.6 + # via requests +oauthlib==3.2.2 + # via requests-oauthlib +openedx-filters==1.6.0 + # via -r requirements/base.in pytz==2024.1 # via django +pyyaml==6.0.1 + # via superset-api-client +requests==2.31.0 + # via + # requests-oauthlib + # superset-api-client +requests-oauthlib==1.3.1 + # via superset-api-client sqlparse==0.4.4 # via django +superset-api-client==0.6.0 + # via -r requirements/base.in typing-extensions==4.9.0 # via asgiref +urllib3==2.2.1 + # via requests +web-fragments==2.1.0 + # via -r requirements/base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 022fc3c..96c7bbf 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -21,11 +21,19 @@ cachetools==5.3.2 # via # -r requirements/ci.txt # tox +certifi==2024.2.2 + # via + # -r requirements/quality.txt + # requests chardet==5.2.0 # via # -r requirements/ci.txt # diff-cover # tox +charset-normalizer==3.3.2 + # via + # -r requirements/quality.txt + # requests click==8.1.7 # via # -r requirements/pip-tools.txt @@ -64,7 +72,11 @@ django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/quality.txt + # django-crum # edx-i18n-tools + # openedx-filters +django-crum==0.7.9 + # via -r requirements/quality.txt edx-i18n-tools==1.3.0 # via -r requirements/dev.in edx-lint==5.3.6 @@ -78,6 +90,10 @@ filelock==3.13.1 # -r requirements/ci.txt # tox # virtualenv +idna==3.6 + # via + # -r requirements/quality.txt + # requests importlib-metadata==7.0.1 # via # -r requirements/pip-tools.txt @@ -105,6 +121,12 @@ mccabe==0.7.0 # via # -r requirements/quality.txt # pylint +oauthlib==3.2.2 + # via + # -r requirements/quality.txt + # requests-oauthlib +openedx-filters==1.6.0 + # via -r requirements/quality.txt packaging==23.2 # via # -r requirements/ci.txt @@ -195,6 +217,16 @@ pyyaml==6.0.1 # -r requirements/quality.txt # code-annotations # edx-i18n-tools + # superset-api-client +requests==2.31.0 + # via + # -r requirements/quality.txt + # requests-oauthlib + # superset-api-client +requests-oauthlib==1.3.1 + # via + # -r requirements/quality.txt + # superset-api-client six==1.16.0 # via # -r requirements/quality.txt @@ -211,6 +243,8 @@ stevedore==5.1.0 # via # -r requirements/quality.txt # code-annotations +superset-api-client==0.6.0 + # via -r requirements/quality.txt text-unidecode==1.3 # via # -r requirements/quality.txt @@ -240,10 +274,16 @@ typing-extensions==4.9.0 # asgiref # astroid # pylint +urllib3==2.2.1 + # via + # -r requirements/quality.txt + # requests virtualenv==20.25.0 # via # -r requirements/ci.txt # tox +web-fragments==2.1.0 + # via -r requirements/quality.txt wheel==0.42.0 # via # -r requirements/pip-tools.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 34dc482..ccf30a9 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -21,11 +21,15 @@ beautifulsoup4==4.12.3 build==1.0.3 # via -r requirements/doc.in certifi==2024.2.2 - # via requests + # via + # -r requirements/test.txt + # requests cffi==1.16.0 # via cryptography charset-normalizer==3.3.2 - # via requests + # via + # -r requirements/test.txt + # requests click==8.1.7 # via # -r requirements/test.txt @@ -44,6 +48,10 @@ django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # django-crum + # openedx-filters +django-crum==0.7.9 + # via -r requirements/test.txt doc8==1.1.1 # via -r requirements/doc.in docutils==0.19 @@ -59,7 +67,9 @@ exceptiongroup==1.2.0 # -r requirements/test.txt # pytest idna==3.6 - # via requests + # via + # -r requirements/test.txt + # requests imagesize==1.4.1 # via sphinx importlib-metadata==7.0.1 @@ -101,6 +111,12 @@ more-itertools==10.2.0 # via jaraco-classes nh3==0.2.15 # via readme-renderer +oauthlib==3.2.2 + # via + # -r requirements/test.txt + # requests-oauthlib +openedx-filters==1.6.0 + # via -r requirements/test.txt packaging==23.2 # via # -r requirements/test.txt @@ -154,14 +170,22 @@ pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations + # superset-api-client readme-renderer==42.0 # via twine requests==2.31.0 # via + # -r requirements/test.txt + # requests-oauthlib # requests-toolbelt # sphinx # sphinxcontrib-images + # superset-api-client # twine +requests-oauthlib==1.3.1 + # via + # -r requirements/test.txt + # superset-api-client requests-toolbelt==1.0.0 # via twine restructuredtext-lint==1.4.0 @@ -226,6 +250,8 @@ stevedore==5.1.0 # -r requirements/test.txt # code-annotations # doc8 +superset-api-client==0.6.0 + # via -r requirements/test.txt text-unidecode==1.3 # via # -r requirements/test.txt @@ -250,8 +276,11 @@ typing-extensions==4.9.0 # rich urllib3==2.2.1 # via + # -r requirements/test.txt # requests # twine +web-fragments==2.1.0 + # via -r requirements/test.txt zipp==3.17.0 # via # importlib-metadata diff --git a/requirements/quality.txt b/requirements/quality.txt index ece826a..3b17427 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -12,6 +12,14 @@ astroid==3.0.3 # via # pylint # pylint-celery +certifi==2024.2.2 + # via + # -r requirements/test.txt + # requests +charset-normalizer==3.3.2 + # via + # -r requirements/test.txt + # requests click==8.1.7 # via # -r requirements/test.txt @@ -34,12 +42,20 @@ django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # django-crum + # openedx-filters +django-crum==0.7.9 + # via -r requirements/test.txt edx-lint==5.3.6 # via -r requirements/quality.in exceptiongroup==1.2.0 # via # -r requirements/test.txt # pytest +idna==3.6 + # via + # -r requirements/test.txt + # requests iniconfig==2.0.0 # via # -r requirements/test.txt @@ -58,6 +74,12 @@ markupsafe==2.1.5 # jinja2 mccabe==0.7.0 # via pylint +oauthlib==3.2.2 + # via + # -r requirements/test.txt + # requests-oauthlib +openedx-filters==1.6.0 + # via -r requirements/test.txt packaging==23.2 # via # -r requirements/test.txt @@ -111,6 +133,16 @@ pyyaml==6.0.1 # via # -r requirements/test.txt # code-annotations + # superset-api-client +requests==2.31.0 + # via + # -r requirements/test.txt + # requests-oauthlib + # superset-api-client +requests-oauthlib==1.3.1 + # via + # -r requirements/test.txt + # superset-api-client six==1.16.0 # via edx-lint snowballstemmer==2.2.0 @@ -123,6 +155,8 @@ stevedore==5.1.0 # via # -r requirements/test.txt # code-annotations +superset-api-client==0.6.0 + # via -r requirements/test.txt text-unidecode==1.3 # via # -r requirements/test.txt @@ -141,3 +175,9 @@ typing-extensions==4.9.0 # asgiref # astroid # pylint +urllib3==2.2.1 + # via + # -r requirements/test.txt + # requests +web-fragments==2.1.0 + # via -r requirements/test.txt diff --git a/requirements/test.txt b/requirements/test.txt index 638da99..2e01c16 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -8,6 +8,14 @@ asgiref==3.7.2 # via # -r requirements/base.txt # django +certifi==2024.2.2 + # via + # -r requirements/base.txt + # requests +charset-normalizer==3.3.2 + # via + # -r requirements/base.txt + # requests click==8.1.7 # via code-annotations code-annotations==1.6.0 @@ -17,14 +25,28 @@ coverage[toml]==7.4.1 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt + # django-crum + # openedx-filters +django-crum==0.7.9 + # via -r requirements/base.txt exceptiongroup==1.2.0 # via pytest +idna==3.6 + # via + # -r requirements/base.txt + # requests iniconfig==2.0.0 # via pytest jinja2==3.1.3 # via code-annotations markupsafe==2.1.5 # via jinja2 +oauthlib==3.2.2 + # via + # -r requirements/base.txt + # requests-oauthlib +openedx-filters==1.6.0 + # via -r requirements/base.txt packaging==23.2 # via pytest pbr==6.0.0 @@ -46,13 +68,27 @@ pytz==2024.1 # -r requirements/base.txt # django pyyaml==6.0.1 - # via code-annotations + # via + # -r requirements/base.txt + # code-annotations + # superset-api-client +requests==2.31.0 + # via + # -r requirements/base.txt + # requests-oauthlib + # superset-api-client +requests-oauthlib==1.3.1 + # via + # -r requirements/base.txt + # superset-api-client sqlparse==0.4.4 # via # -r requirements/base.txt # django stevedore==5.1.0 # via code-annotations +superset-api-client==0.6.0 + # via -r requirements/base.txt text-unidecode==1.3 # via python-slugify tomli==2.0.1 @@ -63,3 +99,9 @@ typing-extensions==4.9.0 # via # -r requirements/base.txt # asgiref +urllib3==2.2.1 + # via + # -r requirements/base.txt + # requests +web-fragments==2.1.0 + # via -r requirements/base.txt diff --git a/setup.py b/setup.py index 8048220..06b7394 100755 --- a/setup.py +++ b/setup.py @@ -157,4 +157,12 @@ def is_requirement(line): 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', ], + entry_points={ + 'lms.djangoapp': [ + 'aspects = aspects.apps:AspectsConfig', + ], + 'cms.djangoapp': [ + 'aspects = aspects.apps:AspectsConfig', + ], + }, ) diff --git a/test_settings.py b/test_settings.py index c51c483..fe4db2e 100644 --- a/test_settings.py +++ b/test_settings.py @@ -14,6 +14,7 @@ def root(*args): """ return join(abspath(dirname(__file__)), *args) +DEBUG = True DATABASES = { 'default': { @@ -57,3 +58,13 @@ def root(*args): ], }, }] + +ASPECTS_INSTRUCTOR_DASHBOARD_UUID = "test-dashboard-uuid" + +SUPERSET_CONFIG = { + "url": "http://dummy-superset-url:8088", + "username": "superset", + "password": "superset", +} + +SUPERSET_EXTRA_FILTERS_FORMAT = [] \ No newline at end of file diff --git a/tests/test_dummy.py b/tests/test_dummy.py deleted file mode 100644 index 3a92518..0000000 --- a/tests/test_dummy.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Dummy test to check if the test suite is working. -""" -import pytest - - -@pytest.mark.skip(reason="Placeholder to allow pytest to succeed before real tests are in place.") -def test_placeholder(): - """ - TODO: Delete this test once there are real tests. - """ diff --git a/tox.ini b/tox.ini index 93cf810..e6e2d5e 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ match-dir = (?!migrations) [pytest] DJANGO_SETTINGS_MODULE = test_settings -addopts = --cov aspects --cov tests --cov-report term-missing --cov-report xml +addopts = --cov aspects --cov-report term-missing --cov-report xml norecursedirs = .* docs requirements site-packages [testenv] @@ -71,12 +71,10 @@ allowlist_externals = deps = -r{toxinidir}/requirements/quality.txt commands = - touch tests/__init__.py - pylint aspects tests test_utils manage.py setup.py - rm tests/__init__.py - pycodestyle aspects tests manage.py setup.py - pydocstyle aspects tests manage.py setup.py - isort --check-only --diff tests test_utils aspects manage.py setup.py test_settings.py + pylint aspects test_utils manage.py setup.py + pycodestyle aspects manage.py setup.py + pydocstyle aspects manage.py setup.py + isort --check-only --diff test_utils aspects manage.py setup.py test_settings.py make selfcheck [testenv:pii_check]