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

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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
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
12 changes: 6 additions & 6 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Up @@ -6,7 +6,7 @@
from .models import Dashboard, DashboardQuery


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

Expand All @@ -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)
Expand Down
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
Expand Up @@ -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
Expand Down
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
Expand Down
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 }}"
Expand Down
30 changes: 23 additions & 7 deletions django_sql_dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 = {
Expand All @@ -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(
Expand Down Expand Up @@ -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 = [
Expand All @@ -293,6 +308,7 @@ def _dashboard_index(
"extra_qs": extra_qs,
"duration_ms": duration_ms,
"templates": templates,
"query": query_object,
}
)
finally:
Expand Down Expand Up @@ -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)
),
},
)

Expand Down
9 changes: 4 additions & 5 deletions test_project/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion test_project/test_dashboard_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
24 changes: 24 additions & 0 deletions test_project/test_save_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Up @@ -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


Expand Down Expand Up @@ -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/",
Expand Down Expand Up @@ -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


Expand Down
Loading