Skip to content
  • Sponsor
  • Notifications You must be signed in to change notification settings
  • Fork 38

Add title, description, settings and custom template to dashboard queries #156

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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
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
12 changes: 6 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -10,14 +10,14 @@ jobs:
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
postgresql-version: [12, 13, 14, 15, 16]
postgresql-version: [13, 14, 15, 16]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v2
- uses: actions/cache@v3
name: Configure pip caching
with:
path: ~/.cache/pip
@@ -47,9 +47,9 @@ jobs:
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: "3.12"
- uses: actions/cache@v2
9 changes: 4 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -8,14 +8,14 @@ jobs:
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
postgresql-version: [12, 13, 14, 15, 16]
postgresql-version: [13, 14, 15, 16]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v2
- uses: actions/cache@v3
name: Configure pip caching
with:
path: ~/.cache/pip
@@ -43,4 +43,3 @@ jobs:
pytest
- name: Check formatting
run: black . --check

10 changes: 6 additions & 4 deletions django_sql_dashboard/admin.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
from .models import Dashboard, DashboardQuery


class DashboardQueryInline(admin.TabularInline):
class DashboardQueryInline(admin.StackedInline):
model = DashboardQuery
extra = 1

@@ -16,10 +16,12 @@ def has_change_permission(self, request, obj=None):
return obj.user_can_edit(request.user)

def get_readonly_fields(self, request, obj=None):
readonly_fields = ["created_at"]
if not request.user.has_perm("django_sql_dashboard.execute_sql"):
return ("sql",)
else:
return tuple()
readonly_fields.extend(
["sql", "title", "description", "settings", "template"]
)
return readonly_fields


@admin.register(Dashboard)
54 changes: 54 additions & 0 deletions django_sql_dashboard/migrations/0005_add_description_to_query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Generated by Django 3.2.8 on 2023-04-14 16:44

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):
dependencies = [
("django_sql_dashboard", "0004_add_description_help_text"),
]

operations = [
migrations.AddField(
model_name="dashboardquery",
name="created_at",
field=models.DateTimeField(default=django.utils.timezone.now),
),
migrations.AddField(
model_name="dashboardquery",
name="description",
field=models.TextField(
blank=True, help_text="Optional description (Markdown allowed)"
),
),
migrations.AddField(
model_name="dashboardquery",
name="settings",
field=models.JSONField(
blank=True,
default=dict,
help_text="Settings for this query (JSON). These settings are passed to the template.",
null=True,
),
),
migrations.AddField(
model_name="dashboardquery",
name="template",
field=models.CharField(
blank=True,
help_text="Template to use for rendering this query. Leave blank to use the default template or fetch based on the column names.",
max_length=255,
),
),
migrations.AddField(
model_name="dashboardquery",
name="title",
field=models.CharField(blank=True, max_length=128),
),
migrations.AlterField(
model_name="dashboardquery",
name="sql",
field=models.TextField(verbose_name="SQL query"),
),
]
18 changes: 17 additions & 1 deletion django_sql_dashboard/models.py
Original file line number Diff line number Diff line change
@@ -145,7 +145,23 @@ class DashboardQuery(models.Model):
dashboard = models.ForeignKey(
Dashboard, related_name="queries", on_delete=models.CASCADE
)
sql = models.TextField()
title = models.CharField(blank=True, max_length=128)
sql = models.TextField(verbose_name="SQL query")
created_at = models.DateTimeField(default=timezone.now)
description = models.TextField(
blank=True, help_text="Optional description (Markdown allowed)"
)
template = models.CharField(
max_length=255,
blank=True,
help_text="Template to use for rendering this query. Leave blank to use the default template or fetch based on the column names.",
)
settings = models.JSONField(
blank=True,
null=True,
default=dict,
help_text="Settings for this query (JSON). These settings are passed to the template.",
)

def __str__(self):
return self.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
{% load django_sql_dashboard %}

