Show search_path in SQL entry window
authorKarl O. Pinc <kop@karlpinc.com>
Sat, 14 Sep 2024 23:05:47 +0000 (18:05 -0500)
committerKarl O. Pinc <kop@karlpinc.com>
Sat, 14 Sep 2024 23:05:47 +0000 (18:05 -0500)
src/pgwui_sql/templates/sql_edit.mak
src/pgwui_sql/views/base.py [new file with mode: 0644]
src/pgwui_sql/views/sql.py
src/pgwui_sql/views/sql_edit.py [new file with mode: 0644]
tests/templates/test_templates.py

index c2f229ddf244f3b2eddb42e517b9b52ed3af7b2f..439a6de22e4a46c1fd0fefba233814c2293561f0 100644 (file)
 </%block>
 
 <%def name="sql_row(tab_index)">
+      <tr>
+        <%self.lib:td_label for_id="search_path_id">
+          search_path
+        </%self.lib:td_label>
+        <%self.lib:td_input tab_index="${tab_index}">
+          <input name="search_path"
+                 tabindex="${tab_index.val}"
+                 id="search_path_id"
+                 type="text"
+                 size="30"
+                 value="${search_path}"
+                 />
+        </%self.lib:td_input>
+      </tr>
       <tr>
         <%self.lib:td_input tab_index="${tab_index}" colspan="2">
           <textarea name="sql"
diff --git a/src/pgwui_sql/views/base.py b/src/pgwui_sql/views/base.py
new file mode 100644 (file)
index 0000000..aa46742
--- /dev/null
@@ -0,0 +1,68 @@
+# Copyright (C) 2024 The Meme Factory, Inc. http://www.karlpinc.com/
+
+# This file is part of PGWUI_SQL.
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program.  If not, see
+# <http://www.gnu.org/licenses/>.
+#
+
+from wtforms.fields import TextAreaField
+import attrs
+import pgwui_core.core
+import pgwui_core.forms
+
+
+@attrs.define(slots=False)
+class SQLInitialPost(pgwui_core.forms.UserInitialPost):
+    sql = attrs.field(default='')
+
+
+class SQLWTForm(pgwui_core.forms.AuthWTForm):
+    '''The wtform used to connect to the db to execute SQL.'''
+    # We don't actually use the labels, wanting the template to
+    # look (and render) like html, but I'll define them anyway
+    # just to keep my hand in.
+    sql = TextAreaField('SQL:', id='sql_id')
+
+
+@attrs.define(slots=False)
+class SQLForm(pgwui_core.forms.UploadFormBaseMixin,
+              pgwui_core.forms.AuthLoadedForm):
+    '''
+    Acts like a dict, but with extra methods.
+
+    Attributes:
+      uh      The UploadHandler instance using the form
+    '''
+    def read(self):
+        '''
+        Read form data from the client
+        '''
+
+        # Read parent's data
+        super().read()
+
+        # Read our own data
+        if self._form.sql.data is None:
+            self['sql'] = ''
+        else:
+            self['sql'] = self._form.sql.data
+
+    def write(self, result, errors):
+        '''
+        Produces the dict pyramid will use to render the form.
+        '''
+        response = super().write(result, errors)
+        response['sql'] = self['sql']
+        return response
index 565731e5190a0e76e001d528244a08266c1ccbae..d6e252a61519988c1cab0fef5ed1001f02337865 100644 (file)
 #
 
 from pyramid.view import view_config
-from wtforms.fields import TextAreaField
 import attrs
 import logging
 import markupsafe
 import psycopg.errors
 
 import pgwui_core.core
-import pgwui_core.forms
 import pgwui_core.utils
+import pgwui_sql.views.base
 from pgwui_common.view import auth_base_view
 
 from pgwui_sql import exceptions as sql_ex
@@ -37,51 +36,6 @@ sql_ex.ExampleOnOffAskError('42')
 log = logging.getLogger(__name__)
 
 
