kopia lustrzana https://github.com/simonw/datasette
				
				
				
			Canned query support + database/query template refactor
Named canned queries can now be defined in metadata.json like this:
    {
        "databases": {
            "timezones": {
                "queries": {
                    "timezone_for_point": "select tzid from timezones ..."
                }
            }
        }
    }
These will be shown in a new "Queries" section beneath "Views" on the database page.
As part of this, I refactored the logic for the database index page. It used
to combine the functionality for listing available tables and the
functionality for executing custom SQL queries in a single template and view.
I have split that template out into database.html and query.html and reworked
the view to more clearly separate the custom SQL executing code.
Refs #20
			
			
				sanic-07
			
			
		
							rodzic
							
								
									0cfd7ce59d
								
							
						
					
					
						commit
						a743cdeafc
					
				
							
								
								
									
										110
									
								
								datasette/app.py
								
								
								
								
							
							
						
						
									
										110
									
								
								datasette/app.py
								
								
								
								
							|  | @ -50,6 +50,8 @@ class RenderMixin(HTTPMethodView): | |||
| 
 | ||||
| 
 | ||||
| class BaseView(RenderMixin): | ||||
|     re_named_parameter = re.compile(':([a-zA-Z0-9_]+)') | ||||
| 
 | ||||
|     def __init__(self, datasette): | ||||
|         self.ds = datasette | ||||
|         self.files = datasette.files | ||||
|  | @ -258,6 +260,46 @@ class BaseView(RenderMixin): | |||
|             ) | ||||
|         return r | ||||
| 
 | ||||
|     async def custom_sql(self, request, name, hash, sql, editable=True, canned_query=None): | ||||
|         params = request.raw_args | ||||
|         if 'sql' in params: | ||||
|             params.pop('sql') | ||||
|         # Extract any :named parameters | ||||
|         named_parameters = self.re_named_parameter.findall(sql) | ||||
|         named_parameter_values = { | ||||
|             named_parameter: params.get(named_parameter) or '' | ||||
|             for named_parameter in named_parameters | ||||
|         } | ||||
| 
 | ||||
|         # Set to blank string if missing from params | ||||
|         for named_parameter in named_parameters: | ||||
|             if named_parameter not in params: | ||||
|                 params[named_parameter] = '' | ||||
| 
 | ||||
|         extra_args = {} | ||||
|         if params.get('_sql_time_limit_ms'): | ||||
|             extra_args['custom_time_limit'] = int(params['_sql_time_limit_ms']) | ||||
|         rows, truncated, description = await self.execute( | ||||
|             name, sql, params, truncate=True, **extra_args | ||||
|         ) | ||||
|         columns = [r[0] for r in description] | ||||
|         return { | ||||
|             'database': name, | ||||
|             'rows': rows, | ||||
|             'truncated': truncated, | ||||
|             'columns': columns, | ||||
|             'query': { | ||||
|                 'sql': sql, | ||||
|                 'params': params, | ||||
|             } | ||||
|         }, { | ||||
|             'database_hash': hash, | ||||
|             'custom_sql': True, | ||||
|             'named_parameter_values': named_parameter_values, | ||||
|             'editable': editable, | ||||
|             'canned_query': canned_query, | ||||
|         }, ('query-{}.html'.format(to_css_class(name)), 'query.html') | ||||
| 
 | ||||
| 
 | ||||
| class IndexView(RenderMixin): | ||||
|     def __init__(self, datasette): | ||||
|  | @ -315,12 +357,13 @@ async def favicon(request): | |||
| 
 | ||||
| 
 | ||||
| class DatabaseView(BaseView): | ||||
|     re_named_parameter = re.compile(':([a-zA-Z0-9_]+)') | ||||
| 
 | ||||
|     async def data(self, request, name, hash): | ||||
|         if request.args.get('sql'): | ||||
|             return await self.custom_sql(request, name, hash) | ||||
|             sql = request.raw_args.pop('sql') | ||||
|             validate_sql_select(sql) | ||||
|             return await self.custom_sql(request, name, hash, sql) | ||||
|         info = self.ds.inspect()[name] | ||||
|         metadata = self.ds.metadata.get('databases', {}).get(name, {}) | ||||
|         tables = list(info['tables'].values()) | ||||
|         tables.sort(key=lambda t: (t['hidden'], t['name'])) | ||||
|         return { | ||||
|  | @ -328,48 +371,14 @@ class DatabaseView(BaseView): | |||
|             'tables': tables, | ||||
|             'hidden_count': len([t for t in tables if t['hidden']]), | ||||
|             'views': info['views'], | ||||
|             'queries': [{ | ||||
|                 'name': query_name, | ||||
|                 'sql': query_sql, | ||||
|             } for query_name, query_sql in (metadata.get('queries') or {}).items()], | ||||
|         }, { | ||||
|             'database_hash': hash, | ||||
|             'show_hidden': request.args.get('_show_hidden'), | ||||
|         }, ('database-{}.html'.format(to_css_class(name)), 'database.html') | ||||
| 
 | ||||
