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 #2

Merged
merged 1 commit into from
Mar 1, 2024
Merged
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
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
Loading