<div class="query-results">
{% if result.query %}
{% if result.query.title %}<h2>{{ result.query.title }}</h2>{% endif %}
{% if result.query.description %}
{{ result.query.description|sql_dashboard_markdown }}
{% endif %}
{% endif %}
{% block widget_results %}{% endblock %}
<details><summary style="font-size: 0.7em; margin-bottom: 0.5em; cursor: pointer;">SQL query</summary>
{% if saved_dashboard %}<pre class="sql">{{ result.sql }}</pre>{% else %}<textarea
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{% load django_sql_dashboard %}<div class="query-results" id="query-results-{{ result.index }}">
{% if result.query %}
{% if result.query.title %}<h2>{{ result.query.title }}</h2>{% endif %}
{% if result.query.description %}
{{ result.query.description|sql_dashboard_markdown }}
{% endif %}
{% endif %}
{% if saved_dashboard %}<details><summary style="cursor: pointer;">SQL query</summary><pre class="sql">{{ result.sql }}</pre>{% else %}<textarea
name="sql"
rows="{{ result.textarea_rows }}"
30 changes: 23 additions & 7 deletions django_sql_dashboard/views.py
Original file line number Diff line number Diff line change
@@ -17,9 +17,7 @@
StreamingHttpResponse,
)
from django.shortcuts import get_object_or_404, render
from django.utils.safestring import mark_safe

from psycopg2.extensions import quote_ident
from django.template.loader import TemplateDoesNotExist, get_template, select_template

