Initial attempt at showing whitespace in SQL results
authorKarl O. Pinc <kop@karlpinc.com>
Tue, 24 Sep 2024 22:13:08 +0000 (17:13 -0500)
committerKarl O. Pinc <kop@karlpinc.com>
Tue, 24 Sep 2024 22:13:08 +0000 (17:13 -0500)
src/pgwui_sql/static/pgwui_sql.css
src/pgwui_sql/templates/sql.mak
src/pgwui_sql/views/sql.py

index 0463a2e9c0729a4dcbcba2e224865f7c450696e8..458e3b93ce7fe66ca45307138dc24c9c73057eb8 100644 (file)
@@ -59,4 +59,8 @@ textarea.sqltext { height: 40em;
                  top: 0;
                  background-color: white;  /* kludge, see above */
                  text-align: left; }
-.stickycontainer { overflow-y: clip; }    /* attach to heading's container */
+.stickycontainer { overflow-y: clip; }     /* attach to heading's container */
+.stickyfooting { position: sticky;         /* attach to footer */
+                 bottom: 0;
+                 background-color: white;  /* kludge, see above */
+                 text-align: right; }
index 7e28e0db8cfce205d1b2da598d58d51b1d8d45d2..18adeba8afe5d66828c8764a4e886dc0f01dcfc8 100644 (file)
@@ -26,6 +26,8 @@
     search_path  The requested search_path
     sql          Text of the sql command(s)
     result_rows  List of SQLResult objects
+    show_spaces  HTML attribute of input element indicating checked checkbox
+                 of the "Show spaces" checkbox
 </%doc>
 
 
   status_result = []
   %>
   
-  <% stmt_break = '<hr>' %>
-  % for result_row in result_rows:
-      ${stmt_break | n}
-      <% stmt_break = '<hr class="innerbreak">' %>
-
-      % if result_row.statusmessage is not None:
-          <p class="sql_status">
-          ${result_row.statusmessage.data.split(' ')[0]}</p>
-      % endif
-
-      % if result_row.rows:
-          <table class="stickycontainer">
-            % if result_row.heading is not None:
-                ${self.render_heading(result_row.heading.data)}
-            % endif
-            <tbody>
-              % for row in result_row.rows:
-                  ${self.render_row(row.data)}
-              % endfor
-            </tbody>
-          </table>
-      % endif
-
-      <p>${result_row.rowcount.data}</p>
-  % endfor
-  <hr>
+  <div class="stickycontainer">
+    <% stmt_break = '<hr>' %>
+    % for result_row in result_rows:
+        ${stmt_break | n}
+        <% stmt_break = '<hr class="innerbreak">' %>
+
+        % if result_row.statusmessage is not None:
+            <p class="sql_status">
+            ${result_row.statusmessage.data.split(' ')[0]}</p>
+        % endif
+
+        % if result_row.rows:
+            <table class="stickycontainer">
+              % if result_row.heading is not None:
+                  ${self.render_heading(result_row.heading.data)}
+              % endif
+              <tbody>
+                % for row in result_row.rows:
+                    ${self.render_row(row.data)}
+                % endfor
+              </tbody>
+            </table>
+        % endif
+
+        <p>${result_row.rowcount.data}</p>
+    % endfor
+    <div class="stickyfooting">
+      <hr>
+      <self.lib:td_label for_id="show_spaces_id">
+        Show spaces
+      </self.lib:td_label>
+      <self.lib:td_input tab_index="${tab_index}">
+        <input name="show_spaces"
+               tabindex="${tab_index.val}"
+               type="checkbox"
+               ${show_spaces | n}
+               onchange="whitespaceDisplay(this.checked)"
+        />
+      </self.lib:td_input>
+    </div>
+  </div>
 </%def>
 
 <%def name="result_form(tab_index)">
                   + ',top=' + window.screenTop)
            .focus();
   }
