diff --git a/platform_plugin_aspects/apps.py b/platform_plugin_aspects/apps.py
index fa2b8a4..36da0ea 100644
--- a/platform_plugin_aspects/apps.py
+++ b/platform_plugin_aspects/apps.py
@@ -3,7 +3,7 @@
"""
from django.apps import AppConfig
-from edx_django_utils.plugins import PluginSettings, PluginSignals
+from edx_django_utils.plugins import PluginSettings, PluginSignals, PluginURLs
class PlatformPluginAspectsConfig(AppConfig):
@@ -14,6 +14,13 @@ class PlatformPluginAspectsConfig(AppConfig):
name = "platform_plugin_aspects"
plugin_app = {
+ PluginURLs.CONFIG: {
+ "lms.djangoapp": {
+ PluginURLs.NAMESPACE: name,
+ PluginURLs.REGEX: r"^aspects/",
+ PluginURLs.RELATIVE_PATH: "urls",
+ },
+ },
PluginSettings.CONFIG: {
"lms.djangoapp": {
"production": {PluginSettings.RELATIVE_PATH: "settings.production"},
diff --git a/platform_plugin_aspects/extensions/filters.py b/platform_plugin_aspects/extensions/filters.py
index 428d53c..90b7c01 100644
--- a/platform_plugin_aspects/extensions/filters.py
+++ b/platform_plugin_aspects/extensions/filters.py
@@ -14,12 +14,6 @@
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):
"""
@@ -36,9 +30,6 @@ def run_filter(
"""
course = context["course"]
dashboards = settings.ASPECTS_INSTRUCTOR_DASHBOARDS
- extra_filters_format = settings.SUPERSET_EXTRA_FILTERS_FORMAT
-
- filters = ASPECTS_SECURITY_FILTERS_FORMAT + extra_filters_format
user = get_current_user()
@@ -51,9 +42,7 @@ def run_filter(
context = generate_superset_context(
context,
- user,
dashboards=dashboards,
- filters=filters,
language=formatted_language,
)
diff --git a/platform_plugin_aspects/static/html/superset.html b/platform_plugin_aspects/static/html/superset.html
index 2782da9..fd5e9fb 100644
--- a/platform_plugin_aspects/static/html/superset.html
+++ b/platform_plugin_aspects/static/html/superset.html
@@ -10,7 +10,7 @@
{{display_name}}
{{exception}}
{% elif not superset_dashboards %}
Dashboard UUID is not set. Please set the dashboard UUID in the Studio.
- {% elif superset_url and superset_token %} {% if xblock_id %}
+ {% elif superset_url and superset_guest_token_url %} {% if xblock_id %}
{% else %}
@@ -31,7 +31,7 @@
{{display_name}}
{% endif %}
diff --git a/platform_plugin_aspects/static/js/embed_dashboard.js b/platform_plugin_aspects/static/js/embed_dashboard.js
index 1013001..0a26b44 100644
--- a/platform_plugin_aspects/static/js/embed_dashboard.js
+++ b/platform_plugin_aspects/static/js/embed_dashboard.js
@@ -1,11 +1,24 @@
-function embedDashboard(dashboard_uuid, superset_url, superset_token, xblock_id) {
+function embedDashboard(dashboard_uuid, superset_url, guest_token_url, xblock_id) {
xblock_id = xblock_id || "";
+
+ async fetchGuestToken() {
+ // Fetch the guest token from your backend
+ const response = await fetch(guest_token_url, {
+ method: 'POST',
+ body: JSON.stringify({
+ // TODO csrf_token: csrf_token,
+ })
+ });
+ const data = await response.json();
+ return data.guestToken;
+ }
+
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
+ fetchGuestToken: fetchGuestToken,
dashboardUiConfig: {
// dashboard UI config: hideTitle, hideTab, hideChartControls, filters.visible, filters.expanded (optional)
hideTitle: true,
@@ -28,6 +41,6 @@ function embedDashboard(dashboard_uuid, superset_url, superset_token, xblock_id)
if (window.superset_dashboards !== undefined) {
window.superset_dashboards.forEach(function(dashboard) {
- embedDashboard(dashboard.uuid, window.superset_url, window.superset_token, dashboard.uuid);
+ embedDashboard(dashboard.uuid, window.superset_url, window.superset_guest_token_url);
});
}
diff --git a/platform_plugin_aspects/static/js/superset.js b/platform_plugin_aspects/static/js/superset.js
index 14ae644..392630c 100644
--- a/platform_plugin_aspects/static/js/superset.js
+++ b/platform_plugin_aspects/static/js/superset.js
@@ -2,11 +2,11 @@
function SupersetXBlock(runtime, element, context) {
const dashboard_uuid = context.dashboard_uuid;
const superset_url = context.superset_url;
- const superset_token = context.superset_token;
+ const superset_guest_token_url = context.superset_guest_token_url;
const xblock_id = context.xblock_id
function initSuperset(supersetEmbeddedSdk) {
- embedDashboard(dashboard_uuid, superset_url, superset_token, xblock_id);
+ embedDashboard(dashboard_uuid, superset_url, superset_guest_token_url, xblock_id);
}
if (typeof require === "function") {
diff --git a/platform_plugin_aspects/tests/test_utils.py b/platform_plugin_aspects/tests/test_utils.py
index 3839373..0b7f95a 100644
--- a/platform_plugin_aspects/tests/test_utils.py
+++ b/platform_plugin_aspects/tests/test_utils.py
@@ -2,19 +2,19 @@
Test utils.
"""
-from collections import namedtuple
from unittest.mock import Mock, patch
from django.conf import settings
from django.test import TestCase
from platform_plugin_aspects.utils import (
+ generate_guest_token,
generate_superset_context,
get_ccx_courses,
get_model,
)
-User = namedtuple("User", ["username"])
+COURSE_ID = "course-v1:org+course+run"
class TestUtils(TestCase):
@@ -104,15 +104,11 @@ def test_get_ccx_courses_feature_disabled(self):
"password": "superset",
},
)
- @patch("platform_plugin_aspects.utils._generate_guest_token")
- def test_generate_superset_context(self, mock_generate_guest_token):
+ def test_generate_superset_context(self):
"""
Test generate_superset_context
"""
- course_mock = Mock()
- filter_mock = Mock()
- user_mock = Mock()
- context = {"course": course_mock}
+ context = {"course": COURSE_ID}
dashboards = settings.ASPECTS_INSTRUCTOR_DASHBOARDS
dashboards.append(
@@ -123,42 +119,41 @@ def test_generate_superset_context(self, mock_generate_guest_token):
}
)
- mock_generate_guest_token.return_value = ("test-token", dashboards)
-
context = generate_superset_context(
context,
- user_mock,
dashboards=dashboards,
- filters=[filter_mock],
language="en_US",
)
- self.assertEqual(context["superset_token"], "test-token")
+ self.assertEqual(
+ context["superset_guest_token_url"], f"/superset_guest_token/{COURSE_ID}"
+ )
self.assertEqual(context["superset_dashboards"], dashboards)
self.assertEqual(context["superset_url"], "http://superset-dummy-url/")
+ self.assertNotIn("superset_token", context)
self.assertNotIn("exception", context)
@patch("platform_plugin_aspects.utils.SupersetClient")
- def test_generate_superset_context_with_superset_client_exception(
+ def test_generate_guest_token_with_superset_client_exception(
self, mock_superset_client
):
"""
- Test generate_superset_context
+ Test generate_guest_token
"""
- 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,
+ token, exception = generate_guest_token(
+ user=user_mock,
+ course=COURSE_ID,
dashboards=[{"name": "test", "uuid": "test-dashboard-uuid"}],
filters=[filter_mock],
)
- self.assertIn("exception", context)
+ mock_superset_client.assert_called_once()
+ self.assertIsNone(token)
+ self.assertEqual(str(exception), "test-exception")
@patch.object(
settings,
@@ -171,49 +166,26 @@ def test_generate_superset_context_with_superset_client_exception(
},
)
@patch("platform_plugin_aspects.utils.SupersetClient")
- def test_generate_superset_context_succesful(self, mock_superset_client):
+ def test_generate_guest_token_succesful(self, mock_superset_client):
"""
- Test generate_superset_context
+ Test generate_guest_token
"""
- 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",
}
- dashboards = [{"name": "test", "uuid": "test-dashboard-uuid"}]
-
- context = generate_superset_context(
- context,
- user_mock,
- dashboards=dashboards,
- filters=[filter_mock],
- )
-
- self.assertEqual(context["superset_token"], "test-token")
- self.assertEqual(context["superset_dashboards"], dashboards)
- self.assertEqual(context["superset_url"], "http://dummy-superset-url/")
-
- def test_generate_superset_context_with_exception(self):
- """
- Test generate_superset_context
- """
- course_mock = Mock()
filter_mock = Mock()
user_mock = Mock()
- user_mock.username = "test-user"
- context = {"course": course_mock}
+ dashboards = [{"name": "test", "uuid": "test-dashboard-uuid"}]
- context = generate_superset_context(
- context,
- user_mock,
- dashboards=[{"name": "test", "uuid": "test-dashboard-uuid"}],
+ token, _errors = generate_guest_token(
+ user=user_mock,
+ course=COURSE_ID,
+ dashboards=dashboards,
filters=[filter_mock],
)
- self.assertIn("exception", context)
+ mock_superset_client.assert_called_once()
+ self.assertEqual(token, "test-token")
diff --git a/platform_plugin_aspects/tests/test_views.py b/platform_plugin_aspects/tests/test_views.py
new file mode 100644
index 0000000..1b7c2c2
--- /dev/null
+++ b/platform_plugin_aspects/tests/test_views.py
@@ -0,0 +1,51 @@
+"""
+Test views.
+"""
+
+from unittest.mock import patch
+
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+from django.urls import reverse
+
+COURSE_ID = "course-v1:org+course+run"
+User = get_user_model()
+
+
+class ViewsTestCase(TestCase):
+ """
+ Test cases for the plugin views and URLs.
+ """
+
+ def setUp(self):
+ """
+ Set up data used by multiple tests.
+ """
+ super().setUp()
+ self.superset_guest_token_url = reverse(
+ "superset_guest_token",
+ kwargs={"course_id": COURSE_ID},
+ )
+ self.user = User.objects.create(
+ username="user",
+ email="user@example.com",
+ )
+ self.user.set_password("password")
+ self.user.save()
+
+ def test_guest_token_requires_authorization(self):
+ """
+ Unauthenticated hits to the endpoint redirect to login.
+ """
+ response = self.client.post(self.superset_guest_token_url)
+ self.assertEqual(response.status_code, 302)
+ self.assertIn("login", response.url)
+
+ @patch("platform_plugin_aspects.views.generate_guest_token")
+ def test_guest_token(self, mock_generate_guest_token):
+ mock_generate_guest_token.return_value = ("test-token", "test-dashboard-uuid")
+ self.client.login(username="user", password="password")
+ response = self.client.post(self.superset_guest_token_url, follow=True)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json().get("guestToken"), "test-token")
+ mock_generate_guest_token.assert_called_once()
diff --git a/platform_plugin_aspects/tests/test_xblock.py b/platform_plugin_aspects/tests/test_xblock.py
index 2355110..552713f 100644
--- a/platform_plugin_aspects/tests/test_xblock.py
+++ b/platform_plugin_aspects/tests/test_xblock.py
@@ -2,10 +2,12 @@
"""
Test basic SupersetXBlock display function
"""
+import json
from unittest import TestCase
from unittest.mock import Mock, patch
from opaque_keys.edx.locator import CourseLocator
+from webob import Request
from xblock.field_data import DictFieldData
from xblock.reference.user_service import XBlockUser
@@ -35,11 +37,21 @@ def service(block, service): # pylint: disable=unused-argument
def local_resource_url(_self, url):
return url
+ def handler_url(_self, handler, *args, **kwargs):
+ """
+ Mock runtime.handlerUrl
+
+ The LMS and CMS runtimes implement handlerUrl, but we have to mock it here.
+ """
+ return f"/{handler}"
+
runtime = Mock(
course_id=course_id,
service=service,
local_resource_url=Mock(side_effect=local_resource_url),
+ handlerUrl=Mock(side_effect=handler_url),
)
+
scope_ids = Mock()
field_data = DictFieldData(kwargs)
xblock = SupersetXBlock(runtime, field_data, scope_ids)
@@ -52,21 +64,12 @@ class TestRender(TestCase):
Test the HTML rendering of the XBlock
"""
- @patch("platform_plugin_aspects.utils.SupersetClient")
- def test_render_instructor(self, mock_superset_client):
+ def test_render_instructor(self):
"""
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(
@@ -87,14 +90,10 @@ def test_render_student(self):
@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
- ):
+ def test_render_translations(self, 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")
@@ -104,20 +103,34 @@ def test_render_translations(
url_resource = resource
self.assertIsNotNone(url_resource, "No 'url' resource found in fragment")
self.assertIn("eo/text.js", url_resource.data)
+ mock_get_language.assert_called_once()
+ mock_resource_exists.assert_called_once()
@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"
+ mock_get_language.assert_called_once()
+
+ @patch("platform_plugin_aspects.xblock.generate_guest_token")
+ def test_guest_token_handler(self, mock_generate_guest_token):
+ mock_generate_guest_token.return_value = ("test-token", "test-dashboard-uuid")
+ request = Request.blank("/")
+ request.method = "POST"
+ request.body = b"{}"
+ xblock = make_an_xblock("instructor")
+ response = xblock.get_superset_guest_token(request)
+
+ assert response.status_code == 200
+ data = json.loads(response.body.decode("utf-8"))
+ assert data.get("guestToken") == "test-token"
+ mock_generate_guest_token.assert_called_once()
diff --git a/platform_plugin_aspects/urls.py b/platform_plugin_aspects/urls.py
new file mode 100644
index 0000000..b142e4e
--- /dev/null
+++ b/platform_plugin_aspects/urls.py
@@ -0,0 +1,20 @@
+"""
+Urls for the Aspects plugin.
+"""
+
+from django.urls import re_path
+
+from . import views
+
+# Copied from openedx.core.constants
+COURSE_ID_PATTERN = r"(?P[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)"
+
+app_name = "platform_plugin_aspects"
+
+urlpatterns = [
+ re_path(
+ rf"superset_guest_token/{COURSE_ID_PATTERN}/?$",
+ views.superset_guest_token,
+ name="superset_guest_token",
+ ),
+]
diff --git a/platform_plugin_aspects/utils.py b/platform_plugin_aspects/utils.py
index 849252f..24277b2 100644
--- a/platform_plugin_aspects/utils.py
+++ b/platform_plugin_aspects/utils.py
@@ -10,6 +10,7 @@
from importlib import import_module
from django.conf import settings
+from django.urls import NoReverseMatch, reverse
from supersetapiclient.client import SupersetClient
from xblock.reference.user_service import XBlockUser
@@ -26,18 +27,16 @@ def _(text):
return text
-def generate_superset_context( # pylint: disable=dangerous-default-value
+def generate_superset_context(
context,
- user,
dashboards,
- filters=[],
language=None,
):
"""
Update context with superset token and dashboard id.
Args:
- context (dict): the context for the instructor dashboard. It must include a course object
+ context (dict): the context for the instructor dashboard. It must include a course ID
user (XBlockUser or User): the current user.
superset_config (dict): superset config.
dashboards (list): list of superset dashboard uuid.
@@ -49,50 +48,57 @@ def generate_superset_context( # pylint: disable=dangerous-default-value
if language:
for dashboard in dashboards:
- if not dashboard["allow_translations"]:
+ if not dashboard.get("allow_translations"):
continue
dashboard["slug"] = f"{dashboard['slug']}-{language}"
dashboard["uuid"] = str(get_uuid5(dashboard["uuid"], language))
- superset_token, dashboards = _generate_guest_token(
- user=user,
- course=course,
- superset_config=superset_config,
- dashboards=dashboards,
- filters=filters,
- )
+ superset_url = _fix_service_url(superset_config.get("service_url"))
- if superset_token:
- superset_url = _fix_service_url(superset_config.get("service_url"))
- context.update(
- {
- "superset_token": superset_token,
- "superset_dashboards": dashboards,
- "superset_url": superset_url,
- }
+ # FIXME -- namespace issue with plugin-registered urls?
+ try:
+ guest_token_url = reverse(
+ "platform_plugin_aspects:superset_guest_token",
+ kwargs={"course_id": course},
)
- else:
- context.update(
- {
- "exception": str(dashboards),
- }
+ except NoReverseMatch:
+ logger.error(
+ "Error reversing platform_plugin_aspects:superset_guest_token, trying without namespace"
)
+ try:
+ guest_token_url = reverse(
+ "superset_guest_token",
+ kwargs={"course_id": course},
+ )
+ logger.info("Reversing superset_guest_token worked")
+ except NoReverseMatch:
+ logger.critical("Error reversing superset_guest_token, giving up")
+ guest_token_url = ""
+
+ context.update(
+ {
+ "superset_dashboards": dashboards,
+ "superset_url": superset_url,
+ "superset_guest_token_url": guest_token_url,
+ }
+ )
return context
-def _generate_guest_token(user, course, superset_config, dashboards, filters):
+def generate_guest_token(user, course, dashboards, filters):
"""
Generate a Superset guest token for the user.
Args:
user: User object.
- course: Course object.
+ course: string Course ID
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 = _fix_service_url(
superset_config.get("internal_service_url")
or superset_config.get("service_url")
diff --git a/platform_plugin_aspects/views.py b/platform_plugin_aspects/views.py
new file mode 100644
index 0000000..38d739f
--- /dev/null
+++ b/platform_plugin_aspects/views.py
@@ -0,0 +1,48 @@
+"""
+Endpoints for the Aspects platform plugin.
+"""
+
+from django.conf import settings
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import ImproperlyConfigured
+from django.http import JsonResponse
+from django.views.decorators.http import require_POST
+from opaque_keys.edx.keys import CourseKey
+
+from .utils import _, generate_guest_token
+
+
+@require_POST
+@login_required
+def superset_guest_token(request, course_id):
+ """
+ Return a guest token for Superset that the given request.user can use to access the Superset API.
+ """
+ # TODO -- restrict access to course staff? how?
+
+ course_key = CourseKey.from_string(course_id)
+ built_in_filters = [
+ f"org = '{course_key.org}'",
+ # TODO? Need to fetch the Course object to add this filter.
+ # f"course_name = '{course_key.display_name}'",
+ f"course_run = '{course_key.run}'",
+ ]
+ dashboards = settings.ASPECTS_INSTRUCTOR_DASHBOARDS
+ extra_filters_format = settings.SUPERSET_EXTRA_FILTERS_FORMAT
+
+ guest_token, exception = generate_guest_token(
+ user=request.user,
+ course=course_id,
+ dashboards=dashboards,
+ filters=built_in_filters + extra_filters_format,
+ )
+
+ if not guest_token:
+ raise ImproperlyConfigured(
+ _(
+ "Unable to fetch Superset guest token, "
+ "mostly likely due to invalid settings.SUPERSET_CONFIG: {exception}"
+ ).format(exception=exception)
+ )
+
+ return JsonResponse({"guestToken": guest_token})
diff --git a/platform_plugin_aspects/xblock.py b/platform_plugin_aspects/xblock.py
index 969afb5..06eb462 100644
--- a/platform_plugin_aspects/xblock.py
+++ b/platform_plugin_aspects/xblock.py
@@ -2,17 +2,19 @@
from __future__ import annotations
+import json
import logging
import pkg_resources
from django.utils import translation
from web_fragments.fragment import Fragment
-from xblock.core import XBlock
+from webob import Response
+from xblock.core import JsonHandlerError, 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
+from .utils import _, generate_guest_token, generate_superset_context
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
@@ -112,12 +114,15 @@ def student_view(self, context=None):
context = generate_superset_context(
context=context,
- user=user,
dashboards=self.dashboards(),
- filters=self.filters,
)
context["xblock_id"] = str(self.scope_ids.usage_id.block_id)
+ # Use our xblock handler instead of the platform plugin's view.
+ context["superset_guest_token_url"] = self.runtime.handlerUrl(
+ self, "get_superset_guest_token"
+ )
+
frag = Fragment()
frag.add_content(self.render_template("static/html/superset.html", context))
frag.add_css(loader.load_unicode("static/css/superset.css"))
@@ -180,3 +185,33 @@ def _get_statici18n_js_url():
):
return text_js.format(locale_code=code)
return None
+
+ @XBlock.json_handler
+ def get_superset_guest_token(
+ self, request_body, suffix=""
+ ): # pylint: disable=unused-argument
+ """Return a guest token for Superset."""
+ user_service = self.runtime.service(self, "user")
+ user = user_service.get_current_user()
+
+ guest_token, exception = generate_guest_token(
+ user=user,
+ course=self.runtime.course_id,
+ dashboards=self.dashboards(),
+ filters=self.filters,
+ )
+
+ if not guest_token:
+ raise JsonHandlerError(
+ 500,
+ _(
+ "Unable to fetch Superset guest token, "
+ "mostly likely due to invalid settings.SUPERSET_CONFIG: {exception}"
+ ).format(exception=exception),
+ )
+
+ return Response(
+ json.dumps({"guestToken": guest_token}),
+ content_type="application/json",
+ charset="UTF-8",
+ )
diff --git a/requirements/test.in b/requirements/test.in
index 1bc4cb8..9c69ca6 100644
--- a/requirements/test.in
+++ b/requirements/test.in
@@ -9,3 +9,4 @@ code-annotations # provides commands used by the pii_check make target.
responses # mocks for the requests library
ddt
django-mock-queries
+xblock-sdk # for testing XBlock handlers
diff --git a/test_settings.py b/test_settings.py
index 987b008..14a50ab 100644
--- a/test_settings.py
+++ b/test_settings.py
@@ -24,9 +24,28 @@
},
}
-INSTALLED_APPS = ("platform_plugin_aspects",)
+INSTALLED_APPS = (
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "platform_plugin_aspects",
+)
+
+ROOT_URLCONF = 'platform_plugin_aspects.urls'
+
+SECRET_KEY = 'very-insecure-key'
+
+MIDDLEWARE = [
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+]
EVENT_SINK_CLICKHOUSE_MODEL_CONFIG = {
+ "auth_user": {
+ "module": "django.contrib.auth.models",
+ "model": "User",
+ },
"user_profile": {
"module": "common.djangoapps.student.models",
"model": "UserProfile",
@@ -35,6 +54,14 @@
"module": "openedx.core.djangoapps.content.course_overviews.models",
"model": "CourseOverview",
},
+ "external_id": {
+ "module": "openedx.core.djangoapps.external_user_ids.models",
+ "model": "ExternalId",
+ },
+ "user_preference": {
+ "module": "openedx.core.djangoapps.user_api.models",
+ "model": "UserPreference",
+ },
}
EVENT_SINK_CLICKHOUSE_COURSE_OVERVIEWS_ENABLED = True