Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds endpoints for fetchGuestToken #27

Merged
merged 14 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion platform_plugin_aspects/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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"},
Expand Down
11 changes: 0 additions & 11 deletions platform_plugin_aspects/extensions/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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()

Expand All @@ -51,9 +42,7 @@ def run_filter(

context = generate_superset_context(
context,
user,
dashboards=dashboards,
filters=filters,
language=formatted_language,
)

Expand Down
14 changes: 11 additions & 3 deletions platform_plugin_aspects/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Signal handler functions, mapped to specific signals in apps.py.
"""

# FIXME TODO TEMPORARY WORKAROUND
# pylint: disable=unused-import
from django.db.models.signals import post_save
from django.dispatch import Signal, receiver

Expand Down Expand Up @@ -33,7 +35,9 @@ def receive_course_publish( # pylint: disable=unused-argument # pragma: no cov
dump_course_to_clickhouse.delay(str(course_key))


@receiver(post_save, sender=get_model("user_profile"))
# FIXME TODO TEMPORARY WORKAROUND
# kombu.exceptions.OperationalError: [Errno 111] Connection refused
# @receiver(post_save, sender=get_model("user_profile"))
def on_user_profile_updated( # pylint: disable=unused-argument # pragma: no cover
sender, instance, **kwargs
):
Expand All @@ -53,7 +57,9 @@ def on_user_profile_updated( # pylint: disable=unused-argument # pragma: no co
)


@receiver(post_save, sender=get_model("external_id"))
# FIXME TODO TEMPORARY WORKAROUND
# kombu.exceptions.OperationalError: [Errno 111] Connection refused
# @receiver(post_save, sender=get_model("external_id"))
def on_externalid_saved( # pylint: disable=unused-argument # pragma: no cover
sender, instance, **kwargs
):
Expand All @@ -73,7 +79,9 @@ def on_externalid_saved( # pylint: disable=unused-argument # pragma: no cover
)


@receiver(USER_RETIRE_LMS_MISC)
# FIXME TODO TEMPORARY WORKAROUND
# kombu.exceptions.OperationalError: [Errno 111] Connection refused
# @receiver(USER_RETIRE_LMS_MISC)
def on_user_retirement( # pylint: disable=unused-argument # pragma: no cover
sender, user, **kwargs
):
Expand Down
4 changes: 2 additions & 2 deletions platform_plugin_aspects/static/html/superset.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ <h2>{{display_name}}</h2>
<p>{{exception}}</p>
{% elif not superset_dashboards %}
<p>Dashboard UUID is not set. Please set the dashboard UUID in the Studio.</p>
{% elif superset_url and superset_token %} {% if xblock_id %}
{% elif superset_url and superset_guest_token_url %} {% if xblock_id %}
<div class="superset-embedded-container" id="superset-embedded-container-{{xblock_id}}"></div>
{% else %}
<div class="aspects-tabs">
Expand All @@ -31,7 +31,7 @@ <h2>{{display_name}}</h2>
<script type="text/javascript">
window.superset_dashboards = {{superset_dashboards | safe }};
window.superset_url = "{{superset_url}}";
window.superset_token = "{{superset_token}}";
window.superset_guest_token_url = "{{superset_guest_token_url}}";
</script>
{% endif %}
</div>
19 changes: 16 additions & 3 deletions platform_plugin_aspects/static/js/embed_dashboard.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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);
});
Ian2012 marked this conversation as resolved.
Show resolved Hide resolved
}
4 changes: 2 additions & 2 deletions platform_plugin_aspects/static/js/superset.js
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
78 changes: 25 additions & 53 deletions platform_plugin_aspects/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand All @@ -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")
51 changes: 51 additions & 0 deletions platform_plugin_aspects/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -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="[email protected]",
)
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()
Loading
Loading