Skip to content

Commit

Permalink
.json export for saved dashboards (#158)
Browse files Browse the repository at this point in the history
* .json for dashboards, closes #157
* Drop Python 3.6 and 3.8, add 3.11 and 3.12
* Use PYTHONPATH to run tests in CI
* Drop PostgreSQL 10, 11 - add 14, 15, 16
* Docs for .json mode
* Apply Black to migrations
  • Loading branch information
simonw authored Dec 16, 2023
1 parent 59b71ea commit 173161b
Show file tree
Hide file tree
Showing 13 changed files with 104 additions and 12 deletions.
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}]},
],
}

0 comments on commit 173161b

Please sign in to comment.