-@attrs.define(slots=False)
-class SQLInitialPost(pgwui_core.forms.UserInitialPost):
-    sql = attrs.field(default='')
-
-
-class SQLWTForm(pgwui_core.forms.AuthWTForm):
-    '''The wtform used to connect to the db to execute SQL.'''
-    # We don't actually use the labels, wanting the template to
-    # look (and render) like html, but I'll define them anyway
-    # just to keep my hand in.
-    sql = TextAreaField('SQL:', id='sql_id')
-
-
-@attrs.define(slots=False)
-class SQLForm(pgwui_core.forms.UploadFormBaseMixin,
-              pgwui_core.forms.AuthLoadedForm):
-    '''
-    Acts like a dict, but with extra methods.
-
-    Attributes:
-      uh      The UploadHandler instance using the form
-    '''
-    def read(self):
-        '''
-        Read form data from the client
-        '''
-
-        # Read parent's data
-        super().read()
-
-        # Read our own data
-        if self._form.sql.data is None:
-            self['sql'] = ''
-        else:
-            self['sql'] = self._form.sql.data
-
-    def write(self, result, errors):
-        '''
-        Produces the dict pyramid will use to render the form.
-        '''
-        response = super().write(result, errors)
-        response['sql'] = self['sql']
-        return response
-
-
 @attrs.define()
 class SQLResult():
     rows = attrs.field(factory=list)
@@ -112,7 +66,7 @@ class ResultRow():
 
 
 @attrs.define(slots=False)
-class SQLHandler(pgwui_core.core.SessionDBHandler):
+class SQLResultsHandler(pgwui_core.core.SessionDBHandler):
     '''
     Deliver no data to the upload engine, instead do all the SQL
     execution here, in the cleanup method.
@@ -127,15 +81,9 @@ class SQLHandler(pgwui_core.core.SessionDBHandler):
     sql_results = attrs.field(factory=list)
 
     def make_form(self):
-        '''
-        Make the upload form needed by this handler.
-        '''
-        return SQLForm().build(self, ip=SQLInitialPost(), fc=SQLWTForm)
-
-    def get_data(self):
-        '''Return no data.  Data is in lines and we have no lines.
-        '''
-        self.data = tuple()
+        return pgwui_sql.views.base.SQLForm().build(
+            self, ip=pgwui_sql.views.base.SQLInitialPost(),
+            fc=pgwui_sql.views.base.SQLWTForm)
 
     def write(self, result, errors):
         '''
@@ -152,6 +100,11 @@ class SQLHandler(pgwui_core.core.SessionDBHandler):
                                       and self.uf['action'] != '')
         return response
 
+    def get_data(self):
+        '''Return no data.  Data is in lines and we have no lines.
+        '''
+        self.data = tuple()
+
     def format_detail(self, err, stmt_text):
         detail = []
         if err.diag.message_detail is not None:
@@ -229,40 +182,7 @@ class SQLHandler(pgwui_core.core.SessionDBHandler):
 @auth_base_view
 def sql_view(request):
 
-    uh = SQLHandler(request).init()
-    response = pgwui_core.core.UploadEngine(uh).run()
-
-    settings = request.registry.settings
-    response.setdefault('pgwui', dict())
-    response['pgwui']['pgwui_sql'] = settings['pgwui']['pgwui_sql']
-
-    response['result_rows'] = uh.sql_results
-
-    if response['report_success']:
-        # if pgwui_core.utils.is_checked(response['csv_checked']):
-        #     download_fmt = 'CSV'
-        # else:
-        #     download_fmt = 'TAB'
-        log.info('Successful sql: DB {db}:'
-                 # ' File ({filename}):'
-                 # ' Format {format}:'
-                 ' By user {user}'
-                 .format(  # filename=response['filename'],
-                     # format=download_fmt,
-                     db=response['db'],
-                     user=response['user']))
-    return response
-
-
-@view_config(route_name='pgwui_sql_edit',
-             renderer='pgwui_sql:templates/sql_edit.mak')
-@auth_base_view
-def sql_edit_view(request):
-    # We don't worry about this contacting the db to execute sql
-    # because pgwui_core.core does not try to alter the db until
-    # the hidden "action" POST variable is something other than ''.
-
-    uh = SQLHandler(request).init()
+    uh = SQLResultsHandler(request).init()
     response = pgwui_core.core.UploadEngine(uh).run()
 
     settings = request.registry.settings