from .models import Dashboard
from .utils import (
@@ -215,6 +213,9 @@ def _dashboard_index(
results_index = -1
if sql_queries:
for sql, parameter_error in zip(sql_queries, sql_query_parameter_errors):
query_object = None
if dashboard:
query_object = dashboard.queries.filter(sql=sql).first()
results_index += 1
sql = sql.strip().rstrip(";")
base_error_result = {
@@ -230,6 +231,7 @@ def _dashboard_index(
"extra_qs": extra_qs,
"error": None,
"templates": ["django_sql_dashboard/widgets/error.html"],
"query": query_object,
}
if parameter_error:
query_results.append(
@@ -265,9 +267,22 @@ def _dashboard_index(
columns = [c.name for c in cursor.description]
template_name = ("-".join(sorted(columns))) + ".html"
if len(template_name) < 255:
try:
get_template(
"django_sql_dashboard/widgets/" + template_name
)
templates.insert(
0,
"django_sql_dashboard/widgets/" + template_name,
)
except (TemplateDoesNotExist, OSError):
pass
if query_object and query_object.template:
templates.insert(
0,
"django_sql_dashboard/widgets/" + template_name,
"django_sql_dashboard/widgets/"
+ query_object.template
+ ".html",
)
display_rows = displayable_rows(rows[:row_limit])
column_details = [
@@ -293,6 +308,7 @@ def _dashboard_index(
"extra_qs": extra_qs,
"duration_ms": duration_ms,
"templates": templates,
"query": query_object,
}
)
finally:
@@ -341,9 +357,9 @@ def _dashboard_index(
},
json_dumps_params={
"indent": 2,
"default": lambda o: o.isoformat()
if hasattr(o, "isoformat")
else str(o),
"default": lambda o: (
o.isoformat() if hasattr(o, "isoformat") else str(o)
),
},
)

9 changes: 4 additions & 5 deletions test_project/test_dashboard.py
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@
import pytest
from bs4 import BeautifulSoup
from django.core import signing
from django.db import connections

from django_sql_dashboard.utils import SQL_SALT, is_valid_base64_json, sign_sql

@@ -142,9 +141,9 @@ def test_dashboard_sql_queries(admin_client, sql, expected_columns, expected_row
assert response.status_code == 200
soup = BeautifulSoup(response.content, "html5lib")
div = soup.select(".query-results")[0]
columns = [th.text.split(" [")[0] for th in div.findAll("th")]
columns = [th.text.split(" [")[0].strip() for th in div.findAll("th")]
trs = div.find("tbody").findAll("tr")
rows = [[td.text for td in tr.findAll("td")] for tr in trs]
rows = [[td.text.strip() for td in tr.findAll("td")] for tr in trs]
assert columns == expected_columns
assert rows == expected_rows

@@ -233,8 +232,8 @@ def test_dashboard_show_available_tables(admin_client):
},
{
"table": "django_sql_dashboard_dashboardquery",
"columns": "id, sql, dashboard_id, _order",
"href_sql": "select id, sql, dashboard_id, _order from django_sql_dashboard_dashboardquery",
"columns": "id, sql, dashboard_id, _order, created_at, description, settings, template, title",
"href_sql": "select id, sql, dashboard_id, _order, created_at, description, settings, template, title from django_sql_dashboard_dashboardquery",
},
{
"table": "switches",
5 changes: 4 additions & 1 deletion test_project/test_dashboard_permissions.py
Original file line number Diff line number Diff line change
@@ -328,7 +328,10 @@ def test_user_can_edit(
assert not user.has_perm("django_sql_dashboard.execute_sql")
html = get_admin_change_form_html(client, user, dashboard_obj)
soup = BeautifulSoup(html, "html5lib")
assert soup.select("td.field-sql p")[0].text == "select 1 + 1"
assert (
soup.select("div.field-sql div.readonly")[0].text.strip()
== "select 1 + 1"
)

user.is_staff = True
user.save()
24 changes: 24 additions & 0 deletions test_project/test_save_dashboard.py
Original file line number Diff line number Diff line change
@@ -18,3 +18,27 @@ def test_save_dashboard(admin_client, dashboard_db):
dashboard = Dashboard.objects.first()
assert dashboard.slug == "one"
assert list(dashboard.queries.values_list("sql", flat=True)) == ["select 1 + 1"]


def test_save_dashboard_query(admin_client, dashboard_db):
assert Dashboard.objects.count() == 0
response = admin_client.post(
"/dashboard/",
{
"sql": "select 1 + 1",
"_save-slug": "one",
"_save-view_policy": "private",
"_save-edit_policy": "private",
},
)
assert response.status_code == 302
# Add title & description to query
dashboard = Dashboard.objects.first()

query = dashboard.queries.first()
query.title = "Query 123"
query.save()

response = admin_client.get("/dashboard/one/")
assert response.status_code == 200
assert "Query 123" in response.content.decode("utf-8")
32 changes: 28 additions & 4 deletions test_project/test_widgets.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
from bs4 import BeautifulSoup
from django.core import signing

from django_sql_dashboard.models import Dashboard
from django_sql_dashboard.utils import unsign_sql


@@ -125,6 +126,31 @@ def test_default_widget_no_count_links_for_ambiguous_columns(
assert not len(ths_with_data_count_url)


def test_custom_query_widget(admin_client, dashboard_db):
response = admin_client.post(
"/dashboard/",
{
"sql": "select '<h1>Hi</h1>' as html, 1 as number",
"_save-title": "",
"_save-slug": "test-query-template",
"_save-description": "",
"_save-view_policy": "private",
"_save-view_group": "",
"_save-edit_policy": "private",
"_save-edit_group": "",
},
follow=True,
)
dashboard = Dashboard.objects.get(slug="test-query-template")
query = dashboard.queries.first()
query.template = "html"
query.save()

response = admin_client.get("/dashboard/test-query-template/")
html = response.content.decode("utf-8")
assert "<h1>Hi</h1>" in html


def test_big_number_widget(admin_client, dashboard_db):
response = admin_client.post(
"/dashboard/",
@@ -165,15 +191,13 @@ def test_html_widget(admin_client, dashboard_db):
response = admin_client.post(
"/dashboard/",
{
"sql": "select '<h1>Hi</h1><script>alert(\"evil\")</script><p>There<br>And</p>' as markdown"
"sql": "select '<h1>Hi</h1><script>alert(\"evil\")</script><p>There<br>And</p>' as html"
},
follow=True,
)
html = response.content.decode("utf-8")
assert (
"<h1>Hi</h1>\n"
'&lt;script&gt;alert("evil")&lt;/script&gt;\n'
"<p>There<br>And</p>"
"<h1>Hi</h1>" '&lt;script&gt;alert("evil")&lt;/script&gt;' "<p>There<br>And</p>"
) in html