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

.json export for saved dashboards #158

Merged
merged 7 commits into from
Dec 16, 2023
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
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
postgresql-version: [10, 11, 12, 13]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
postgresql-version: [12, 13, 14, 15, 16]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand Down Expand Up @@ -50,7 +50,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.10"
python-version: "3.12"
- uses: actions/cache@v2
name: Configure pip caching
with:
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
postgresql-version: [10, 11, 12, 13]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
postgresql-version: [12, 13, 14, 15, 16]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand Down Expand Up @@ -39,6 +39,7 @@ jobs:
run: |
export POSTGRESQL_PATH="/usr/lib/postgresql/$POSTGRESQL_VERSION/bin/postgres"
export INITDB_PATH="/usr/lib/postgresql/$POSTGRESQL_VERSION/bin/initdb"
export PYTHONPATH="pytest_plugins:test_project"
pytest
- name: Check formatting
run: black . --check
Expand Down
1 change: 0 additions & 1 deletion django_sql_dashboard/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@


class Migration(migrations.Migration):

initial = True

dependencies = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("django_sql_dashboard", "0001_initial"),
Expand Down
1 change: 0 additions & 1 deletion django_sql_dashboard/migrations/0003_update_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@


class Migration(migrations.Migration):

dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@


class Migration(migrations.Migration):

dependencies = [
("django_sql_dashboard", "0003_update_metadata"),
]
Expand Down
3 changes: 2 additions & 1 deletion django_sql_dashboard/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from django.urls import path

from .views import dashboard, dashboard_index
from .views import dashboard, dashboard_json, dashboard_index

urlpatterns = [
path("", dashboard_index, name="django_sql_dashboard-index"),
path("<slug>/", dashboard, name="django_sql_dashboard-dashboard"),
path("<slug>.json", dashboard_json, name="django_sql_dashboard-dashboard_json"),
]
28 changes: 27 additions & 1 deletion django_sql_dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.http.response import (
HttpResponseForbidden,
HttpResponseRedirect,
JsonResponse,
StreamingHttpResponse,
)
from django.shortcuts import get_object_or_404, render
Expand Down Expand Up @@ -137,6 +138,7 @@ def _dashboard_index(
too_long_so_use_post=False,
template="django_sql_dashboard/dashboard.html",
extra_context=None,
json_mode=False,
):
query_results = []
alias = getattr(settings, "DASHBOARD_DB_ALIAS", "dashboard")
Expand Down Expand Up @@ -329,6 +331,22 @@ def _dashboard_index(
)
]

if json_mode:
return JsonResponse(
{
"title": title or "SQL Dashboard",
"queries": [
{"sql": r["sql"], "rows": r["rows"]} for r in query_results
],
},
json_dumps_params={
"indent": 2,
"default": lambda o: o.isoformat()
if hasattr(o, "isoformat")
else str(o),
},
)

context = {
"title": title or "SQL Dashboard",
"html_title": html_title,
Expand Down Expand Up @@ -362,7 +380,14 @@ def _dashboard_index(
return response


def dashboard(request, slug):
def dashboard_json(request, slug):
disable_json = getattr(settings, "DASHBOARD_DISABLE_JSON", None)
if disable_json:
return HttpResponseForbidden("JSON export is disabled")
return dashboard(request, slug, json_mode=True)


def dashboard(request, slug, json_mode=False):
dashboard = get_object_or_404(Dashboard, slug=slug)
# Can current user see it, based on view_policy?
view_policy = dashboard.view_policy
Expand Down Expand Up @@ -398,6 +423,7 @@ def dashboard(request, slug):
description=dashboard.description,
dashboard=dashboard,
template="django_sql_dashboard/saved_dashboard.html",
json_mode=json_mode,
)


Expand Down
45 changes: 45 additions & 0 deletions docs/saved-dashboards.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,48 @@ The full list of edit policy options are:
- `superuser`: Any user who is a superuser can edit

Dashboards belong to the user who created them. Only Django super-users can re-assign ownership of dashboards to other users.

## JSON export

If your dashboard is called `/dashboards/demo/` you can add `.json` to get `/dashboards/demo.json` which will return a JSON representation of the dashboard.

The JSON format looks something like this:

```json
{
"title": "Tag word cloud",
"queries": [
{
"sql": "select \"tag\" as wordcloud_word, count(*) as wordcloud_count from (select blog_tag.tag from blog_entry_tags join blog_tag on blog_entry_tags.tag_id = blog_tag.id\r\nunion all\r\nselect blog_tag.tag from blog_blogmark_tags join blog_tag on blog_blogmark_tags.tag_id = blog_tag.id\r\nunion all\r\nselect blog_tag.tag from blog_quotation_tags join blog_tag on blog_quotation_tags.tag_id = blog_tag.id) as results where tag != 'quora' group by \"tag\" order by wordcloud_count desc",
"rows": [
{
"wordcloud_word": "python",
"wordcloud_count": 826
},
{
"wordcloud_word": "javascript",
"wordcloud_count": 604
},
{
"wordcloud_word": "django",
"wordcloud_count": 529
},
{
"wordcloud_word": "security",
"wordcloud_count": 402
},
{
"wordcloud_word": "datasette",
"wordcloud_count": 331
},
{
"wordcloud_word": "projects",
"wordcloud_count": 282
}
],
}
]
}
```

Set the `DASHBOARD_DISABLE_JSON` setting to `True` to disable this feature.
1 change: 1 addition & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ You can customize the following settings in Django's `settings.py` module:
- `DASHBOARD_ROW_LIMIT = 1000` - the maximum number of rows that can be returned from a query. This defaults to 100.
- `DASHBOARD_UPGRADE_OLD_BASE64_LINKS` - prior to version 0.8a0 SQL URLs used base64-encoded JSON. If you set this to `True` any hits that include those old URLs will be automatically redirected to the upgraded new version. Use this if you have an existing installation of `django-sql-dashboard` that people already have saved bookmarks for.
- `DASHBOARD_ENABLE_FULL_EXPORT` - set this to `True` to enable the full results CSV/TSV export feature. It defaults to `False`. Enable this feature only if you are confident that the database alias you are using does not have write permissions to anything.
- `DASHBOARD_DISABLE_JSON` - set to `True` to disable the feature where `/dashboard/name-of-dashboard.json` provides a JSON representation of the dashboard. This defaults to `False`.

## Custom templates

Expand Down
Empty file added pytest_plugins/__init__.py
Empty file.
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os

import pytest
from dj_database_url import parse
from django.conf import settings
Expand Down
23 changes: 23 additions & 0 deletions test_project/test_export.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import pytest


def test_export_requires_setting(admin_client, dashboard_db):
for key in ("export_csv_0", "export_tsv_0"):
response = admin_client.post(
Expand Down Expand Up @@ -62,3 +65,23 @@ def test_export_tsv(admin_client, dashboard_db, settings):
'attachment; filename="select--hello--as-label'
)
assert content_disposition.endswith('.tsv"')


@pytest.mark.parametrize("json_disabled", (False, True))
def test_export_json(admin_client, saved_dashboard, settings, json_disabled):
if json_disabled:
settings.DASHBOARD_DISABLE_JSON = True

response = admin_client.get("/dashboard/test.json")
if json_disabled:
assert response.status_code == 403
return
assert response.status_code == 200
assert response["Content-Type"] == "application/json"
assert response.json() == {
"title": "Test dashboard",
"queries": [
{"sql": "select 11 + 33", "rows": [{"?column?": 44}]},
{"sql": "select 22 + 55", "rows": [{"?column?": 77}]},
],
}