diff --git a/platform_plugin_aspects/__init__.py b/platform_plugin_aspects/__init__.py index 009e61c3..d5f7234d 100644 --- a/platform_plugin_aspects/__init__.py +++ b/platform_plugin_aspects/__init__.py @@ -5,6 +5,6 @@ import os from pathlib import Path -__version__ = "1.0.2" +__version__ = "1.1.0" ROOT_DIRECTORY = Path(os.path.dirname(os.path.abspath(__file__))) diff --git a/platform_plugin_aspects/extensions/filters.py b/platform_plugin_aspects/extensions/filters.py index 7373c86a..4b432a9b 100644 --- a/platform_plugin_aspects/extensions/filters.py +++ b/platform_plugin_aspects/extensions/filters.py @@ -10,7 +10,11 @@ from openedx_filters import PipelineStep from web_fragments.fragment import Fragment -from platform_plugin_aspects.utils import _, generate_superset_context, get_model +from platform_plugin_aspects.utils import ( + _, + generate_superset_context, + get_user_dashboard_locale, +) TEMPLATE_ABSOLUTE_PATH = "/instructor_dashboard/" BLOCK_CATEGORY = "aspects" @@ -34,20 +38,7 @@ def run_filter( show_dashboard_link = settings.SUPERSET_SHOW_INSTRUCTOR_DASHBOARD_LINK user = get_current_user() - - try: - user_language = ( - get_model("user_preference").get_value(user, "pref-lang") or "en" - ) - # If there is no user_preferences model defined this will get thrown - except AttributeError: - user_language = "en" - - formatted_language = user_language.lower().replace("-", "_") - if formatted_language not in [ - loc.lower().replace("-", "_") for loc in settings.SUPERSET_DASHBOARD_LOCALES - ]: - formatted_language = "en" + formatted_language = get_user_dashboard_locale(user) context["course_id"] = course.id context = generate_superset_context( diff --git a/platform_plugin_aspects/extensions/tests/test_filters.py b/platform_plugin_aspects/extensions/tests/test_filters.py index 559f2475..55acaf17 100644 --- a/platform_plugin_aspects/extensions/tests/test_filters.py +++ b/platform_plugin_aspects/extensions/tests/test_filters.py @@ -23,10 +23,10 @@ def setUp(self) -> None: self.course_id = "course-v1:org+course+run" self.context = {"course": Mock(id=self.course_id), "sections": []} - @patch("platform_plugin_aspects.extensions.filters.get_model") - def test_run_filter_with_language( + @patch("platform_plugin_aspects.extensions.filters.get_user_dashboard_locale") + def test_run_filter( self, - mock_get_model, + mock_get_user_dashboard_locale, ): """ Check the filter is not executed when there are no LimeSurvey blocks in the course. @@ -34,7 +34,7 @@ def test_run_filter_with_language( Expected result: - The context is returned without modifications. """ - mock_get_model.return_value.get_value.return_value = "not-a-language" + mock_get_user_dashboard_locale.return_value = "en" context = self.filter.run_filter(self.context, self.template_name) @@ -47,30 +47,4 @@ def test_run_filter_with_language( "template_path_prefix": "/instructor_dashboard/", }.items() <= context["context"]["sections"][0].items() - mock_get_model.assert_called_once() - - @patch("platform_plugin_aspects.extensions.filters.get_model") - def test_run_filter_without_language( - self, - mock_get_model, - ): - """ - Check the filter is not executed when there are no LimeSurvey blocks in the course. - - Expected result: - - The context is returned without modifications. - """ - mock_get_model.return_value.get_value.return_value = None - - context = self.filter.run_filter(self.context, self.template_name) - - assert { - "course_id": self.course_id, - "section_key": BLOCK_CATEGORY, - "section_display_name": "Reports", - "superset_url": "http://superset-dummy-url/", - "superset_guest_token_url": f"https://lms.url/superset_guest_token/{self.course_id}", - "template_path_prefix": "/instructor_dashboard/", - }.items() <= context["context"]["sections"][0].items() - - mock_get_model.assert_called_once() + mock_get_user_dashboard_locale.assert_called_once() diff --git a/platform_plugin_aspects/settings/common.py b/platform_plugin_aspects/settings/common.py index b5361959..89104014 100644 --- a/platform_plugin_aspects/settings/common.py +++ b/platform_plugin_aspects/settings/common.py @@ -104,3 +104,40 @@ def plugin_settings(settings): "model": "ObjectTag", }, } + + settings.ASPECTS_IN_CONTEXT_DASHBOARDS = { + "course": { + "name": _("Course"), + "slug": "in-context-course", + "uuid": "f2880cc1-63e9-48d7-ac3c-d2ff6f6698e2", + "allow_translations": True, + "course_filter_ids": ["NATIVE_FILTER-QLQbulmHH"], + "block_filter_ids": [], + }, + "sequential": { + "name": _("Graded Subsection"), + "slug": "in-context-graded-subsection", + "uuid": "f0321087-6428-4b97-b32e-2dae7d9cc447", + "allow_translations": True, + "course_filter_ids": ["NATIVE_FILTER-oPAuR7ahy"], + "block_filter_ids": ["NATIVE_FILTER-CBWbI7uLq"], + }, + "problem": { + "name": _("Problem"), + "slug": "in-context-problem", + "uuid": "98ff33ff-18dd-48f9-8c58-629ae4f4194b", + "allow_translations": True, + "course_filter_ids": ["NATIVE_FILTER-29CPcbirK"], + "block_filter_ids": ["NATIVE_FILTER-TJwItQhUI"], + }, + "video": { + "name": _("Video"), + "slug": "in-context-video", + "uuid": "bc6510fb-027f-4026-a333-d0c42d3cc35c", + "allow_translations": True, + "course_filter_ids": ["NATIVE_FILTER-uaxvZkSAg"], + "block_filter_ids": ["NATIVE_FILTER-Fse4rzDW0"], + }, + } + settings.IN_CONTEXT_DASHBOARD_COURSE_KEY_COLUMN = "course_key" + settings.IN_CONTEXT_DASHBOARD_BLOCK_ID_COLUMN = "block_id" diff --git a/platform_plugin_aspects/settings/production.py b/platform_plugin_aspects/settings/production.py index e355ff52..9feec029 100644 --- a/platform_plugin_aspects/settings/production.py +++ b/platform_plugin_aspects/settings/production.py @@ -28,3 +28,7 @@ def plugin_settings(settings): "EVENT_SINK_CLICKHOUSE_PII_MODELS", settings.EVENT_SINK_CLICKHOUSE_PII_MODELS, ) + settings.ASPECTS_IN_CONTEXT_DASHBOARDS = settings.ENV_TOKENS.get( + "ASPECTS_IN_CONTEXT_DASHBOARDS", + settings.ASPECTS_IN_CONTEXT_DASHBOARDS, + ) diff --git a/platform_plugin_aspects/tests/test_utils.py b/platform_plugin_aspects/tests/test_utils.py index 86e9d422..97fc6df8 100644 --- a/platform_plugin_aspects/tests/test_utils.py +++ b/platform_plugin_aspects/tests/test_utils.py @@ -15,6 +15,7 @@ get_ccx_courses, get_model, get_tags_for_block, + get_user_dashboard_locale, ) from test_utils.helpers import course_factory @@ -271,3 +272,23 @@ def mock_tag(taxonomy, value, i, parent=None): course_tags = get_tags_for_block(course.location) assert course_tags == [1, 2, 3, 4, 5] mock_get_object_tags.assert_called_once_with(course.location) + + @patch("platform_plugin_aspects.utils.get_model") + def test_get_user_dashboard_locale(self, mock_get_model): + """Test that get_user_dashboard_locale gets user language with fallback to 'en'.""" + mock_get_model.return_value.get_value.return_value = "es-419" + user = Mock() + assert get_user_dashboard_locale(user) == "es_419" + mock_get_model.assert_called_once() + mock_get_model.reset_mock() + + mock_get_model.return_value.get_value.return_value = None + user = Mock() + assert get_user_dashboard_locale(user) == "en" + mock_get_model.assert_called_once() + mock_get_model.reset_mock() + + mock_get_model.return_value.get_value.return_value = "not-a-language" + user = Mock() + assert get_user_dashboard_locale(user) == "en" + mock_get_model.assert_called_once() diff --git a/platform_plugin_aspects/tests/test_views.py b/platform_plugin_aspects/tests/test_views.py index a3fad128..35b2f59d 100644 --- a/platform_plugin_aspects/tests/test_views.py +++ b/platform_plugin_aspects/tests/test_views.py @@ -29,6 +29,13 @@ def setUp(self): super().setUp() self.client = APIClient() self.superset_guest_token_url = f"/superset_guest_token/{COURSE_ID}" + self.superset_in_context_dashboard_course_url = ( + f"/superset_in_context_dashboard/{COURSE_ID}" + ) + self.superset_in_context_dashboard_block_url = ( + "/superset_in_context_dashboard/" + "block-v1:org+course+run+type@problem+block@e25d8eac15224f91bd3aa22bfe28a602" + ) self.user = User.objects.create( username="user", email="user@example.com", @@ -54,8 +61,9 @@ def test_guest_token_invalid_course_id(self): @patch("platform_plugin_aspects.views.get_model") def test_guest_token_course_not_found(self, mock_get_model): mock_model_get = Mock(side_effect=ObjectDoesNotExist) + mock_model_only = Mock(return_value=Mock(get=mock_model_get)) mock_get_model.return_value = Mock( - objects=Mock(get=mock_model_get), + objects=Mock(only=mock_model_only), DoesNotExist=ObjectDoesNotExist, ) @@ -102,8 +110,9 @@ def test_guest_token_with_course_overview( mock_has_object_permission.return_value = True mock_generate_guest_token.return_value = "test-token" mock_model_get = Mock(return_value=Mock(display_name="Course Title")) + mock_model_only = Mock(return_value=Mock(get=mock_model_get)) mock_get_model.return_value = Mock( - objects=Mock(get=mock_model_get), + objects=Mock(only=mock_model_only), DoesNotExist=ObjectDoesNotExist, ) @@ -117,6 +126,109 @@ def test_guest_token_with_course_overview( mock_generate_guest_token.assert_called_once_with( user=self.user, course=CourseKey.from_string(COURSE_ID), - dashboards=settings.ASPECTS_INSTRUCTOR_DASHBOARDS, + dashboards=( + settings.ASPECTS_INSTRUCTOR_DASHBOARDS + + list(settings.ASPECTS_IN_CONTEXT_DASHBOARDS.values()) + ), filters=DEFAULT_FILTERS_FORMAT, ) + + def test_in_context_dashboard_requires_authorization(self): + response = self.client.get(self.superset_in_context_dashboard_course_url) + self.assertEqual(response.status_code, 403) + + def test_in_context_dashboard_requires_course_access(self): + self.client.login(username="user", password="password") + response = self.client.get(self.superset_in_context_dashboard_course_url) + self.assertEqual(response.status_code, 403) + + def test_in_context_dashboard_invalid_usage_key(self): + # Will fail as it is not a well-formed block id. + superset_in_context_dashboard_course_url = ( + "/superset_in_context_dashboard/block-v1:org+course+run" + ) + self.client.login(username="user", password="password") + response = self.client.get(superset_in_context_dashboard_course_url) + self.assertEqual(response.status_code, 404) + + @patch("platform_plugin_aspects.views.get_model") + def test_in_context_dashboard_course_not_found(self, mock_get_model): + mock_model_get = Mock(side_effect=ObjectDoesNotExist) + mock_model_only = Mock(return_value=Mock(get=mock_model_get)) + mock_get_model.return_value = Mock( + objects=Mock(only=mock_model_only), + DoesNotExist=ObjectDoesNotExist, + ) + + self.client.login(username="user", password="password") + response = self.client.get(self.superset_in_context_dashboard_course_url) + self.assertEqual(response.status_code, 404) + mock_model_get.assert_called_once() + + @patch("platform_plugin_aspects.views.get_model") + def test_in_context_dashboard_block_course_not_found(self, mock_get_model): + mock_model_get = Mock(side_effect=ObjectDoesNotExist) + mock_model_only = Mock(return_value=Mock(get=mock_model_get)) + mock_get_model.return_value = Mock( + objects=Mock(only=mock_model_only), + DoesNotExist=ObjectDoesNotExist, + ) + + self.client.login(username="user", password="password") + response = self.client.get(self.superset_in_context_dashboard_block_url) + self.assertEqual(response.status_code, 404) + mock_model_get.assert_called_once() + + @patch.object(IsCourseStaffInstructor, "has_object_permission") + def test_in_context_dashboard_block_no_dashboard(self, mock_has_object_permission): + mock_has_object_permission.return_value = True + # Will be not found as test settings do not include dashboard for video blocks. + superset_in_context_dashboard_video_block_url = ( + "/superset_in_context_dashboard/" + "block-v1:org+course+run+type@video+block@e25d8eac15224f91bd3aa22bfe28a602" + ) + self.client.login(username="user", password="password") + response = self.client.get(superset_in_context_dashboard_video_block_url) + self.assertEqual(response.status_code, 404) + + @patch.object(IsCourseStaffInstructor, "has_object_permission") + @patch("platform_plugin_aspects.views.get_localized_uuid") + def test_in_context_dashboard_course( + self, mock_get_localized_uuid, mock_has_object_permission + ): + mock_has_object_permission.return_value = True + mock_get_localized_uuid.return_value = "00000000-0000-0000-0000-000000000000" + + self.client.login(username="user", password="password") + response = self.client.get(self.superset_in_context_dashboard_course_url) + + mock_has_object_permission.assert_called_once() + + dashboard_uuid = settings.ASPECTS_IN_CONTEXT_DASHBOARDS["course"]["uuid"] + mock_get_localized_uuid.assert_called_once_with(dashboard_uuid, "en") + + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["dashboardId"], "00000000-0000-0000-0000-000000000000") + self.assertEqual(data["defaultCourseRun"], "run") + + @patch.object(IsCourseStaffInstructor, "has_object_permission") + @patch("platform_plugin_aspects.views.get_localized_uuid") + def test_in_context_dashboard_block( + self, mock_get_localized_uuid, mock_has_object_permission + ): + mock_has_object_permission.return_value = True + mock_get_localized_uuid.return_value = "00000000-0000-0000-0000-000000000000" + + self.client.login(username="user", password="password") + response = self.client.get(self.superset_in_context_dashboard_block_url) + + mock_has_object_permission.assert_called_once() + + dashboard_uuid = settings.ASPECTS_IN_CONTEXT_DASHBOARDS["problem"]["uuid"] + mock_get_localized_uuid.assert_called_once_with(dashboard_uuid, "en") + + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["dashboardId"], "00000000-0000-0000-0000-000000000000") + self.assertEqual(data["defaultCourseRun"], "run") diff --git a/platform_plugin_aspects/urls.py b/platform_plugin_aspects/urls.py index fd8b8d29..5b4c9193 100644 --- a/platform_plugin_aspects/urls.py +++ b/platform_plugin_aspects/urls.py @@ -8,14 +8,23 @@ # Copied from openedx.core.constants COURSE_ID_PATTERN = r"(?P[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)" +# Copied from lms.envs.common +USAGE_ID_PATTERN = ( + r"(?P(?:i4x://?[^/]+/[^/]+/[^/]+/[^@]+(?:@[^/]+)?)|(?:[^/]+))" +) app_url_patterns = ( [ re_path( rf"superset_guest_token/{COURSE_ID_PATTERN}/?$", - views.SupersetView.as_view(), + views.SupersetTokenView.as_view(), name="superset_guest_token", ), + re_path( + rf"superset_in_context_dashboard/{USAGE_ID_PATTERN}/?$", + views.SupersetInContextDashboardView.as_view(), + name="superset_in_context_dashboard", + ), ], "platform_plugin_aspects", ) diff --git a/platform_plugin_aspects/utils.py b/platform_plugin_aspects/utils.py index f9b21479..2e64846b 100644 --- a/platform_plugin_aspects/utils.py +++ b/platform_plugin_aspects/utils.py @@ -37,6 +37,9 @@ def _(text): ] +DEFAULT_USER_LANGUAGE = "en" + + def generate_superset_context( context, dashboards, @@ -309,3 +312,51 @@ def get_tags_for_block(usage_key) -> set: tag = tag.parent return list(serialized_tags) + + +def get_user_dashboard_locale(user): + """ + + Get the Superset dashboard locale from the user preference. + + """ + try: + user_language = ( + get_model("user_preference").get_value(user, "pref-lang") + or DEFAULT_USER_LANGUAGE + ) + # If there is no user_preferences model defined this will get thrown + except AttributeError: + user_language = DEFAULT_USER_LANGUAGE + + formatted_language = user_language.lower().replace("-", "_") + if formatted_language not in [ + loc.lower().replace("-", "_") for loc in settings.SUPERSET_DASHBOARD_LOCALES + ]: + formatted_language = DEFAULT_USER_LANGUAGE + + return formatted_language + + +def build_filter(filter_id, column, operator, value): + """ + + Build a Superset native filter option. + + """ + return { + "id": filter_id, + "extraFormData": { + "filters": [ + { + "col": column, + "op": operator, + "val": value, + }, + ], + }, + "filterState": { + "value": value, + }, + "ownState": {}, + } diff --git a/platform_plugin_aspects/views.py b/platform_plugin_aspects/views.py index 100171e7..2ecd58c0 100644 --- a/platform_plugin_aspects/views.py +++ b/platform_plugin_aspects/views.py @@ -4,17 +4,26 @@ from collections import namedtuple +import prison from django.conf import settings from django.core.exceptions import ImproperlyConfigured from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.keys import CourseKey, UsageKey from rest_framework import permissions from rest_framework.authentication import SessionAuthentication from rest_framework.exceptions import APIException, NotFound from rest_framework.generics import GenericAPIView from rest_framework.response import Response -from .utils import DEFAULT_FILTERS_FORMAT, _, generate_guest_token, get_model +from .utils import ( + DEFAULT_FILTERS_FORMAT, + _, + build_filter, + generate_guest_token, + get_localized_uuid, + get_model, + get_user_dashboard_locale, +) try: from openedx.core.lib.api.permissions import ( @@ -58,9 +67,31 @@ def has_object_permission(self, request, view, obj): Course = namedtuple("Course", ["course_id", "display_name"]) -class SupersetView(GenericAPIView): +def _get_course(course_key): """ - Superset-related endpoints provided by the aspects platform plugin. + Return a Course-like object for the requested course id. + + Raise NotFound if the corresponding course does not exist. + """ + # Fetch the CourseOverview (if we're running in edx-platform) + display_name = "" + CourseOverview = get_model("course_overviews") + if CourseOverview: + try: + course_overview = CourseOverview.objects.only("display_name").get( + id=course_key + ) + display_name = course_overview.display_name + except CourseOverview.DoesNotExist as exc: + raise NotFound( + _("Course not found: '{course_id}'").format(course_id=course_key) + ) from exc + return Course(course_id=course_key, display_name=display_name) + + +class SupersetTokenView(GenericAPIView): + """ + Superset guest token endpoint. """ authentication_classes = (SessionAuthentication,) @@ -83,19 +114,7 @@ def get_object(self): _("Invalid course id: '{course_id}'").format(course_id=course_id) ) from exc - # Fetch the CourseOverview (if we're running in edx-platform) - display_name = "" - CourseOverview = get_model("course_overviews") - if CourseOverview: - try: - course_overview = CourseOverview.objects.get(id=course_key) - display_name = course_overview.display_name - except CourseOverview.DoesNotExist as exc: - raise NotFound( - _("Course not found: '{course_id}'").format(course_id=course_id) - ) from exc - - course = Course(course_id=course_key, display_name=display_name) + course = _get_course(course_key) # May raise a permission denied self.check_object_permissions(self.request, course) @@ -108,7 +127,8 @@ def get(self, request, *args, **kwargs): """ course = self.get_object() - dashboards = settings.ASPECTS_INSTRUCTOR_DASHBOARDS + dashboards = settings.ASPECTS_INSTRUCTOR_DASHBOARDS.copy() + dashboards.extend(settings.ASPECTS_IN_CONTEXT_DASHBOARDS.values()) extra_filters_format = settings.SUPERSET_EXTRA_FILTERS_FORMAT try: @@ -122,3 +142,102 @@ def get(self, request, *args, **kwargs): raise APIException() from exc return Response({"guestToken": guest_token}) + + +class SupersetInContextDashboardView(GenericAPIView): + """ + Endpoint for in-context analytics embedded Superset dashboard parameters. + """ + + authentication_classes = (SessionAuthentication,) + permission_classes = ( + permissions.IsAuthenticated, + IsStaffOrReadOnly | IsCourseStaffInstructor, + ) + + lookup_field = "usage_id" + + def get_object(self): + """ + Return a usage key or course key for the requested usage_key. + """ + usage_id = self.kwargs.get(self.lookup_field, "") + try: + usage_key = UsageKey.from_string(usage_id) + except InvalidKeyError as exc: + try: + course_key = usage_key = CourseKey.from_string(usage_id) + except InvalidKeyError: + raise NotFound( + _("Invalid usage id: '{usage_id}'").format(usage_id=usage_id) + ) from exc + else: + course_key = usage_key.course_key + + course = _get_course(course_key) + self.check_object_permissions(self.request, course) + + return usage_key + + def get(self, request, *args, **kwargs): + """ + Return Superset context for embedding the dashboard for the requested block. + """ + usage_key = self.get_object() + if isinstance(usage_key, CourseKey): + block_type = "course" + course_key = usage_key + else: + block_type = usage_key.block_type + course_key = usage_key.course_key + + course_key_col = settings.IN_CONTEXT_DASHBOARD_COURSE_KEY_COLUMN + block_id_col = settings.IN_CONTEXT_DASHBOARD_BLOCK_ID_COLUMN + + dashboards = settings.ASPECTS_IN_CONTEXT_DASHBOARDS + dashboard = dashboards.get(block_type) + if dashboard is None: + raise NotFound( + _("No dashboard for block type: '{block_type}'").format( + block_type=block_type + ) + ) + + block_filter = {} + if not isinstance(usage_key, CourseKey): + for filter_id in dashboard["block_filter_ids"]: + block_filter[filter_id] = build_filter( + filter_id, block_id_col, "IN", [usage_key.block_id] + ) + + course_runs = {} + # In the future we might provide other course runs so that the + # embedding application can apply their corresponding filters + # instead of needing the Superset filter UI. + for course_run in [course_key.run]: + course_filter = block_filter.copy() + for filter_id in dashboard["course_filter_ids"]: + course_filter[filter_id] = build_filter( + filter_id, course_key_col, "IN", [str(course_key)] + ) + course_runs[course_run] = { + # Superset filter URL parameter is encoded as Rison. + "native_filters": prison.dumps(course_filter), + } + + if dashboard.get("allow_translations"): + language = get_user_dashboard_locale(request.user) + dashboard = dashboard.copy() + dashboard["uuid"] = get_localized_uuid(dashboard["uuid"], language) + + superset_config = settings.SUPERSET_CONFIG + superset_url = superset_config.get("service_url") + + return Response( + { + "dashboardId": dashboard["uuid"], + "supersetUrl": superset_url, + "courseRuns": course_runs, + "defaultCourseRun": course_key.run, + } + ) diff --git a/requirements/base.in b/requirements/base.in index 3ed1e28b..097684b2 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -16,3 +16,4 @@ edx-opaque-keys # Parsing library for course and usage keys djangorestframework # REST API framework edx-toggles XBlock +prison # Rison encoder, same one as used by Superset diff --git a/requirements/base.txt b/requirements/base.txt index 97f9d095..e2f8fe81 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -96,6 +96,8 @@ openedx-filters==2.0.1 # via -r requirements/base.in pbr==6.1.1 # via stevedore +prison==0.2.1 + # via -r requirements/base.in prompt-toolkit==3.0.51 # via click-repl psutil==7.0.0 @@ -133,6 +135,7 @@ simplejson==3.20.1 six==1.17.0 # via # fs + # prison # python-dateutil sqlparse==0.5.3 # via django diff --git a/requirements/dev.txt b/requirements/dev.txt index dbc62ac9..c6e836b6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -269,6 +269,8 @@ pluggy==1.5.0 # tox polib==1.2.0 # via edx-i18n-tools +prison==0.2.1 + # via -r requirements/quality.txt prompt-toolkit==3.0.51 # via # -r requirements/quality.txt @@ -377,6 +379,7 @@ six==1.17.0 # -r requirements/quality.txt # edx-lint # fs + # prison # python-dateutil snowballstemmer==2.2.0 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index 652379c3..118d9f54 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -194,7 +194,7 @@ model-bakery==1.20.4 # via # -r requirements/test.txt # django-mock-queries -more-itertools==10.6.0 +more-itertools==10.7.0 # via # jaraco-classes # jaraco-functools @@ -228,6 +228,8 @@ pluggy==1.5.0 # via # -r requirements/test.txt # pytest +prison==0.2.1 + # via -r requirements/test.txt prompt-toolkit==3.0.51 # via # -r requirements/test.txt @@ -329,6 +331,7 @@ six==1.17.0 # via # -r requirements/test.txt # fs + # prison # python-dateutil snowballstemmer==2.2.0 # via sphinx diff --git a/requirements/quality.txt b/requirements/quality.txt index 9346ac0f..58734c23 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -200,6 +200,8 @@ pluggy==1.5.0 # via # -r requirements/test.txt # pytest +prison==0.2.1 + # via -r requirements/test.txt prompt-toolkit==3.0.51 # via # -r requirements/test.txt @@ -290,6 +292,7 @@ six==1.17.0 # -r requirements/test.txt # edx-lint # fs + # prison # python-dateutil snowballstemmer==2.2.0 # via pydocstyle diff --git a/requirements/test.txt b/requirements/test.txt index bd940fd0..44c8eb7d 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -159,6 +159,8 @@ pbr==6.1.1 # stevedore pluggy==1.5.0 # via pytest +prison==0.2.1 + # via -r requirements/base.txt prompt-toolkit==3.0.51 # via # -r requirements/base.txt @@ -229,6 +231,7 @@ six==1.17.0 # via # -r requirements/base.txt # fs + # prison # python-dateutil sqlparse==0.5.3 # via diff --git a/test_settings.py b/test_settings.py index 064fcd78..b3968938 100644 --- a/test_settings.py +++ b/test_settings.py @@ -99,6 +99,26 @@ } ] +ASPECTS_IN_CONTEXT_DASHBOARDS = { + "course": { + "slug": "in-context-course", + "uuid": "f2880cc1-63e9-48d7-ac3c-d2ff6f6698e2", + "allow_translations": True, + "course_filter_ids": ["NATIVE_FILTER-QLQbulmHH"], + "block_filter_ids": [], + }, + "problem": { + "slug": "in-context-problem", + "uuid": "98ff33ff-18dd-48f9-8c58-629ae4f4194b", + "allow_translations": True, + "course_filter_ids": ["NATIVE_FILTER-29CPcbirK"], + "block_filter_ids": ["NATIVE_FILTER-TJwItQhUI"], + }, +} + +IN_CONTEXT_DASHBOARD_COURSE_KEY_COLUMN = "course_key" +IN_CONTEXT_DASHBOARD_BLOCK_ID_COLUMN = "block_id" + EVENT_SINK_CLICKHOUSE_BACKEND_CONFIG = { "url": "https://foo.bar", "username": "bob",