From bc4d95b06a28ccb577c143a0307a7c273ba33b71 Mon Sep 17 00:00:00 2001 From: David Kane Date: Wed, 13 Oct 2021 10:21:43 +0100 Subject: [PATCH 01/20] add new fields to models --- django_sql_dashboard/admin.py | 2 +- .../migrations/0005_auto_20211013_0953.py | 34 +++++++++++++++++++ django_sql_dashboard/models.py | 6 ++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 django_sql_dashboard/migrations/0005_auto_20211013_0953.py diff --git a/django_sql_dashboard/admin.py b/django_sql_dashboard/admin.py index c9fef63..8a502a8 100644 --- a/django_sql_dashboard/admin.py +++ b/django_sql_dashboard/admin.py @@ -17,7 +17,7 @@ def has_change_permission(self, request, obj=None): def get_readonly_fields(self, request, obj=None): if not request.user.has_perm("django_sql_dashboard.execute_sql"): - return ("sql",) + return ("sql", "title", "description", "settings", ) else: return tuple() diff --git a/django_sql_dashboard/migrations/0005_auto_20211013_0953.py b/django_sql_dashboard/migrations/0005_auto_20211013_0953.py new file mode 100644 index 0000000..76d054f --- /dev/null +++ b/django_sql_dashboard/migrations/0005_auto_20211013_0953.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.8 on 2021-10-13 08:53 + +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, null=True), + ), + migrations.AddField( + model_name='dashboardquery', + name='title', + field=models.CharField(blank=True, max_length=128), + ), + ] diff --git a/django_sql_dashboard/models.py b/django_sql_dashboard/models.py index 3dd7007..ae7cb10 100644 --- a/django_sql_dashboard/models.py +++ b/django_sql_dashboard/models.py @@ -145,6 +145,12 @@ class DashboardQuery(models.Model): dashboard = models.ForeignKey( Dashboard, related_name="queries", on_delete=models.CASCADE ) + title = models.CharField(blank=True, max_length=128) + description = models.TextField( + blank=True, help_text="Optional description (Markdown allowed)" + ) + settings = models.JSONField(blank=True, null=True) + created_at = models.DateTimeField(default=timezone.now) sql = models.TextField() def __str__(self): From 65c1416021469132e2d2c66ce503d2c313ec050c Mon Sep 17 00:00:00 2001 From: David Kane Date: Wed, 13 Oct 2021 12:23:17 +0100 Subject: [PATCH 02/20] inital changes for dashboard queries --- django_sql_dashboard/admin.py | 7 +++- .../migrations/0005_auto_20211013_0953.py | 22 ++++++------ .../widgets/_base_widget.html | 4 +++ .../django_sql_dashboard/widgets/default.html | 20 +++++++++-- django_sql_dashboard/views.py | 36 +++++++++++++++++-- 5 files changed, 72 insertions(+), 17 deletions(-) diff --git a/django_sql_dashboard/admin.py b/django_sql_dashboard/admin.py index 8a502a8..731b9e5 100644 --- a/django_sql_dashboard/admin.py +++ b/django_sql_dashboard/admin.py @@ -17,7 +17,12 @@ def has_change_permission(self, request, obj=None): def get_readonly_fields(self, request, obj=None): if not request.user.has_perm("django_sql_dashboard.execute_sql"): - return ("sql", "title", "description", "settings", ) + return ( + "sql", + "title", + "description", + "settings", + ) else: return tuple() diff --git a/django_sql_dashboard/migrations/0005_auto_20211013_0953.py b/django_sql_dashboard/migrations/0005_auto_20211013_0953.py index 76d054f..f1693c5 100644 --- a/django_sql_dashboard/migrations/0005_auto_20211013_0953.py +++ b/django_sql_dashboard/migrations/0005_auto_20211013_0953.py @@ -7,28 +7,30 @@ class Migration(migrations.Migration): dependencies = [ - ('django_sql_dashboard', '0004_add_description_help_text'), + ("django_sql_dashboard", "0004_add_description_help_text"), ] operations = [ migrations.AddField( - model_name='dashboardquery', - name='created_at', + 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)'), + model_name="dashboardquery", + name="description", + field=models.TextField( + blank=True, help_text="Optional description (Markdown allowed)" + ), ), migrations.AddField( - model_name='dashboardquery', - name='settings', + model_name="dashboardquery", + name="settings", field=models.JSONField(blank=True, null=True), ), migrations.AddField( - model_name='dashboardquery', - name='title', + model_name="dashboardquery", + name="title", field=models.CharField(blank=True, max_length=128), ), ] diff --git a/django_sql_dashboard/templates/django_sql_dashboard/widgets/_base_widget.html b/django_sql_dashboard/templates/django_sql_dashboard/widgets/_base_widget.html index 828595a..9082b53 100644 --- a/django_sql_dashboard/templates/django_sql_dashboard/widgets/_base_widget.html +++ b/django_sql_dashboard/templates/django_sql_dashboard/widgets/_base_widget.html @@ -1,4 +1,8 @@
+ {% if result.title %}

{{ result.title }}

{% endif %} + {% if result.description %} + {{ result.description|sql_dashboard_markdown }} + {% endif %} {% block widget_results %}{% endblock %}
SQL query {% if saved_dashboard %}
{{ result.sql }}
{% else %}{% endif %} + >{{ result.sql|default:"" }} + {% endif %} {% if not saved_dashboard %}

-

{% else %}
{% endif %} +

+ {% else %} + + {% endif %} {% if result.truncated %}

Results were truncated diff --git a/django_sql_dashboard/views.py b/django_sql_dashboard/views.py index c51bcef..6b16aa8 100644 --- a/django_sql_dashboard/views.py +++ b/django_sql_dashboard/views.py @@ -8,8 +8,9 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.db import connections +from django.db.models import query from django.db.utils import ProgrammingError -from django.forms import CharField, ModelForm, Textarea +from django.forms import CharField, ModelForm, Textarea, inlineformset_factory from django.http.response import ( HttpResponseForbidden, HttpResponseRedirect, @@ -20,7 +21,7 @@ from psycopg2.extensions import quote_ident -from .models import Dashboard +from .models import Dashboard, DashboardQuery from .utils import ( apply_sort, check_for_base64_upgrade, @@ -58,6 +59,26 @@ class Meta: } +DashboardQueryFormSet = inlineformset_factory( + Dashboard, + DashboardQuery, + fields=( + "title", + "description", + "sql", + ), + widgets={ + "description": Textarea( + attrs={ + "placeholder": "Optional description, markdown allowed", + "rows": 4, + } + ) + }, + # extra=1, +) + + @login_required def dashboard_index(request): if not request.user.has_perm("django_sql_dashboard.execute_sql"): @@ -65,6 +86,7 @@ def dashboard_index(request): sql_queries = [] too_long_so_use_post = False save_form = SaveDashboardForm(prefix="_save") + save_query_form = DashboardQueryFormSet(prefix="_save_query") if request.method == "POST": # Is this an export? if any( @@ -114,6 +136,14 @@ def dashboard_index(request): sql_queries.append(sql) else: unverified_sql_queries.append(sql) + save_query_form_data = { + '_save_query-TOTAL_FORMS': str(len(sql_queries) + 1), + '_save_query-INITIAL_FORMS': str(0), + } + # for k, sql in enumerate(sql_queries): + # save_query_form_data[f"_save_query-{k}-sql"] = sql + save_query_form = DashboardQueryFormSet(initial=[{"sql": sql} for sql in sql_queries], prefix="_save_query") + print(save_query_form.as_table()) if getattr(settings, "DASHBOARD_UPGRADE_OLD_BASE64_LINKS", None): redirect_querystring = check_for_base64_upgrade(sql_queries) if redirect_querystring: @@ -123,7 +153,7 @@ def dashboard_index(request): sql_queries, unverified_sql_queries=unverified_sql_queries, too_long_so_use_post=too_long_so_use_post, - extra_context={"save_form": save_form}, + extra_context={"save_form": save_form, "save_query_form": save_query_form}, ) From 7134d0723be0a60e54aa5f2ff6dddad081fcab8e Mon Sep 17 00:00:00 2001 From: David Kane Date: Fri, 14 Apr 2023 16:09:02 +0100 Subject: [PATCH 03/20] improve admin editing of models revert formset --- django_sql_dashboard/admin.py | 2 +- .../migrations/0006_auto_20230414_1540.py | 28 ++++ django_sql_dashboard/models.py | 16 +- .../django_sql_dashboard/widgets/default.html | 142 +++++++----------- django_sql_dashboard/views.py | 49 ++---- 5 files changed, 111 insertions(+), 126 deletions(-) create mode 100644 django_sql_dashboard/migrations/0006_auto_20230414_1540.py diff --git a/django_sql_dashboard/admin.py b/django_sql_dashboard/admin.py index 731b9e5..4e93060 100644 --- a/django_sql_dashboard/admin.py +++ b/django_sql_dashboard/admin.py @@ -6,7 +6,7 @@ from .models import Dashboard, DashboardQuery -class DashboardQueryInline(admin.TabularInline): +class DashboardQueryInline(admin.StackedInline): model = DashboardQuery extra = 1 diff --git a/django_sql_dashboard/migrations/0006_auto_20230414_1540.py b/django_sql_dashboard/migrations/0006_auto_20230414_1540.py new file mode 100644 index 0000000..0aaf8f0 --- /dev/null +++ b/django_sql_dashboard/migrations/0006_auto_20230414_1540.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.8 on 2023-04-14 14:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_sql_dashboard', '0005_auto_20211013_0953'), + ] + + operations = [ + 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.AlterField( + 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.AlterField( + model_name='dashboardquery', + name='sql', + field=models.TextField(verbose_name='SQL query'), + ), + ] diff --git a/django_sql_dashboard/models.py b/django_sql_dashboard/models.py index ae7cb10..583ca45 100644 --- a/django_sql_dashboard/models.py +++ b/django_sql_dashboard/models.py @@ -146,12 +146,22 @@ class DashboardQuery(models.Model): Dashboard, related_name="queries", on_delete=models.CASCADE ) 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)" ) - settings = models.JSONField(blank=True, null=True) - created_at = models.DateTimeField(default=timezone.now) - sql = models.TextField() + 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 diff --git a/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html b/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html index 6cd1823..c353ed9 100644 --- a/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html +++ b/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html @@ -1,51 +1,28 @@ {% load django_sql_dashboard %}

- {% if saved_dashboard %} - {% if result.title %}

{{ result.title }}

{% endif %} - {% if result.description %} - {{ result.description|sql_dashboard_markdown }} - {% endif %} -
- SQL query -
{{ result.sql }}
- {% else %} - {{ save_query_form.as_p }} - - {% endif %} - {% if not saved_dashboard %}

- -

- {% else %} -
+ {% if result.query %} + {% if result.query.title %}

{{ result.query.title }}

{% endif %} + {% if result.query.description %}{{ result.query.description|sql_dashboard_markdown }}{% endif %} {% endif %} + {% if saved_dashboard %}
+ SQL query +
{{ result.sql }}
{% else %}{% endif %} + {% if not saved_dashboard %}

+ +

{% else %} +
{% endif %} {% if result.truncated %} -

- Results were truncated - {% if user_can_export_data and not saved_dashboard %} - - - {% endif %} -

+

+ Results were truncated + {% if user_can_export_data and not saved_dashboard %} + + + {% endif %} +

{% else %} -

{{ result.row_lists|length }} row{{ result.row_lists|length|pluralize }}

+

{{ result.row_lists|length }} row{{ result.row_lists|length|pluralize }}

{% endif %} {% if result.error %}

@@ -56,13 +33,15 @@ {% for column in result.column_details %} - {{ column.name }} + + {{ column.name }} {% endfor %} @@ -76,47 +55,38 @@ {% endfor %} -

Copy and export data +
+ Copy and export data {% if user_can_export_data and not saved_dashboard %} -
- - -
+
+ + +
{% endif %}

Duration: {{ result.duration_ms|floatformat:2 }}ms

-
+
\ No newline at end of file diff --git a/django_sql_dashboard/views.py b/django_sql_dashboard/views.py index 6b16aa8..f5a111e 100644 --- a/django_sql_dashboard/views.py +++ b/django_sql_dashboard/views.py @@ -8,20 +8,16 @@ from django.conf import settings from django.contrib.auth.decorators import login_required from django.db import connections -from django.db.models import query from django.db.utils import ProgrammingError -from django.forms import CharField, ModelForm, Textarea, inlineformset_factory +from django.forms import CharField, ModelForm, Textarea from django.http.response import ( HttpResponseForbidden, HttpResponseRedirect, StreamingHttpResponse, ) from django.shortcuts import get_object_or_404, render -from django.utils.safestring import mark_safe -from psycopg2.extensions import quote_ident - -from .models import Dashboard, DashboardQuery +from .models import Dashboard from .utils import ( apply_sort, check_for_base64_upgrade, @@ -59,26 +55,6 @@ class Meta: } -DashboardQueryFormSet = inlineformset_factory( - Dashboard, - DashboardQuery, - fields=( - "title", - "description", - "sql", - ), - widgets={ - "description": Textarea( - attrs={ - "placeholder": "Optional description, markdown allowed", - "rows": 4, - } - ) - }, - # extra=1, -) - - @login_required def dashboard_index(request): if not request.user.has_perm("django_sql_dashboard.execute_sql"): @@ -86,7 +62,6 @@ def dashboard_index(request): sql_queries = [] too_long_so_use_post = False save_form = SaveDashboardForm(prefix="_save") - save_query_form = DashboardQueryFormSet(prefix="_save_query") if request.method == "POST": # Is this an export? if any( @@ -136,14 +111,6 @@ def dashboard_index(request): sql_queries.append(sql) else: unverified_sql_queries.append(sql) - save_query_form_data = { - '_save_query-TOTAL_FORMS': str(len(sql_queries) + 1), - '_save_query-INITIAL_FORMS': str(0), - } - # for k, sql in enumerate(sql_queries): - # save_query_form_data[f"_save_query-{k}-sql"] = sql - save_query_form = DashboardQueryFormSet(initial=[{"sql": sql} for sql in sql_queries], prefix="_save_query") - print(save_query_form.as_table()) if getattr(settings, "DASHBOARD_UPGRADE_OLD_BASE64_LINKS", None): redirect_querystring = check_for_base64_upgrade(sql_queries) if redirect_querystring: @@ -153,7 +120,7 @@ def dashboard_index(request): sql_queries, unverified_sql_queries=unverified_sql_queries, too_long_so_use_post=too_long_so_use_post, - extra_context={"save_form": save_form, "save_query_form": save_query_form}, + extra_context={"save_form": save_form}, ) @@ -243,6 +210,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 = { @@ -258,6 +228,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( @@ -297,6 +268,11 @@ def _dashboard_index( 0, "django_sql_dashboard/widgets/" + template_name, ) + if query_object and query_object.template: + templates.insert( + 0, + "django_sql_dashboard/widgets/" + query_object.template, + ) display_rows = displayable_rows(rows[:row_limit]) column_details = [ { @@ -321,6 +297,7 @@ def _dashboard_index( "extra_qs": extra_qs, "duration_ms": duration_ms, "templates": templates, + "query": query_object, } ) finally: From 8e72195da19e09ac18cb597a29837af3f40471c2 Mon Sep 17 00:00:00 2001 From: David Kane Date: Fri, 14 Apr 2023 16:10:51 +0100 Subject: [PATCH 04/20] make created_at always readonly --- django_sql_dashboard/admin.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/django_sql_dashboard/admin.py b/django_sql_dashboard/admin.py index 4e93060..d58433e 100644 --- a/django_sql_dashboard/admin.py +++ b/django_sql_dashboard/admin.py @@ -16,15 +16,10 @@ 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", - "title", - "description", - "settings", - ) - else: - return tuple() + readonly_fields.extend(["sql", "title", "description", "settings"]) + return readonly_fields @admin.register(Dashboard) From 8359a087fffbad0eac2f3ecedfcc3a35f46f3315 Mon Sep 17 00:00:00 2001 From: David Kane Date: Fri, 14 Apr 2023 16:13:58 +0100 Subject: [PATCH 05/20] make template field readonly too --- django_sql_dashboard/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/django_sql_dashboard/admin.py b/django_sql_dashboard/admin.py index d58433e..ad62770 100644 --- a/django_sql_dashboard/admin.py +++ b/django_sql_dashboard/admin.py @@ -18,7 +18,9 @@ def has_change_permission(self, request, obj=None): def get_readonly_fields(self, request, obj=None): readonly_fields = ["created_at"] if not request.user.has_perm("django_sql_dashboard.execute_sql"): - readonly_fields.extend(["sql", "title", "description", "settings"]) + readonly_fields.extend( + ["sql", "title", "description", "settings", "template"] + ) return readonly_fields From 586be5a03180ba699f62cd1201c12ad0d2afd1df Mon Sep 17 00:00:00 2001 From: David Kane Date: Fri, 14 Apr 2023 16:17:06 +0100 Subject: [PATCH 06/20] remove support for python 3.6 (end of life) --- .github/workflows/test.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f37db08..347a5e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] postgresql-version: [10, 11, 12, 13] steps: - uses: actions/checkout@v2 diff --git a/setup.py b/setup.py index 85f96f0..bd606d5 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup -VERSION = "1.1" +VERSION = "1.2" def get_long_description(): From a5a810b02441afc8380f9c56a55719c91c89ed70 Mon Sep 17 00:00:00 2001 From: David Kane Date: Fri, 14 Apr 2023 16:29:52 +0100 Subject: [PATCH 07/20] include python 3.11 in tests --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 347a5e3..0b93dde 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] postgresql-version: [10, 11, 12, 13] steps: - uses: actions/checkout@v2 @@ -39,7 +39,7 @@ jobs: run: | export POSTGRESQL_PATH="/usr/lib/postgresql/$POSTGRESQL_VERSION/bin/postgres" export INITDB_PATH="/usr/lib/postgresql/$POSTGRESQL_VERSION/bin/initdb" - pytest + python -m pytest - name: Check formatting run: black . --check From 8f1ef0e502eb97e2a165f88cb217d0480ec00510 Mon Sep 17 00:00:00 2001 From: David Kane Date: Fri, 14 Apr 2023 16:33:21 +0100 Subject: [PATCH 08/20] change supported postgres versions --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b93dde..88bb864 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] - postgresql-version: [10, 11, 12, 13] + postgresql-version: [12, 13, 14, 15] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} From 83ecd2dd619fb49e3ef8cb8684e34f35a73364f8 Mon Sep 17 00:00:00 2001 From: David Kane Date: Fri, 14 Apr 2023 17:20:06 +0100 Subject: [PATCH 09/20] Get existing tests passing --- .../widgets/_base_widget.html | 10 +- .../django_sql_dashboard/widgets/default.html | 124 ++++++++++-------- django_sql_dashboard/views.py | 19 ++- test_project/test_dashboard.py | 9 +- test_project/test_dashboard_permissions.py | 2 +- test_project/test_widgets.py | 1 + 6 files changed, 99 insertions(+), 66 deletions(-) diff --git a/django_sql_dashboard/templates/django_sql_dashboard/widgets/_base_widget.html b/django_sql_dashboard/templates/django_sql_dashboard/widgets/_base_widget.html index 9082b53..8184219 100644 --- a/django_sql_dashboard/templates/django_sql_dashboard/widgets/_base_widget.html +++ b/django_sql_dashboard/templates/django_sql_dashboard/widgets/_base_widget.html @@ -1,7 +1,11 @@ +{% load django_sql_dashboard %} +
- {% if result.title %}

{{ result.title }}

{% endif %} - {% if result.description %} - {{ result.description|sql_dashboard_markdown }} + {% if result.query %} + {% if result.query.title %}

{{ result.query.title }}

{% endif %} + {% if result.query.description %} + {{ result.query.description|sql_dashboard_markdown }} + {% endif %} {% endif %} {% block widget_results %}{% endblock %}
SQL query diff --git a/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html b/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html index c353ed9..100944f 100644 --- a/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html +++ b/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html @@ -3,26 +3,39 @@ {% if result.query.title %}

{{ result.query.title }}

{% endif %} {% if result.query.description %}{{ result.query.description|sql_dashboard_markdown }}{% endif %} {% endif %} - {% if saved_dashboard %}
- SQL query -
{{ result.sql }}
{% else %}{% endif %} - {% if not saved_dashboard %}

- -

{% else %} -
{% endif %} + {% if saved_dashboard %}
SQL query
{{ result.sql }}
{% else %}{% endif %} + {% if not saved_dashboard %}

+ +

{% else %}
{% endif %} {% if result.truncated %} -

- Results were truncated - {% if user_can_export_data and not saved_dashboard %} - - - {% endif %} -

+

+ Results were truncated + {% if user_can_export_data and not saved_dashboard %} + + + {% endif %} +

{% else %} -

{{ result.row_lists|length }} row{{ result.row_lists|length|pluralize }}

+

{{ result.row_lists|length }} row{{ result.row_lists|length|pluralize }}

{% endif %} {% if result.error %}

@@ -33,15 +46,13 @@ {% for column in result.column_details %} - - {{ column.name }} + {{ column.name }} {% endfor %} @@ -55,38 +66,47 @@ {% endfor %} -

- Copy and export data +
Copy and export data {% if user_can_export_data and not saved_dashboard %} -
- - -
+
+ + +
{% endif %}

Duration: {{ result.duration_ms|floatformat:2 }}ms

\ No newline at end of file diff --git a/django_sql_dashboard/views.py b/django_sql_dashboard/views.py index f5a111e..59e2889 100644 --- a/django_sql_dashboard/views.py +++ b/django_sql_dashboard/views.py @@ -16,6 +16,7 @@ StreamingHttpResponse, ) from django.shortcuts import get_object_or_404, render +from django.template.loader import TemplateDoesNotExist, get_template, select_template from .models import Dashboard from .utils import ( @@ -264,14 +265,22 @@ def _dashboard_index( columns = [c.name for c in cursor.description] template_name = ("-".join(sorted(columns))) + ".html" if len(template_name) < 255: - templates.insert( - 0, - "django_sql_dashboard/widgets/" + template_name, - ) + 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/" + query_object.template, + "django_sql_dashboard/widgets/" + + query_object.template + + ".html", ) display_rows = displayable_rows(rows[:row_limit]) column_details = [ diff --git a/test_project/test_dashboard.py b/test_project/test_dashboard.py index 4fbdd8f..26b5dfd 100644 --- a/test_project/test_dashboard.py +++ b/test_project/test_dashboard.py @@ -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, title, template", + "href_sql": "select id, sql, dashboard_id, _order, created_at, description, settings, title, template from django_sql_dashboard_dashboardquery", }, { "table": "switches", diff --git a/test_project/test_dashboard_permissions.py b/test_project/test_dashboard_permissions.py index 5a466ec..8849d0a 100644 --- a/test_project/test_dashboard_permissions.py +++ b/test_project/test_dashboard_permissions.py @@ -328,7 +328,7 @@ 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 div")[0].text == "select 1 + 1" user.is_staff = True user.save() diff --git a/test_project/test_widgets.py b/test_project/test_widgets.py index 8957ef9..11931a3 100644 --- a/test_project/test_widgets.py +++ b/test_project/test_widgets.py @@ -92,6 +92,7 @@ def test_default_widget_column_count_links(admin_client, dashboard_db): assert th["data-count-url"] querystring = th["data-count-url"].split("?")[1] bits = dict(parse_qsl(querystring)) + print(unsign_sql(bits["sql"])[0]) assert unsign_sql(bits["sql"])[0] == ( 'select "id", count(*) as n from (SELECT * FROM (\n' " VALUES (1, %(label)s, 4.5), " From a2158d9ca3a7416d1270a739dfacc12f352b23c7 Mon Sep 17 00:00:00 2001 From: David Kane Date: Fri, 14 Apr 2023 17:28:00 +0100 Subject: [PATCH 10/20] test fixes --- .../migrations/0006_auto_20230414_1540.py | 29 ++++++++++++------- .../django_sql_dashboard/widgets/default.html | 6 ++-- test_project/test_dashboard_permissions.py | 5 +++- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/django_sql_dashboard/migrations/0006_auto_20230414_1540.py b/django_sql_dashboard/migrations/0006_auto_20230414_1540.py index 0aaf8f0..ac8b06d 100644 --- a/django_sql_dashboard/migrations/0006_auto_20230414_1540.py +++ b/django_sql_dashboard/migrations/0006_auto_20230414_1540.py @@ -6,23 +6,32 @@ class Migration(migrations.Migration): dependencies = [ - ('django_sql_dashboard', '0005_auto_20211013_0953'), + ("django_sql_dashboard", "0005_auto_20211013_0953"), ] operations = [ 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), + 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.AlterField( - 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), + 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.AlterField( - model_name='dashboardquery', - name='sql', - field=models.TextField(verbose_name='SQL query'), + model_name="dashboardquery", + name="sql", + field=models.TextField(verbose_name="SQL query"), ), ] diff --git a/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html b/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html index 100944f..da21bfe 100644 --- a/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html +++ b/django_sql_dashboard/templates/django_sql_dashboard/widgets/default.html @@ -1,7 +1,9 @@ {% load django_sql_dashboard %}
{% if result.query %} {% if result.query.title %}

{{ result.query.title }}

{% endif %} - {% if result.query.description %}{{ result.query.description|sql_dashboard_markdown }}{% endif %} + {% if result.query.description %} + {{ result.query.description|sql_dashboard_markdown }} + {% endif %} {% endif %} {% if saved_dashboard %}
SQL query
{{ result.sql }}
{% else %}