django-superset-integration 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_superset_integration-0.1.3/MANIFEST.in +2 -0
- django_superset_integration-0.1.3/PKG-INFO +94 -0
- django_superset_integration-0.1.3/README.md +81 -0
- django_superset_integration-0.1.3/django_superset_integration/__init__.py +0 -0
- django_superset_integration-0.1.3/django_superset_integration/admin.py +87 -0
- django_superset_integration-0.1.3/django_superset_integration/apps.py +7 -0
- django_superset_integration-0.1.3/django_superset_integration/fetch_superset_guest_token.py +129 -0
- django_superset_integration-0.1.3/django_superset_integration/forms.py +38 -0
- django_superset_integration-0.1.3/django_superset_integration/migrations/0001_initial.py +103 -0
- django_superset_integration-0.1.3/django_superset_integration/migrations/__init__.py +0 -0
- django_superset_integration-0.1.3/django_superset_integration/models.py +142 -0
- django_superset_integration-0.1.3/django_superset_integration/static/django_superset_integration/css/django-superset-integration.css +16 -0
- django_superset_integration-0.1.3/django_superset_integration/templates/django_superset_integration/superset-integration.html +72 -0
- django_superset_integration-0.1.3/django_superset_integration/urls.py +11 -0
- django_superset_integration-0.1.3/django_superset_integration.egg-info/PKG-INFO +94 -0
- django_superset_integration-0.1.3/django_superset_integration.egg-info/SOURCES.txt +18 -0
- django_superset_integration-0.1.3/django_superset_integration.egg-info/dependency_links.txt +1 -0
- django_superset_integration-0.1.3/django_superset_integration.egg-info/top_level.txt +1 -0
- django_superset_integration-0.1.3/pyproject.toml +24 -0
- django_superset_integration-0.1.3/setup.cfg +4 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-superset-integration
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A Django app to integrate Apache Superset dashboards into a Django application
|
|
5
|
+
Author-email: Alica Burlot <alica.burlot@developpement-durable.gouv.fr>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/B-Alica/django-superset-integration
|
|
8
|
+
Project-URL: Issues, https://github.com/B-Alica/django-superset-integration/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# django-superset-integration
|
|
15
|
+
|
|
16
|
+
`django-superset-integration` is a Django app to integration Apache Superset dashboards into a Django application.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
1. Add `django_superset_integration` to your `INSTALLED_APPS` setting like this:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
INSTALLED_APPS = [
|
|
24
|
+
...,
|
|
25
|
+
"django_superset_integration",
|
|
26
|
+
...,
|
|
27
|
+
]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
2. Include the superset-integration URLconf in your project `urls.py` like this:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
path("superset_integration/", include("django_superset_integration.urls")),
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. You will need a cryptography Fernet key, so you need to install cryptography:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
pip install cryptography
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
4. Generate a Fernet key in a python terminal:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from cryptography.fernet import Fernet
|
|
46
|
+
FERNET_KEY = Fernet.generate_key()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
5. The result is a bytestring like `b'jozEHFGLKJHEFUIHEZ4'`. **Copy ONLY the content of the string, not the b nor the quotation marks**
|
|
50
|
+
|
|
51
|
+
6. In your env variables, create a variable `FERNET_KEY` with the copied content as value
|
|
52
|
+
|
|
53
|
+
7. By default, all dashboard data will be displayed. You can override this by creating your own filtering function and adding it in your `settings.py`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
RLS_FUNCTION = "my_app.my_module.create_rls_clause"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Your function must take a parameter `user` and return a SQL rls clause like this : `[{"clause": "1=1"}]`
|
|
60
|
+
(where you replace 1=1 by the clause you want).
|
|
61
|
+
|
|
62
|
+
See Superset documentation for more information
|
|
63
|
+
|
|
64
|
+
8. Make sure that your Superset instance parameter `GUEST_TOKEN_JWT_EXP_SECONDS` is more than 300 (5 minutes). Otherwise it will expire before it can be refreshed. For example, set it to 600 (10 minutes).
|
|
65
|
+
|
|
66
|
+
9. In the template where you want to integrate the dashboard, add the following in your `<head>`:
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<link href="{% static 'css/ponctual-rejects.css' %}" rel="stylesheet"/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
10. Then add the following at the emplacement where you want the dashboard:
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
{% include "django_superset_integration/superset-integration.html" %}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
11. In your view's `get_context_data`, add the following:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
dashboard_name = my_dashboard
|
|
82
|
+
dashboard = SupersetDashboard.objects.get(name__iexact=dashboard_name)
|
|
83
|
+
context["dashboard_integration_id"] = dashboard.integration_id
|
|
84
|
+
context["dashboard_id"] = dashboard.id
|
|
85
|
+
context["superset_domain"] = dashboard.domain.address
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
12. Run `python manage.py migrate` to create the models.
|
|
89
|
+
|
|
90
|
+
13. Start the development server and visit the admin site to create a `SupersetInstance` object.
|
|
91
|
+
|
|
92
|
+
14. After you have created a `SupersetInstance` object, create a `SupersetDashboard` object.
|
|
93
|
+
|
|
94
|
+
15. That should be it!
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# django-superset-integration
|
|
2
|
+
|
|
3
|
+
`django-superset-integration` is a Django app to integration Apache Superset dashboards into a Django application.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
1. Add `django_superset_integration` to your `INSTALLED_APPS` setting like this:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
INSTALLED_APPS = [
|
|
11
|
+
...,
|
|
12
|
+
"django_superset_integration",
|
|
13
|
+
...,
|
|
14
|
+
]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
2. Include the superset-integration URLconf in your project `urls.py` like this:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
path("superset_integration/", include("django_superset_integration.urls")),
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
3. You will need a cryptography Fernet key, so you need to install cryptography:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
pip install cryptography
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
4. Generate a Fernet key in a python terminal:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from cryptography.fernet import Fernet
|
|
33
|
+
FERNET_KEY = Fernet.generate_key()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
5. The result is a bytestring like `b'jozEHFGLKJHEFUIHEZ4'`. **Copy ONLY the content of the string, not the b nor the quotation marks**
|
|
37
|
+
|
|
38
|
+
6. In your env variables, create a variable `FERNET_KEY` with the copied content as value
|
|
39
|
+
|
|
40
|
+
7. By default, all dashboard data will be displayed. You can override this by creating your own filtering function and adding it in your `settings.py`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
RLS_FUNCTION = "my_app.my_module.create_rls_clause"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Your function must take a parameter `user` and return a SQL rls clause like this : `[{"clause": "1=1"}]`
|
|
47
|
+
(where you replace 1=1 by the clause you want).
|
|
48
|
+
|
|
49
|
+
See Superset documentation for more information
|
|
50
|
+
|
|
51
|
+
8. Make sure that your Superset instance parameter `GUEST_TOKEN_JWT_EXP_SECONDS` is more than 300 (5 minutes). Otherwise it will expire before it can be refreshed. For example, set it to 600 (10 minutes).
|
|
52
|
+
|
|
53
|
+
9. In the template where you want to integrate the dashboard, add the following in your `<head>`:
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<link href="{% static 'css/ponctual-rejects.css' %}" rel="stylesheet"/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
10. Then add the following at the emplacement where you want the dashboard:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
{% include "django_superset_integration/superset-integration.html" %}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
11. In your view's `get_context_data`, add the following:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
dashboard_name = my_dashboard
|
|
69
|
+
dashboard = SupersetDashboard.objects.get(name__iexact=dashboard_name)
|
|
70
|
+
context["dashboard_integration_id"] = dashboard.integration_id
|
|
71
|
+
context["dashboard_id"] = dashboard.id
|
|
72
|
+
context["superset_domain"] = dashboard.domain.address
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
12. Run `python manage.py migrate` to create the models.
|
|
76
|
+
|
|
77
|
+
13. Start the development server and visit the admin site to create a `SupersetInstance` object.
|
|
78
|
+
|
|
79
|
+
14. After you have created a `SupersetInstance` object, create a `SupersetDashboard` object.
|
|
80
|
+
|
|
81
|
+
15. That should be it!
|
|
File without changes
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.contrib.admin.models import DELETION
|
|
3
|
+
from django.utils.html import escape, format_html
|
|
4
|
+
from django.urls import reverse
|
|
5
|
+
|
|
6
|
+
from .models import (
|
|
7
|
+
SupersetInstance,
|
|
8
|
+
SupersetDashboard,
|
|
9
|
+
)
|
|
10
|
+
from .forms import SupersetInstanceCreationForm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@admin.register(SupersetInstance)
|
|
14
|
+
class SupersetInstanceAdmin(admin.ModelAdmin):
|
|
15
|
+
"""
|
|
16
|
+
Admin model for SupersetInstance
|
|
17
|
+
Displayed name: "Instance Superset"
|
|
18
|
+
|
|
19
|
+
:model:`SupersetInstance`
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
add_form = SupersetInstanceCreationForm
|
|
23
|
+
model = SupersetInstance
|
|
24
|
+
|
|
25
|
+
list_display = (
|
|
26
|
+
"address",
|
|
27
|
+
"username",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
list_filter = ("username",)
|
|
31
|
+
|
|
32
|
+
search_fields = (
|
|
33
|
+
"address",
|
|
34
|
+
"username",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
ordering = (
|
|
38
|
+
"address",
|
|
39
|
+
"username",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def save_form(self, request, form, change):
|
|
43
|
+
"""
|
|
44
|
+
Override admin.ModelAdmin.save_form
|
|
45
|
+
"""
|
|
46
|
+
if not change:
|
|
47
|
+
form.instance = self.model.objects.create(
|
|
48
|
+
address=form.cleaned_data["address"],
|
|
49
|
+
username=form.cleaned_data["username"],
|
|
50
|
+
)
|
|
51
|
+
form.instance.set_password(form.cleaned_data["password"])
|
|
52
|
+
return super().save_form(request, form, change)
|
|
53
|
+
|
|
54
|
+
def get_fields(self, request, obj=None):
|
|
55
|
+
"""
|
|
56
|
+
Exclude field "password" in change form
|
|
57
|
+
"""
|
|
58
|
+
fields = super().get_fields(request, obj)
|
|
59
|
+
if obj:
|
|
60
|
+
fields.remove("password")
|
|
61
|
+
return fields
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@admin.register(SupersetDashboard)
|
|
65
|
+
class SupersetDashboardAdmin(admin.ModelAdmin):
|
|
66
|
+
"""
|
|
67
|
+
Admin model for SupersetDashboard
|
|
68
|
+
|
|
69
|
+
:model:`SupersetDashboard`
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
list_per_page = 20
|
|
73
|
+
|
|
74
|
+
list_display = (
|
|
75
|
+
"name",
|
|
76
|
+
"integration_id",
|
|
77
|
+
"domain",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
ordering = ("name",)
|
|
81
|
+
|
|
82
|
+
search_fields = (
|
|
83
|
+
"name",
|
|
84
|
+
"domain",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
list_filter = ("domain",)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import requests
|
|
3
|
+
from cryptography.fernet import Fernet
|
|
4
|
+
|
|
5
|
+
from django.http import HttpResponse
|
|
6
|
+
from django.views.decorators.http import require_safe
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
|
|
9
|
+
from .models import SupersetDashboard
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_rls_clause(user):
|
|
13
|
+
"""
|
|
14
|
+
SQL clause to apply to the dashboard data
|
|
15
|
+
Can be adjusted to limit data displayed in the dashboard
|
|
16
|
+
Clause "1=1" apply no filter, so we get all the data
|
|
17
|
+
Clause "1=0" get no data
|
|
18
|
+
|
|
19
|
+
Return format : [{"clause": "1=1"}]
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
if not user:
|
|
23
|
+
# No data
|
|
24
|
+
return [{"clause": "1=0"}]
|
|
25
|
+
|
|
26
|
+
# All data
|
|
27
|
+
return [{"clause": "1=1"}]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@require_safe
|
|
31
|
+
def fetch_superset_guest_token(request, dashboard_id: str):
|
|
32
|
+
"""
|
|
33
|
+
Get a guest token for integration of a Superset dashboard
|
|
34
|
+
1 - Get an access token
|
|
35
|
+
2 - Get a CSRF token by using the access token
|
|
36
|
+
3 - Get a guest token by using the CSRF token
|
|
37
|
+
|
|
38
|
+
Return a HttpResponse with the guest token
|
|
39
|
+
|
|
40
|
+
Parameters:
|
|
41
|
+
request
|
|
42
|
+
dashboard_id (str): SupersetDashboard object ID in database
|
|
43
|
+
(not to be confused with attribute integration_id of
|
|
44
|
+
SupersetDashboard object, which is used to integrate the
|
|
45
|
+
dashboard with SupersetEmbeddedSdk)
|
|
46
|
+
"""
|
|
47
|
+
# Get a guest_token from Superset API
|
|
48
|
+
# The guest_token allow client access to the Superset dashboard
|
|
49
|
+
with requests.Session() as session:
|
|
50
|
+
dashboard = SupersetDashboard.objects.get(id=int(dashboard_id))
|
|
51
|
+
dashboard_integration_id = dashboard.integration_id
|
|
52
|
+
superset_domain = dashboard.domain.address
|
|
53
|
+
superset_username = dashboard.domain.username
|
|
54
|
+
|
|
55
|
+
# Authentication for API access
|
|
56
|
+
url = f"https://{superset_domain}/api/v1/security/login"
|
|
57
|
+
|
|
58
|
+
def get_password(password):
|
|
59
|
+
cipher_suite = Fernet(settings.ENCRYPTION_KEY)
|
|
60
|
+
decrypted_password = cipher_suite.decrypt(password.encode())
|
|
61
|
+
return decrypted_password.decode()
|
|
62
|
+
|
|
63
|
+
params = {
|
|
64
|
+
"provider": "db",
|
|
65
|
+
"refresh": "True",
|
|
66
|
+
"username": superset_username,
|
|
67
|
+
"password": get_password(dashboard.domain.password),
|
|
68
|
+
}
|
|
69
|
+
session.headers.update({"Content-Type": "application/json"})
|
|
70
|
+
response = session.post(url, json=params)
|
|
71
|
+
|
|
72
|
+
access_token = response.json()["access_token"]
|
|
73
|
+
|
|
74
|
+
session.headers.update({"Authorization": f"Bearer {access_token}"})
|
|
75
|
+
url = f"https://{superset_domain}/api/v1/security/csrf_token/"
|
|
76
|
+
response = session.get(url)
|
|
77
|
+
csrf_token = response.json()["result"]
|
|
78
|
+
|
|
79
|
+
cookie = response.headers["set-cookie"].split("; ")[0]
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
user = request.user
|
|
83
|
+
except Exception:
|
|
84
|
+
# If no user, no guest_token
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# SQL clause to apply to the dashboard data
|
|
88
|
+
# Can be adjusted to limit data displayed in the dashboard
|
|
89
|
+
# Clause "1=1" apply no filter, so we get all the data
|
|
90
|
+
# Clause "1=0" get no data
|
|
91
|
+
# the default function can be overridden by creating
|
|
92
|
+
# your own and setting its path in settings.RLS_FUNCTION
|
|
93
|
+
rls_function_path = getattr(settings, "RLS_FUNCTION", None)
|
|
94
|
+
if rls_function_path:
|
|
95
|
+
# Dynamically import the function
|
|
96
|
+
module_path, func_name = rls_function_path.rsplit(".", 1)
|
|
97
|
+
module = importlib.import_module(module_path)
|
|
98
|
+
rls_function = getattr(module, func_name)
|
|
99
|
+
else:
|
|
100
|
+
rls_function = create_rls_clause
|
|
101
|
+
|
|
102
|
+
rls = rls_function(user)
|
|
103
|
+
|
|
104
|
+
if not rls:
|
|
105
|
+
# If no rls clause, show no data
|
|
106
|
+
rls = [{"clause": "1=0"}]
|
|
107
|
+
|
|
108
|
+
# Get guest_token to display the dashboard
|
|
109
|
+
params = {
|
|
110
|
+
"resources": [
|
|
111
|
+
{"id": dashboard_integration_id, "type": "dashboard"}
|
|
112
|
+
],
|
|
113
|
+
"rls": rls,
|
|
114
|
+
"user": {
|
|
115
|
+
"first_name": "Prenom",
|
|
116
|
+
"last_name": "Nom",
|
|
117
|
+
"username": superset_username,
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
headers = {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
"X-Csrftoken": csrf_token,
|
|
123
|
+
"Cookie": cookie,
|
|
124
|
+
}
|
|
125
|
+
session.headers.update(headers)
|
|
126
|
+
url = f"https://{superset_domain}/api/v1/security/guest_token/"
|
|
127
|
+
response = session.post(url, json=params)
|
|
128
|
+
guest_token = response.json()["token"]
|
|
129
|
+
return HttpResponse(guest_token)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
from .models import SupersetInstance
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SupersetInstanceCreationForm(forms.ModelForm):
|
|
8
|
+
"""
|
|
9
|
+
SupersetInstance creation form
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
address = forms.CharField(
|
|
13
|
+
widget=forms.TextInput(
|
|
14
|
+
attrs={"autofocus": True, "required": True, "id": "address-field"}
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
username = forms.CharField(
|
|
19
|
+
widget=forms.TextInput(
|
|
20
|
+
attrs={"required": True, "id": "username-field"}
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
password = forms.CharField(
|
|
25
|
+
label=_("Password"),
|
|
26
|
+
strip=False,
|
|
27
|
+
widget=forms.PasswordInput(
|
|
28
|
+
attrs={"required": True, id: "password-field"}
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
class Meta:
|
|
33
|
+
model = SupersetInstance
|
|
34
|
+
fields = (
|
|
35
|
+
"address",
|
|
36
|
+
"username",
|
|
37
|
+
"password",
|
|
38
|
+
)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Generated by Django 5.1.4 on 2025-03-03 12:27
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name="SupersetInstance",
|
|
16
|
+
fields=[
|
|
17
|
+
(
|
|
18
|
+
"id",
|
|
19
|
+
models.BigAutoField(
|
|
20
|
+
auto_created=True,
|
|
21
|
+
primary_key=True,
|
|
22
|
+
serialize=False,
|
|
23
|
+
verbose_name="ID",
|
|
24
|
+
),
|
|
25
|
+
),
|
|
26
|
+
(
|
|
27
|
+
"address",
|
|
28
|
+
models.CharField(
|
|
29
|
+
max_length=250,
|
|
30
|
+
unique=True,
|
|
31
|
+
verbose_name="adresse de l'instance Superset",
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
(
|
|
35
|
+
"username",
|
|
36
|
+
models.CharField(
|
|
37
|
+
default="superset_api",
|
|
38
|
+
max_length=250,
|
|
39
|
+
verbose_name="nom d'utilisateur",
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
(
|
|
43
|
+
"password",
|
|
44
|
+
models.CharField(
|
|
45
|
+
blank=True,
|
|
46
|
+
max_length=128,
|
|
47
|
+
null=True,
|
|
48
|
+
verbose_name="mot de passe",
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
],
|
|
52
|
+
),
|
|
53
|
+
migrations.CreateModel(
|
|
54
|
+
name="SupersetDashboard",
|
|
55
|
+
fields=[
|
|
56
|
+
(
|
|
57
|
+
"id",
|
|
58
|
+
models.BigAutoField(
|
|
59
|
+
auto_created=True,
|
|
60
|
+
primary_key=True,
|
|
61
|
+
serialize=False,
|
|
62
|
+
verbose_name="ID",
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
(
|
|
66
|
+
"integration_id",
|
|
67
|
+
models.CharField(
|
|
68
|
+
max_length=50,
|
|
69
|
+
unique=True,
|
|
70
|
+
verbose_name="ID d'intégration",
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
(
|
|
74
|
+
"name",
|
|
75
|
+
models.CharField(
|
|
76
|
+
max_length=250, unique=True, verbose_name="Nom"
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
"comment",
|
|
81
|
+
models.TextField(
|
|
82
|
+
blank=True, null=True, verbose_name="Commentaire"
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
(
|
|
86
|
+
"superset_link",
|
|
87
|
+
models.CharField(
|
|
88
|
+
blank=True,
|
|
89
|
+
max_length=500,
|
|
90
|
+
null=True,
|
|
91
|
+
verbose_name="Lien vers le dashboard dans Superset",
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
(
|
|
95
|
+
"domain",
|
|
96
|
+
models.ForeignKey(
|
|
97
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
98
|
+
to="superset_integration.supersetinstance",
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
],
|
|
102
|
+
),
|
|
103
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.contrib.admin.models import LogEntry
|
|
4
|
+
from django.db.models.signals import pre_save
|
|
5
|
+
from django.dispatch import receiver
|
|
6
|
+
|
|
7
|
+
from cryptography.fernet import Fernet
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SupersetInstance(models.Model):
|
|
11
|
+
"""
|
|
12
|
+
SupersetInstance model containing your Superset instance's address
|
|
13
|
+
and the username to access it
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
Attributes
|
|
17
|
+
----------
|
|
18
|
+
address: str
|
|
19
|
+
Url address of your Superset instance
|
|
20
|
+
|
|
21
|
+
username: str
|
|
22
|
+
Username to access your Superset instance
|
|
23
|
+
By default: "superset_api"
|
|
24
|
+
|
|
25
|
+
password: str
|
|
26
|
+
Password to access your Superset instance
|
|
27
|
+
|
|
28
|
+
Methods
|
|
29
|
+
----------
|
|
30
|
+
set_password(self, raw_password):
|
|
31
|
+
Set password in password field after hashing it with django.contrib.auth.hashers.make_password
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
address = models.CharField(
|
|
35
|
+
"adresse de l'instance Superset",
|
|
36
|
+
max_length=250,
|
|
37
|
+
blank=False,
|
|
38
|
+
null=False,
|
|
39
|
+
unique=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
username = models.CharField(
|
|
43
|
+
"nom d'utilisateur",
|
|
44
|
+
max_length=250,
|
|
45
|
+
blank=False,
|
|
46
|
+
null=False,
|
|
47
|
+
default="superset_api",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
password = models.CharField(
|
|
51
|
+
"mot de passe",
|
|
52
|
+
max_length=128,
|
|
53
|
+
blank=True,
|
|
54
|
+
null=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
return f"Instance Superset : {self.address}"
|
|
59
|
+
|
|
60
|
+
def set_password(self, raw_password):
|
|
61
|
+
cipher_suite = Fernet(settings.ENCRYPTION_KEY)
|
|
62
|
+
encrypted_password = cipher_suite.encrypt(raw_password.encode())
|
|
63
|
+
self.password = encrypted_password.decode()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@receiver(pre_save, sender=SupersetInstance)
|
|
67
|
+
def superset_instance_pre_save_handler(sender, instance, *args, **kwargs):
|
|
68
|
+
"""
|
|
69
|
+
SupersetInstance.address should not begin with "http://" or "https://"
|
|
70
|
+
so we remove this part if necessary
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if instance.address.startswith("http://") or instance.address.startswith(
|
|
74
|
+
"https://"
|
|
75
|
+
):
|
|
76
|
+
instance.address = instance.address.split("//", 1)[1]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SupersetDashboard(models.Model):
|
|
80
|
+
"""
|
|
81
|
+
SupersetDashboard containing the integration ID and
|
|
82
|
+
name of your Superset dashboard
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
Attributes
|
|
86
|
+
----------
|
|
87
|
+
integration_id: str
|
|
88
|
+
Integratrion ID of your Superset dashboard
|
|
89
|
+
Format : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
90
|
+
where x are numbers and letters
|
|
91
|
+
|
|
92
|
+
name: str
|
|
93
|
+
Name of the dashboard
|
|
94
|
+
|
|
95
|
+
domain: SupersetInstance
|
|
96
|
+
Foreign key
|
|
97
|
+
Superset instance containing the dashboard
|
|
98
|
+
|
|
99
|
+
comment: str
|
|
100
|
+
Free text field for your comments
|
|
101
|
+
|
|
102
|
+
superset_link: str
|
|
103
|
+
Link to the dashboard in Superset
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
integration_id = models.CharField(
|
|
107
|
+
"ID d'intégration",
|
|
108
|
+
max_length=50,
|
|
109
|
+
blank=False,
|
|
110
|
+
null=False,
|
|
111
|
+
unique=True,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
name = models.CharField(
|
|
115
|
+
"Nom",
|
|
116
|
+
max_length=250,
|
|
117
|
+
blank=False,
|
|
118
|
+
null=False,
|
|
119
|
+
unique=True,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
domain = models.ForeignKey(
|
|
123
|
+
SupersetInstance,
|
|
124
|
+
on_delete=models.CASCADE,
|
|
125
|
+
null=False,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
comment = models.TextField(
|
|
129
|
+
"Commentaire",
|
|
130
|
+
blank=True,
|
|
131
|
+
null=True,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
superset_link = models.CharField(
|
|
135
|
+
"Lien vers le dashboard dans Superset",
|
|
136
|
+
max_length=500,
|
|
137
|
+
blank=True,
|
|
138
|
+
null=True,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def __str__(self):
|
|
142
|
+
return f"Dashboard : {self.name}"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{% load static %}
|
|
2
|
+
|
|
3
|
+
<link href="{% static 'superset_integration/css/superset-integration.css' %}" rel="stylesheet"/>
|
|
4
|
+
|
|
5
|
+
<button id="superset-fullscreen" class="fr-btn fr-mb-3w">
|
|
6
|
+
Afficher en plein écran
|
|
7
|
+
</button>
|
|
8
|
+
|
|
9
|
+
<button id="superset-fullscreen-exit" class="fr-btn">
|
|
10
|
+
Quitter le mode plein écran
|
|
11
|
+
</button>
|
|
12
|
+
|
|
13
|
+
<div id="superset-integration">
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<script src="{% static 'js/superset-embedded-sdk.js' %}"></script>
|
|
17
|
+
<script>
|
|
18
|
+
// Get guest token from django app backend
|
|
19
|
+
// The token has a lifetime of 5 min. This function is called
|
|
20
|
+
// periodically to refresh the token
|
|
21
|
+
async function fetchGuestTokenFromBackend() {
|
|
22
|
+
const response = await fetch('/superset_integration/guest_token/{{ dashboard_id }}');
|
|
23
|
+
const token = await response.text();
|
|
24
|
+
return token;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Display dashboard inside #graph element
|
|
28
|
+
supersetEmbeddedSdk.embedDashboard({
|
|
29
|
+
id: "{{ dashboard_integration_id }}",
|
|
30
|
+
supersetDomain: "https://" + "{{ superset_domain }}",
|
|
31
|
+
mountPoint: document.getElementById("superset-integration"),
|
|
32
|
+
fetchGuestToken: () => fetchGuestTokenFromBackend(),
|
|
33
|
+
dashboardUiConfig: {
|
|
34
|
+
hideTitle: true,
|
|
35
|
+
filters: {
|
|
36
|
+
expanded: true,
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let iframe = document.getElementById("superset-integration").firstElementChild;
|
|
42
|
+
let iframe_height = "1200px";
|
|
43
|
+
|
|
44
|
+
$(window).on('load',function () {
|
|
45
|
+
{% if iframe_height and iframe_height.isdigit %}
|
|
46
|
+
iframe_height = "{{ iframe_height }}px";
|
|
47
|
+
{% elif iframe_height %}
|
|
48
|
+
iframe_height = "{{ iframe_height }}";
|
|
49
|
+
{% endif %}
|
|
50
|
+
iframe.height = iframe_height;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
$("#superset-fullscreen").click(function(ev)
|
|
54
|
+
{
|
|
55
|
+
$(".fr-header").hide();
|
|
56
|
+
$(".fr-footer").hide();
|
|
57
|
+
$("#superset-fullscreen-exit").show();
|
|
58
|
+
$("#superset-fullscreen-exit").css({visibility: "visible"});
|
|
59
|
+
$("#superset-fullscreen-exit").css({opacity: 100});
|
|
60
|
+
$("#superset-integration").addClass("fullScreen");
|
|
61
|
+
iframe.height = "100%";
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
$("#superset-fullscreen-exit").click(function(ev)
|
|
65
|
+
{
|
|
66
|
+
$(".fr-header").show();
|
|
67
|
+
$(".fr-footer").show();
|
|
68
|
+
$("#superset-fullscreen-exit").hide();
|
|
69
|
+
$("#superset-integration").removeClass("fullScreen");
|
|
70
|
+
iframe.height = iframe_height;
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-superset-integration
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A Django app to integrate Apache Superset dashboards into a Django application
|
|
5
|
+
Author-email: Alica Burlot <alica.burlot@developpement-durable.gouv.fr>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/B-Alica/django-superset-integration
|
|
8
|
+
Project-URL: Issues, https://github.com/B-Alica/django-superset-integration/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# django-superset-integration
|
|
15
|
+
|
|
16
|
+
`django-superset-integration` is a Django app to integration Apache Superset dashboards into a Django application.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
1. Add `django_superset_integration` to your `INSTALLED_APPS` setting like this:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
INSTALLED_APPS = [
|
|
24
|
+
...,
|
|
25
|
+
"django_superset_integration",
|
|
26
|
+
...,
|
|
27
|
+
]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
2. Include the superset-integration URLconf in your project `urls.py` like this:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
path("superset_integration/", include("django_superset_integration.urls")),
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. You will need a cryptography Fernet key, so you need to install cryptography:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
pip install cryptography
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
4. Generate a Fernet key in a python terminal:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from cryptography.fernet import Fernet
|
|
46
|
+
FERNET_KEY = Fernet.generate_key()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
5. The result is a bytestring like `b'jozEHFGLKJHEFUIHEZ4'`. **Copy ONLY the content of the string, not the b nor the quotation marks**
|
|
50
|
+
|
|
51
|
+
6. In your env variables, create a variable `FERNET_KEY` with the copied content as value
|
|
52
|
+
|
|
53
|
+
7. By default, all dashboard data will be displayed. You can override this by creating your own filtering function and adding it in your `settings.py`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
RLS_FUNCTION = "my_app.my_module.create_rls_clause"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Your function must take a parameter `user` and return a SQL rls clause like this : `[{"clause": "1=1"}]`
|
|
60
|
+
(where you replace 1=1 by the clause you want).
|
|
61
|
+
|
|
62
|
+
See Superset documentation for more information
|
|
63
|
+
|
|
64
|
+
8. Make sure that your Superset instance parameter `GUEST_TOKEN_JWT_EXP_SECONDS` is more than 300 (5 minutes). Otherwise it will expire before it can be refreshed. For example, set it to 600 (10 minutes).
|
|
65
|
+
|
|
66
|
+
9. In the template where you want to integrate the dashboard, add the following in your `<head>`:
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<link href="{% static 'css/ponctual-rejects.css' %}" rel="stylesheet"/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
10. Then add the following at the emplacement where you want the dashboard:
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
{% include "django_superset_integration/superset-integration.html" %}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
11. In your view's `get_context_data`, add the following:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
dashboard_name = my_dashboard
|
|
82
|
+
dashboard = SupersetDashboard.objects.get(name__iexact=dashboard_name)
|
|
83
|
+
context["dashboard_integration_id"] = dashboard.integration_id
|
|
84
|
+
context["dashboard_id"] = dashboard.id
|
|
85
|
+
context["superset_domain"] = dashboard.domain.address
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
12. Run `python manage.py migrate` to create the models.
|
|
89
|
+
|
|
90
|
+
13. Start the development server and visit the admin site to create a `SupersetInstance` object.
|
|
91
|
+
|
|
92
|
+
14. After you have created a `SupersetInstance` object, create a `SupersetDashboard` object.
|
|
93
|
+
|
|
94
|
+
15. That should be it!
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
django_superset_integration/__init__.py
|
|
5
|
+
django_superset_integration/admin.py
|
|
6
|
+
django_superset_integration/apps.py
|
|
7
|
+
django_superset_integration/fetch_superset_guest_token.py
|
|
8
|
+
django_superset_integration/forms.py
|
|
9
|
+
django_superset_integration/models.py
|
|
10
|
+
django_superset_integration/urls.py
|
|
11
|
+
django_superset_integration.egg-info/PKG-INFO
|
|
12
|
+
django_superset_integration.egg-info/SOURCES.txt
|
|
13
|
+
django_superset_integration.egg-info/dependency_links.txt
|
|
14
|
+
django_superset_integration.egg-info/top_level.txt
|
|
15
|
+
django_superset_integration/migrations/0001_initial.py
|
|
16
|
+
django_superset_integration/migrations/__init__.py
|
|
17
|
+
django_superset_integration/static/django_superset_integration/css/django-superset-integration.css
|
|
18
|
+
django_superset_integration/templates/django_superset_integration/superset-integration.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django_superset_integration
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "django-superset-integration"
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
description = "A Django app to integrate Apache Superset dashboards into a Django application"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Alica Burlot",email = "alica.burlot@developpement-durable.gouv.fr"}
|
|
11
|
+
]
|
|
12
|
+
license = "GPL-3.0-or-later"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
dependencies = [
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/B-Alica/django-superset-integration"
|
|
24
|
+
Issues = "https://github.com/B-Alica/django-superset-integration/issues"
|