Skip to content

Commit 9e4adea

Browse files
committed
Revert "Refactor named parameters feature to allow extensibility (#148)"
This reverts commit fa839b8 Refs #148
1 parent 486736f commit 9e4adea

6 files changed

Lines changed: 65 additions & 111 deletions

File tree

django_sql_dashboard/templates/django_sql_dashboard/_parameters.html

Lines changed: 0 additions & 13 deletions
This file was deleted.

django_sql_dashboard/templates/django_sql_dashboard/dashboard.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,20 @@ <h2 style="margin-top: 0.5em">Unverified SQL</h2>
2828
{% if query_results %}
2929
<p><a href="#save-dashboard">Save this dashboard</a> | <a href="{{ request.path }}">Remove all queries</a></p>
3030
{% endif %}
31-
{% include "django_sql_dashboard/_parameters.html" %}
31+
{% if parameter_values %}
32+
<h3>Query parameters</h3>
33+
<div class="query-parameters">
34+
{% for name, value in parameter_values %}
35+
<label for="qp{{ forloop.counter }}">{{ name }}</label>
36+
<input type="text" id="qp{{ forloop.counter }}" name="{{ name }}" value="{{ value }}">
37+
{% endfor %}
38+
</div>
39+
<input
40+
class="btn"
41+
type="submit"
42+
value="Run quer{% if query_results|length > 1 %}ies{% else %}y{% endif %}"
43+
/>
44+
{% endif %}
3245
{% for result in query_results %}
3346
{% include result.templates with result=result %}
3447
{% endfor %}