+  function whitespaceDisplay(checked) {
+           const tdSqltextSheet = document.adoptedStyleSheets[0];
+
+           if (checked) {
+             tdSqltextSheet.disabled = false;
+           } else {
+             tdSqltextSheet.disabled = true;
+           }
+  }
+
+  // See: https://web.dev/articles/constructable-stylesheets
+  function setupCSS() {
+    tdSqltextSheet = new CSSStyleSheet(disabled=true);
+    tdSqltextSheet.replaceSync(
+      'td.sqltext {
+         text-decoration-line: underline;
+         text-decoration-color: silver;
+         text-decoration-style: double; }');
+    document.adoptedStyleSheets = [tdSqltextSheet];
+  }
+
+  if (document.readyState === "loading") {
+    // Loading hasn't finished yet
+    document.addEventListener("DOMContentLoaded", setupCSS);
+  } else {
+    // `DOMContentLoaded` has already fired
+    setupCSS();
+  }
 </script>
 
 <noscript>
index 0838f19095fe61f52f89d795427c78c6c429e336..f261302dd8eb70e109bd745ef4fed306d3e67478 100644 (file)
@@ -27,6 +27,7 @@ import markupsafe
 import psycopg.errors
 import pyramid.response
 import tempfile
+import wtforms.fields
 
 import pgwui_core.core
 import pgwui_core.utils
@@ -34,12 +35,61 @@ import pgwui_sql.views.base
 from pgwui_common.view import auth_base_view
 
 from pgwui_sql import exceptions as sql_ex
-from pgwui_core.constants import CSV
+from pgwui_core.constants import (
+    CSV,
+    CHECKED,
+    UNCHECKED)
 from pgwui_sql.constants import MANY_FILES
 
 log = logging.getLogger(__name__)
 
 
+@attrs.define(slots=False)
+class SQLInitialPost(pgwui_sql.views.base.SQLBaseInitialPost):
+    show_spaces = attrs.field(default=False)
+
+
+class SQLWTForm(pgwui_sql.views.base.SQLBaseWTForm):
+    '''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.
+    show_spaces = wtforms.fields.BooleanField(
+        'Show spaces', id='show_spaces_id')
+
+
+@attrs.define(slots=False)
+class SQLForm(pgwui_sql.views.base.SQLBaseForm):
+    '''
+    Acts like a dict, but with extra methods.
+
+    Attributes:
+      uh      The UploadHandler instance using the form
+    '''
+    show_spaces = attrs.field(default=False)
+
+    def read(self):
+        '''
+        Read form data from the client
+        '''
+        # Read parent's data
+        super().read()
+
+        # Read our own data
+        self['show_spaces'] = self._form.show_spaces.data
+
+    def write(self, result, errors):
+        '''
+        Produces the dict pyramid will use to render the form.
+        '''
+        response = super().write(result, errors)
+        if self['show_spaces']:
+            response['show_spaces'] = CHECKED
+        else:
+            response['show_spaces'] = UNCHECKED
+        return response
+
+
 @attrs.define()
 class SQLResult():
     rows = attrs.field(factory=list)
@@ -78,7 +128,7 @@ class SQLResultsHandler(pgwui_core.core.SessionDBHandler):
 
     Attributes:
       request       A pyramid request instance
-      uf            A SQLBaseForm instance
+      uf            A SQLForm instance
       session       A pyramid session instance
       ue
       cur
@@ -89,9 +139,7 @@ class SQLResultsHandler(pgwui_core.core.SessionDBHandler):
     tfile = attrs.field(default=None)
 
     def make_form(self):
-        return pgwui_sql.views.base.SQLBaseForm().build(
-            self, ip=pgwui_sql.views.base.SQLBaseInitialPost(),
-            fc=pgwui_sql.views.base.SQLBaseWTForm)
+        return SQLForm().build(self, ip=SQLInitialPost(), fc=SQLWTForm)
 
     def read(self):
         super().read()
@@ -148,6 +196,9 @@ class SQLResultsHandler(pgwui_core.core.SessionDBHandler):
         self.data = tuple()
 
     def format_detail(self, err, stmt_text):
+        # The textarea HTML spec has changed from receiving textarea
+        # data with cr+lf as EOF to just cr.  See:
+        # https://github.com/whatwg/html/issues/6647
         detail = []
         if err.diag.message_detail is not None:
             detail.append(markupsafe.escape(err.diag.message_detail))