diff --git a/src/pgwui_sql/views/sql_edit.py b/src/pgwui_sql/views/sql_edit.py
new file mode 100644 (file)
index 0000000..81f5625
--- /dev/null
@@ -0,0 +1,134 @@
+# Copyright (C) 2024 The Meme Factory, Inc. http://www.karlpinc.com/
+
+# This file is part of PGWUI_SQL.
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU Affero General Public License
+# as published by the Free Software Foundation, either version 3 of
+# the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program.  If not, see
+# <http://www.gnu.org/licenses/>.
+#
+
+from pyramid.view import view_config
+import attrs
+import logging
+import pgwui_core.core
+import pgwui_core.forms
+import pgwui_sql.views.base
+import pgwui_sql.exceptions as sql_ex
+
+from pgwui_common.view import auth_base_view
+
+log = logging.getLogger(__name__)
+
+
+@attrs.define(slots=False)
+class SQLEditForm(pgwui_sql.views.base.SQLForm):
+    '''Always set the "action" so that the SQL gets executed
+    '''
+    def read(self):
+        super().read()
+        self['action'] = 'u'
+
+
+# Utility functions
+def _fetcher(cur):
+    '''Get the result of `SHOW search_path;` from a psycopg3 cursor.
+    '''
+    result = cur.fetchone()
+    final = cur.fetchone()
+    if final is not None:
+        raise sql_ex.ExecutionError(
+            'Problem obtaining the search_path', '',
+            f'Extra result row returned ({final})')
+    return result[0]
+
+
+@attrs.define(slots=False)
+class SQLEditHandler(pgwui_core.core.SessionDBHandler):
+    '''
+    Execute pre-defined SQL statement(s)
+    '''
+    def make_form(self):
+        return SQLEditForm().build(
+            self, ip=pgwui_sql.views.base.SQLInitialPost(),
+            fc=pgwui_sql.views.base.SQLWTForm)
+
+    def get_data(self):
+        '''
+        Build and stash the SQL to be executed.
+
+        Returns:
+          A SQLData instance
+        '''
+        # Get the search_path
+        self.data = pgwui_core.sql.SQLData(
+            [pgwui_core.sql.SQLCommand(
+                'SHOW search_path;', (), fetcher=_fetcher)])
+
+    def factory(self, ue):
+        '''Make a db loader function from an UploadEngine.
+
+        Input:
+
+        Side Effects:
+        Yes, lots.
+        '''
+        return pgwui_core.sql.ExecuteSQL(ue, self)
+
+    def render(self, errors, result):
+        '''Instead of rendering, just return our results so we can
+        decide what to do next.
+
+        Input:
+          errors    List of Error instances
+          result    Db connection result dict
+        '''
+        response = super().render(errors, result)
+        return (response, errors)
+
+
+@view_config(route_name='pgwui_sql_edit',
+             renderer='pgwui_sql:templates/sql_edit.mak')
+@auth_base_view
+def sql_edit_view(request):
+    # This can use the UnsafeUploadEngine because it does not alter
+    # server state.  We are not getting any form data from the user.
+    # We don't have access to the CSRF token because the page is not
+    # sent a POST request.
+
+    uh = SQLEditHandler(request).init()
+    response, errors = pgwui_core.core.UnsafeUploadEngine(uh).run()
+    response['errors'] = errors
+    if errors:
+        log.warning(f'Failed to get search_path: ({errors})')
+
+    settings = request.registry.settings
+    response.setdefault('pgwui', dict())
+    response['pgwui']['pgwui_sql'] = settings['pgwui']['pgwui_sql']
+
+    response['search_path'] = uh.data.stmts[0].result
+
+    # if pgwui_core.utils.is_checked(response['csv_checked']):
+    #     download_fmt = 'CSV'
+    # else:
+    #     download_fmt = 'TAB'
+    log.debug('Successful sql editor request: DB {db}:'
+              # ' File ({filename}):'
+              # ' Format {format}:'
+              ' By user {user}:'
+              ' search_path ({search_path})'
+              .format(  # filename=response['filename'],
+                  # format=download_fmt,
+                  db=response['db'],
+                  user=response['user'],
+                  search_path=response['search_path']))
+    return response
index 4d85598a648710c9fc122e34de1ffb737daded3f..6bb5e49709a8a976e850c759db04cb96b8b06b3f 100644 (file)
@@ -45,6 +45,7 @@ stock_template_args = {
     'user': '',
     'csrf_token': 'somecsrftoken',
     'sql': 'select 1;',
+    'search_path': '"$user", somedb',
 }
 
 # The templates to test