|     async def custom_sql(self, request, name, hash): | ||||
|         params = request.raw_args | ||||
|         sql = params.pop('sql') | ||||
|         validate_sql_select(sql) | ||||
| 
 | ||||
|         # Extract any :named parameters | ||||
|         named_parameters = self.re_named_parameter.findall(sql) | ||||
|         named_parameter_values = { | ||||
|             named_parameter: params.get(named_parameter) or '' | ||||
|             for named_parameter in named_parameters | ||||
|         } | ||||
| 
 | ||||
|         # Set to blank string if missing from params | ||||
|         for named_parameter in named_parameters: | ||||
|             if named_parameter not in params: | ||||
|                 params[named_parameter] = '' | ||||
| 
 | ||||
|         extra_args = {} | ||||
|         if params.get('_sql_time_limit_ms'): | ||||
|             extra_args['custom_time_limit'] = int(params['_sql_time_limit_ms']) | ||||
|         rows, truncated, description = await self.execute( | ||||
|             name, sql, params, truncate=True, **extra_args | ||||
|         ) | ||||
|         columns = [r[0] for r in description] | ||||
|         return { | ||||
|             'database': name, | ||||
|             'rows': rows, | ||||
|             'truncated': truncated, | ||||
|             'columns': columns, | ||||
|             'query': { | ||||
|                 'sql': sql, | ||||
|                 'params': params, | ||||
|             } | ||||
|         }, { | ||||
|             'database_hash': hash, | ||||
|             'custom_sql': True, | ||||
|             'named_parameter_values': named_parameter_values, | ||||
|             'editable': True, | ||||
|         }, ('database-{}.html'.format(to_css_class(name)), 'database.html') | ||||
| 
 | ||||
| 
 | ||||
