Skip to content

Commit

Permalink
Merge pull request #2 from openedx/cag/embed-superset
Browse files Browse the repository at this point in the history
feat: allow to embed superset dashboard in instructor dashboard
  • Loading branch information
bmtcril authored Mar 1, 2024
2 parents 9a3734c + 68bd822 commit bf1a2c0
Show file tree
Hide file tree
Showing 32 changed files with 786 additions and 75 deletions.
2 changes: 1 addition & 1 deletion manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django # pylint: disable=unused-import, wrong-import-position
import django # pylint: disable=unused-import
except ImportError as import_error:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
Expand Down
9 changes: 7 additions & 2 deletions platform_plugin_aspects/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
"""
Aspects plugins for edx-platform
Aspects plugins for edx-platform.
"""

__version__ = '0.1.0'
import os
from pathlib import Path

__version__ = "0.1.0"

ROOT_DIRECTORY = Path(os.path.dirname(os.path.abspath(__file__)))
21 changes: 19 additions & 2 deletions platform_plugin_aspects/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,24 @@

class PlatformPluginAspectsConfig(AppConfig):
"""
Configuration for the platform_plugin_aspects Django application.
Configuration for the aspects Django application.
"""

name = 'platform_plugin_aspects'
name = "platform_plugin_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 platform_plugin_aspects.extensions import filters # pylint: disable=unused-import, import-outside-toplevel
Empty file.
64 changes: 64 additions & 0 deletions platform_plugin_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 platform_plugin_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("platform_plugin_aspects", path)
return data.decode("utf8")
3 changes: 0 additions & 3 deletions platform_plugin_aspects/models.py

This file was deleted.

Empty file.
23 changes: 23 additions & 0 deletions platform_plugin_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 platform_plugin_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 platform_plugin_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 platform_plugin_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 platform_plugin_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 platform_plugin_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 platform_plugin_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.");
});
}
}
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>

This file was deleted.

Empty file.
46 changes: 46 additions & 0 deletions platform_plugin_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 platform_plugin_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("platform_plugin_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],
)
Loading

0 comments on commit bf1a2c0

Please sign in to comment.