django_sql_dashboard/templates/django_sql_dashboard/saved_dashboard.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,20 @@ <h1>{% if dashboard.title %}{{ dashboard.title }}{% else %}{{ dashboard.slug }}{
3030
</p>
3131

3232
<form action="{{ request.path }}" method="GET">
33-
{% include "django_sql_dashboard/_parameters.html" %}
33+
{% if parameter_values %}
34+
<h3>Query parameters</h3>
35+
<div class="query-parameters">
36+
{% for name, value in parameter_values %}
37+
<label for="qp{{ forloop.counter }}">{{ name }}</label>
38+
<input type="text" id="qp{{ forloop.counter }}" name="{{ name }}" value="{{ value }}">
39+
{% endfor %}
40+
</div>
41+
<input
42+
class="btn"
43+
type="submit"
44+
value="Run quer{% if query_results|length > 1 %}ies{% else %}y{% endif %}"
45+
/>
46+
{% endif %}
3447
{% for result in query_results %}
3548
{% include result.templates with result=result %}
3649
{% endfor %}

django_sql_dashboard/utils.py

Lines changed: 14 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
from collections import namedtuple
66

77
from django.core import signing
8-
from django.conf import settings
9-
from django.utils.html import escape
10-
from django.utils.safestring import mark_safe
118

129
SQL_SALT = "django_sql_dashboard:query"
1310

@@ -61,6 +58,20 @@ def displayable_rows(rows):
6158
return fixed
6259

6360

61+
_named_parameters_re = re.compile(r"\%\(([^\)]+)\)s")
62+
63+
64+
def extract_named_parameters(sql):
65+
params = _named_parameters_re.findall(sql)
66+
# Validation step: after removing params, are there
67+
# any single `%` symbols that will confuse psycopg2?
68+
without_params = _named_parameters_re.sub("", sql)
69+
without_double_percents = without_params.replace("%%", "")
70+
if "%" in without_double_percents:
71+
raise ValueError(r"Found a single % character")
72+
return params
73+
74+
6475
def check_for_base64_upgrade(queries):
6576
if not queries:
6677
return
@@ -106,79 +117,3 @@ def apply_sort(sql, sort_column, is_desc=False):
106117
else:
107118
sql = "select * from ({}) as results".format(sql)
108119
return sql + ' order by "{}"{}'.format(sort_column, " desc" if is_desc else "")
109-
110-
111-
class Parameter:
112-
extract_re = re.compile(r"\%\(([^\)]+)\)s")
113-
114-
def __init__(self, name, default_value=""):
115-
self.name = name
116-
self.default_value = self.get_sanitized(default_value, for_default=True)
117-
118-
def ensure_consistency(self, previous):
119-
if self.name != previous.name:
120-
raise ValueError("Invalid name for parameter '%s': previously registered with name '%s'" % (self.name, previous.name))
121-
if self.default_value != "" and self.default_value != previous.default_value:
122-
raise ValueError("Invalid default value '%s' for parameter '%s': previously registered with default value '%s'" % (self.default_value, self.name, previous.default_value))
123-
124-
def get_sanitized(self, value, for_default=False):
125-
if value is None or (for_default and value == "null"):
126-
return None # represents DB null value
127-
if not isinstance(value, str):
128-
raise ValueError("Invalid %svalue for parameter '%s': '%s'" % ("default " if for_default else "", self.name, type(value).__name__))
129-
return value
130-
131-
@property
132-
def value(self):
133-
return self._value if hasattr(self, "_value") else self.default_value
134-
135-
@value.setter
136-
def value(self, new_value):
137-
self._value = self.get_sanitized(new_value) if new_value != "" else self.default_value
138-
139-
def form_control(self):
140-
return mark_safe(f"""<label for="qp_{escape(self.name)}">{escape(self.name)}</label>
141-
<input type="text" id="qp_{escape(self.name)}" name="{escape(self.name)}" value="{escape(self.value) if self.value is not None else ""}">""")
142-
143-
@classmethod
144-
def extract(cls, sql: str, value_sources: list[dict[str, str]], target: list=[]):
145-
for found in cls.extract_re.findall(sql):
146-
# Ensure 'found' is an iterable of capturing groups, even if there is only one capturing group in the regex
147-
if isinstance(found, str):
148-
found = [found]
149-
new_param = cls(*found)
150-
151-
# Ensure parameters are added only once
152-
previous_param = next((param for param in target if param.name == new_param.name), None)
153-
if previous_param:
154-
new_param.ensure_consistency(previous_param)
155-
else:
156-
target.append(new_param)
157-
158-
# Validation step: after removing params, are there
159-
# any single `%` symbols that will confuse psycopg2?
160-
without_params = cls.extract_re.sub("", sql)
161-
without_double_percents = without_params.replace("%%", "")
162-
if "%" in without_double_percents:
163-
raise ValueError(r"Found a single % character")
164-
165-
# Read values form sources
166-
for param in target:
167-
for source in value_sources:
168-
if param.name in source:
169-
param.value = source[param.name]
170-
break
171-
172-
return target
173-
174-
@classmethod
175-
def execute(cls, cursor, sql: str, parameters: list=[]):
176-
values = { param.name: param.value for param in parameters }
177-
cursor.execute(sql, values)
178-
179-
PARAMETER_CLASS = getattr(settings, "DASHBOARD_PARAMETER_CLASS", Parameter)
180-
if isinstance(PARAMETER_CLASS, str):
181-
from importlib import import_module
182-
183-
[module_name, class_name] = PARAMETER_CLASS.rsplit('.', 1)
184-
PARAMETER_CLASS = getattr(import_module(module_name), class_name)

django_sql_dashboard/views.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
apply_sort,
2626
check_for_base64_upgrade,
2727
displayable_rows,
28-
PARAMETER_CLASS,
28+
extract_named_parameters,
2929
postgresql_reserved_words,
3030
sign_sql,
3131
unsign_sql,
@@ -192,7 +192,10 @@ def _dashboard_index(
192192
sql_query_parameter_errors = []
193193
for sql in sql_queries:
194194
try:
195-
PARAMETER_CLASS.extract(sql, value_sources=[request.POST, request.GET], target=parameters)
195+
extracted = extract_named_parameters(sql)
196+
for p in extracted:
197+
if p not in parameters:
198+
parameters.append(p)
196199
sql_query_parameter_errors.append(False)
197200
except ValueError as e:
198201
if "%" in sql:
@@ -202,9 +205,9 @@ def _dashboard_index(
202205
else:
203206
sql_query_parameter_errors.append(str(e))
204207
parameter_values = {
205-
parameter.name: parameter.value
208+
parameter: request.POST.get(parameter, request.GET.get(parameter, ""))
206209
for parameter in parameters
207-
if parameter.name != "sql"
210+
if parameter != "sql"
208211
}
209212
extra_qs = "&{}".format(urlencode(parameter_values)) if parameter_values else ""
210213
results_index = -1
@@ -247,7 +250,7 @@ def _dashboard_index(
247250
# Running a SELECT prevents future SET TRANSACTION READ WRITE:
248251
cursor.execute("SELECT 1;")
249252
cursor.fetchall()
250-
PARAMETER_CLASS.execute(cursor, sql, parameters)
253+
cursor.execute(sql, parameter_values)
251254
try:
252255
rows = list(cursor.fetchmany(row_limit + 1))
253256
except ProgrammingError as e:
@@ -300,12 +303,12 @@ def _dashboard_index(
300303
if dashboard and dashboard.title:
301304
html_title = dashboard.title
302305

303-
# Add named parameter values to the page title, when they are distinct from the default values
306+
# Add named parameter values, if any exist
304307
provided_values = {
305-
param.name: param.value for param in parameters if param.value != param.default_value
308+
key: value for key, value in parameter_values.items() if value.strip()
306309
}
307310
if provided_values:
308-
if len(parameters) == 1:
311+
if len(provided_values) == 1:
309312
html_title += ": {}".format(list(provided_values.values())[0])
310313
else:
311314
html_title += ": {}".format(
@@ -340,7 +343,7 @@ def _dashboard_index(
340343
"user_can_execute_sql": user_can_execute_sql,
341344
"user_can_export_data": getattr(settings, "DASHBOARD_ENABLE_FULL_EXPORT", None)
342345
and user_can_execute_sql,
343-
"parameters": parameters,
346+
"parameter_values": parameter_values.items(),
344347
"too_long_so_use_post": too_long_so_use_post,
345348
"saved_dashboards": saved_dashboards,
346349
}
@@ -407,7 +410,10 @@ def export_sql_results(request):
407410
assert format in ("csv", "tsv")
408411
sqls = request.POST.getlist("sql")
409412
sql = sqls[int(sql_index)]
410-
parameters = PARAMETER_CLASS.extract(sql, value_sources=[request.POST])
413+
parameter_values = {
414+
parameter: request.POST.get(parameter, "")
415+
for parameter in extract_named_parameters(sql)
416+
}
411417
alias = getattr(settings, "DASHBOARD_DB_ALIAS", "dashboard")
412418
# Decide on filename
413419
sql_hash = hashlib.sha256(sql.encode("utf-8")).hexdigest()[:6]
@@ -437,7 +443,7 @@ def read_and_flush():
437443

438444
def rows():
439445
try:
440-
PARAMETER_CLASS.execute(cursor, sql, parameters)
446+
cursor.execute(sql, parameter_values)
441447
done_header = False
442448
while True:
443449
records = cursor.fetchmany(size=2000)

test_project/test_parameters.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ def test_parameter_form(admin_client, dashboard_db):
2626
html = response.content.decode("utf-8")
2727
# Form should have three form fields
2828
for fragment in (
29-
'<label for="qp_foo">foo</label>',
30-
'<input type="text" id="qp_foo" name="foo" value="">',
31-
'<label for="qp_bar">bar</label>',
32-
'<input type="text" id="qp_bar" name="bar" value="">',
33-
'<label for="qp_baz">baz</label>',
34-
'<input type="text" id="qp_baz" name="baz" value="">',
29+
'<label for="qp1">foo</label>',
30+
'<input type="text" id="qp1" name="foo" value="">',
31+
'<label for="qp2">bar</label>',
32+
'<input type="text" id="qp2" name="bar" value="">',
33+
'<label for="qp3">baz</label>',
34+
'<input type="text" id="qp3" name="baz" value="">',
3535
):
3636
assert fragment in html
3737

0 commit comments

Comments
 (0)