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: allow to embed superset dashboard in instructor dashboard #192

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion aspects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)))
19 changes: 18 additions & 1 deletion aspects/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Empty file added aspects/extensions/__init__.py
Empty file.
64 changes: 64 additions & 0 deletions aspects/extensions/filters.py
Original file line number Diff line number Diff line change
@@ -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")
Empty file added aspects/settings/__init__.py
Empty file.
23 changes: 23 additions & 0 deletions aspects/settings/common.py
Original file line number Diff line number Diff line change
@@ -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 = []
19 changes: 19 additions & 0 deletions aspects/settings/production.py
Original file line number Diff line number Diff line change
@@ -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
)
5 changes: 5 additions & 0 deletions aspects/static/css/superset.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.superset-embedded-container > iframe {
height: 720px;
width: 100%;
display: block;
}
25 changes: 25 additions & 0 deletions aspects/static/html/superset.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% load i18n %}

<script src="https://unpkg.com/@superset-ui/embedded-sdk"></script>

<div class="email-notifier-instructor-wrapper" width="parent">
<h2>{{display_name}}</h2>

{% if exception %}
<p>{% trans 'Superset is not configured properly. Please contact your system administrator.'%}</p>
<p>
{{exception}}
</p>
{% elif not dashboard_uuid %}
<p>
Dashboard UUID is not set. Please set the dashboard UUID in the Studio. {{dashboard_uuid}}
</p>
{% elif superset_url and superset_token %}
<div class="superset-embedded-container" id="superset-embedded-container-{{xblock_id}}"></div>
<script type="text/javascript">
window.dashboard_uuid ="{{dashboard_uuid}}";
window.superset_url = "{{superset_url}}";
window.superset_token = "{{superset_token}}";
</script>
{% endif %}
</div>
30 changes: 30 additions & 0 deletions aspects/static/js/embed_dashboard.js
Original file line number Diff line number Diff line change
@@ -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);
}
36 changes: 36 additions & 0 deletions aspects/static/js/superset.js
Original file line number Diff line number Diff line change
@@ -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/[email protected]/bundle/index.min.js")
.done(function () {
callback();
})
.fail(function () {
console.error("Error loading supersetEmbeddedSdk.");
});
}
}
26 changes: 0 additions & 26 deletions aspects/templates/aspects/base.html

This file was deleted.

8 changes: 8 additions & 0 deletions aspects/templates/instructor_dashboard/aspects.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<%page args="section_data" expression_filter="h"/>
<%! from openedx.core.djangolib.markup import HTML %>

<%include file="/courseware/xqa_interface.html/"/>

<section class="superset">
${HTML(section_data['fragment'].body_html())}
</section>
Empty file added aspects/tests/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions aspects/tests/test_filters.py
Original file line number Diff line number Diff line change
@@ -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],
)
57 changes: 57 additions & 0 deletions aspects/tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -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"],
)
Loading
Loading