|  | @ -466,6 +475,9 @@ class RowTableShared(BaseView): | |||
| class TableView(RowTableShared): | ||||
|     async def data(self, request, name, hash, table): | ||||
|         table = urllib.parse.unquote_plus(table) | ||||
|         canned_query = self.ds.get_canned_query(name, table) | ||||
|         if canned_query is not None: | ||||
|             return await self.custom_sql(request, name, hash, canned_query['sql'], editable=False, canned_query=table) | ||||
|         pks = await self.pks_for_table(name, table) | ||||
|         is_view = bool(list(await self.execute(name, "SELECT count(*) from sqlite_master WHERE type = 'view' and name=:n", { | ||||
|             'n': table, | ||||
|  | @ -792,6 +804,20 @@ class Datasette: | |||
|         self.template_dir = template_dir | ||||
|         self.static_mounts = static_mounts or [] | ||||
| 
 | ||||
|     def get_canned_query(self, database_name, query_name): | ||||
|         query = self.metadata.get( | ||||
|             'databases', {} | ||||
|         ).get( | ||||
|             database_name, {} | ||||
|         ).get( | ||||
|             'queries', {} | ||||
|         ).get(query_name) | ||||
|         if query: | ||||
|             return { | ||||
|                 'name': query_name, | ||||
|                 'sql': query, | ||||
|             } | ||||
| 
 | ||||
|     def asset_urls(self, key): | ||||
|         for url_or_dict in (self.metadata.get(key) or []): | ||||
|             if isinstance(url_or_dict, dict): | ||||
|  |  | |||
|  | @ -0,0 +1,7 @@ | |||
| <script src="/-/static/codemirror-5.31.0.js"></script> | ||||
| <link rel="stylesheet" href="/-/static/codemirror-5.31.0-min.css" /> | ||||
| <script src="/-/static/codemirror-5.31.0-sql.min.js"></script> | ||||
| <style> | ||||
|     .CodeMirror { height: auto; min-height: 70px; width: 80%; border: 1px solid #ddd; } | ||||
|     .CodeMirror-scroll { max-height: 200px; } | ||||
| </style> | ||||
|  | @ -0,0 +1,13 @@ | |||
| <script> | ||||
|     var editor = CodeMirror.fromTextArea(document.getElementsByName("sql")[0], { | ||||
|       lineNumbers: true, | ||||
|       mode: "text/x-sql", | ||||
|       lineWrapping: true, | ||||
|     }); | ||||
|     editor.setOption("extraKeys", { | ||||
|       "Shift-Enter": function() { | ||||
|         document.getElementsByClassName("sql")[0].submit(); | ||||
|       }, | ||||
|       Tab: false | ||||
|     }); | ||||
| </script> | ||||
|  | @ -3,63 +3,22 @@ | |||
| {% block title %}{{ database }}{% endblock %} | ||||
| 
 | ||||
| {% block extra_head %} | ||||
| {% if columns %} | ||||
| <style> | ||||
| @media only screen and (max-width: 576px) { | ||||
| {% for column in columns %} | ||||
|     td:nth-of-type({{ loop.index }}):before { content: "{{ column|escape_css_string }}"; } | ||||
| {% endfor %} | ||||
| } | ||||
| </style> | ||||
| {% endif %} | ||||
| <script src="/-/static/codemirror-5.31.0.js"></script> | ||||
| <link rel="stylesheet" href="/-/static/codemirror-5.31.0-min.css" /> | ||||
| <script src="/-/static/codemirror-5.31.0-sql.min.js"></script> | ||||
| <style> | ||||
|     .CodeMirror { height: auto; min-height: 70px; width: 80%; border: 1px solid #ddd; } | ||||
|     .CodeMirror-scroll { max-height: 200px; } | ||||
| </style> | ||||
| {% include "_codemirror.html" %} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block body_class %}db db-{{ database|to_css_class }}{% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| <div class="hd"><a href="/">home</a>{% if query %} / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a>{% endif %}</div> | ||||
| <div class="hd"><a href="/">home</a></div> | ||||
| 
 | ||||
| <h1 style="padding-left: 10px; border-left: 10px solid #{{ database_hash[:6] }}">{{ database }}</h1> | ||||
| 
 | ||||
| <form class="sql" action="/{{ database }}-{{ database_hash }}" method="get"> | ||||
|     <h3>Custom SQL query{% if rows %} returning {% if truncated %}more than {% endif %}{{ "{:,}".format(rows|length) }} row{% if rows|length == 1 %}{% else %}s{% endif %}{% endif %}</h3> | ||||
|     <p><textarea name="sql">{% if query and query.sql %}{{ query.sql }}{% else %}select * from {{ tables[0].name|escape_table_name }}{% endif %}</textarea></p> | ||||
|     {% if named_parameter_values %} | ||||
|         <h3>Query parameters</h3> | ||||
|         {% for name, value in named_parameter_values.items() %} | ||||
|             <p><label for="qp{{ loop.counter }}">{{ name }}</label> <input type="text" id="qp{{ loop.counter }}" name="{{ name }}" value="{{ value }}"></p> | ||||
|         {% endfor %} | ||||
|     {% endif %} | ||||
|     <h3>Custom SQL query</h3> | ||||
|     <p><textarea name="sql">select * from {{ tables[0].name|escape_table_name }}</textarea></p> | ||||
|     <p><input type="submit" value="Run SQL"></p> | ||||
| </form> | ||||
| 
 | ||||
| {% if rows %} | ||||
| <p>This data as <a href="{{ url_json }}">.json</a>, <a href="{{ url_jsono }}">.jsono</a></p> | ||||
| <table> | ||||
|     <thead> | ||||
|         <tr> | ||||
|             {% for column in columns %}<th scope="col">{{ column }}</th>{% endfor %} | ||||
|         </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|     {% for row in rows %} | ||||
|         <tr> | ||||
|             {% for td in row %} | ||||
|                 <td>{% if td == None %}{{ " "|safe }}{% else %}{{ td }}{% endif %}</td> | ||||
|             {% endfor %} | ||||
|         </tr> | ||||
|     {% endfor %} | ||||
|     </tbody> | ||||
| </table> | ||||
| {% endif %} | ||||
| 
 | ||||
| {% for table in tables %} | ||||
| {% if show_hidden or not table.hidden %} | ||||
| <div class="db-table"> | ||||
|  | @ -83,20 +42,17 @@ | |||
|     </ul> | ||||
| {% endif %} | ||||
| 
 | ||||
| {% if queries %} | ||||
|     <h2>Queries</h2> | ||||
|     <ul> | ||||
|         {% for query in queries %} | ||||
|             <li><a href="/{{ database }}-{{ database_hash }}/{{ query.name|urlencode }}" title="{{ query.sql }}">{{ query.name }}</a></li> | ||||
|         {% endfor %} | ||||
|     </ul> | ||||
| {% endif %} | ||||
| 
 | ||||
| <p>Download SQLite DB: <a href="/{{ database }}-{{ database_hash }}.db">{{ database }}.db</a></p> | ||||
| 
 | ||||
| <script> | ||||
|     var editor = CodeMirror.fromTextArea(document.getElementsByName("sql")[0], { | ||||
|       lineNumbers: true, | ||||
|       mode: "text/x-sql", | ||||
|       lineWrapping: true, | ||||
|     }); | ||||
|     editor.setOption("extraKeys", { | ||||
|       "Shift-Enter": function() { | ||||
|         document.getElementsByClassName("sql")[0].submit(); | ||||
|       }, | ||||
|       Tab: false | ||||
|     }); | ||||
| </script> | ||||
| {% include "_codemirror_foot.html" %} | ||||
| 
 | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -0,0 +1,63 @@ | |||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block title %}{{ database }}{% if query and query.sql %}{{ query.sql }}{% endif %}{% endblock %} | ||||
| 
 | ||||
| {% block extra_head %} | ||||
| {% if columns %} | ||||
| <style> | ||||
| @media only screen and (max-width: 576px) { | ||||
| {% for column in columns %} | ||||
|     td:nth-of-type({{ loop.index }}):before { content: "{{ column|escape_css_string }}"; } | ||||
| {% endfor %} | ||||
| } | ||||
| </style> | ||||
| {% endif %} | ||||
| {% include "_codemirror.html" %} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block body_class %}query db-{{ database|to_css_class }}{% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| <div class="hd"><a href="/">home</a> / <a href="/{{ database }}-{{ database_hash }}">{{ database }}</a></div> | ||||
| 
 | ||||
| <h1 style="padding-left: 10px; border-left: 10px solid #{{ database_hash[:6] }}">{{ database }}</h1> | ||||
| 
 | ||||
| <form class="sql" action="/{{ database }}-{{ database_hash }}{% if canned_query %}/{{ canned_query }}{% endif %}" method="get"> | ||||
|     <h3>Custom SQL query{% if rows %} returning {% if truncated %}more than {% endif %}{{ "{:,}".format(rows|length) }} row{% if rows|length == 1 %}{% else %}s{% endif %}{% endif %}</h3> | ||||
|     {% if editable %} | ||||
|         <p><textarea name="sql">{% if query and query.sql %}{{ query.sql }}{% else %}select * from {{ tables[0].name|escape_table_name }}{% endif %}</textarea></p> | ||||
|     {% else %} | ||||
|         <pre>{% if query %}{{ query.sql }}{% endif %}</pre> | ||||
|     {% endif %} | ||||
|     {% if named_parameter_values %} | ||||
|         <h3>Query parameters</h3> | ||||
|         {% for name, value in named_parameter_values.items() %} | ||||
|             <p><label for="qp{{ loop.counter }}">{{ name }}</label> <input type="text" id="qp{{ loop.counter }}" name="{{ name }}" value="{{ value }}"></p> | ||||
|         {% endfor %} | ||||
|     {% endif %} | ||||
|     <p><input type="submit" value="Run SQL"></p> | ||||
| </form> | ||||
| 
 | ||||
| {% if rows %} | ||||
| <p>This data as <a href="{{ url_json }}">.json</a>, <a href="{{ url_jsono }}">.jsono</a></p> | ||||
| <table> | ||||
|     <thead> | ||||
|         <tr> | ||||
|             {% for column in columns %}<th scope="col">{{ column }}</th>{% endfor %} | ||||
|         </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|     {% for row in rows %} | ||||
|         <tr> | ||||
|             {% for td in row %} | ||||
|                 <td>{% if td == None %}{{ " "|safe }}{% else %}{{ td }}{% endif %}</td> | ||||
|             {% endfor %} | ||||
|         </tr> | ||||
|     {% endfor %} | ||||
|     </tbody> | ||||
| </table> | ||||
| {% endif %} | ||||
| 
 | ||||
| {% include "_codemirror_foot.html" %} | ||||
| 
 | ||||
| {% endblock %} | ||||
		Ładowanie…
	
		Reference in New Issue
	
	 Simon Willison
						Simon Willison