From 15ab0c959244c4de7a515e647fb60b8dd22d64b0 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 19 Aug 2024 13:48:18 -0500 Subject: [PATCH 01/85] fix: add pager stats to all grid vue data (fixes view history) also various other tweaks to modernize --- tailbone/grids/core.py | 6 +++++- tailbone/templates/grids/complete.mako | 2 +- tailbone/templates/master/view.mako | 30 +++++++++----------------- tailbone/views/master.py | 11 +++++----- 4 files changed, 21 insertions(+), 28 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 3caf909c..6ec55987 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -237,7 +237,7 @@ class Grid(WuttaGrid): kwargs.setdefault('sort_defaults', [(sortkey, sortdir)]) if kwargs.get('pageable'): - warnings.warn("component param is deprecated for Grid(); " + warnings.warn("pageable param is deprecated for Grid(); " "please use vue_tagname param instead", DeprecationWarning, stacklevel=2) kwargs.setdefault('paginated', kwargs.pop('pageable')) @@ -1703,6 +1703,10 @@ class Grid(WuttaGrid): results['checked_rows_code'] = '[{}]'.format( ', '.join(['{}[{}]'.format(var, i) for i in checked])) + if self.paginated and self.paginate_on_backend: + results['pager_stats'] = self.get_vue_pager_stats() + + # TODO: is this actually needed now that we have pager_stats? if self.paginated and self.pager is not None: results['total_items'] = self.pager.item_count results['per_page'] = self.pager.items_per_page diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index 8dc2d6dc..c136273b 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -115,7 +115,7 @@ ## paging % if grid.paginated: paginated - pagination-size="is-small" + pagination-size="${'small' if request.use_oruga else 'is-small'}" :per-page="perPage" :current-page="currentPage" @page-change="onPageChange" diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako index a61020f3..37f57237 100644 --- a/tailbone/templates/master/view.mako +++ b/tailbone/templates/master/view.mako @@ -120,9 +120,7 @@ </p> </div> - <versions-grid ref="versionsGrid" - @view-revision="viewRevision"> - </versions-grid> + ${versions_grid.render_vue_tag(ref='versionsGrid', **{'@view-revision': 'viewRevision'})} <${b}-modal :width="1200" % if request.use_oruga: @@ -237,17 +235,16 @@ </%def> <%def name="render_row_grid_component()"> - <tailbone-grid ref="rowGrid" id="rowGrid"></tailbone-grid> + ${rows_grid.render_vue_tag(id='rowGrid', ref='rowGrid')} </%def> <%def name="render_this_page_template()"> % if getattr(master, 'has_rows', False): - ## TODO: stop using |n filter - ${rows_grid.render_complete(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))|n} + ${rows_grid.render_vue_template(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))} % endif ${parent.render_this_page_template()} % if expose_versions: - ${versions_grid.render_complete()|n} + ${versions_grid.render_vue_template()} % endif </%def> @@ -338,19 +335,12 @@ <%def name="finalize_this_page_vars()"> ${parent.finalize_this_page_vars()} - <script type="text/javascript"> - - % if getattr(master, 'has_rows', False): - TailboneGrid.data = function() { return TailboneGridData } - Vue.component('tailbone-grid', TailboneGrid) - % endif - - % if expose_versions: - VersionsGrid.data = function() { return VersionsGridData } - Vue.component('versions-grid', VersionsGrid) - % endif - - </script> + % if getattr(master, 'has_rows', False): + ${rows_grid.render_vue_finalize()} + % endif + % if expose_versions: + ${versions_grid.render_vue_finalize()} + % endif </%def> diff --git a/tailbone/views/master.py b/tailbone/views/master.py index dde72106..ac74a070 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -347,8 +347,6 @@ class MasterView(View): # return grid data only, if partial page was requested if self.request.GET.get('partial'): context = grid.get_table_data() - if grid.paginated and grid.paginate_on_backend: - context['pager_stats'] = grid.get_vue_pager_stats() return self.json_response(context) context = { @@ -587,7 +585,8 @@ class MasterView(View): 'filterable': self.rows_filterable, 'use_byte_string_filters': self.use_byte_string_filters, 'sortable': self.rows_sortable, - 'pageable': self.rows_pageable, + 'sort_multiple': not self.request.use_oruga, + 'paginated': self.rows_pageable, 'extra_row_class': self.row_grid_extra_class, 'url': lambda obj: self.get_row_action_url('view', obj), } @@ -675,7 +674,7 @@ class MasterView(View): defaults = { 'model_class': continuum.transaction_class(self.get_model_class()), 'width': 'full', - 'pageable': True, + 'paginated': True, 'url': lambda txn: self.request.route_url(route, uuid=instance.uuid, txnid=txn.id), } if 'actions' not in kwargs: @@ -1387,8 +1386,8 @@ class MasterView(View): 'vue_tagname': 'versions-grid', 'ajax_data_url': self.get_action_url('revisions_data', obj), 'sortable': True, - 'default_sortkey': 'changed', - 'default_sortdir': 'desc', + 'sort_multiple': not self.request.use_oruga, + 'sort_defaults': ('changed', 'desc'), 'actions': [ self.make_action('view', icon='eye', url='#', click_handler='viewRevision(props.row)'), From b762a0782a1b677817166609ee8b94bca872a7e2 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 19 Aug 2024 13:57:36 -0500 Subject: [PATCH 02/85] =?UTF-8?q?bump:=20version=200.19.2=20=E2=86=92=200.?= =?UTF-8?q?19.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fe71f3f..c8017445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.19.3 (2024-08-19) + +### Fix + +- add pager stats to all grid vue data (fixes view history) + ## v0.19.2 (2024-08-19) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 8f840642..3e07abaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.19.2" +version = "0.19.3" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -59,7 +59,7 @@ dependencies = [ "transaction", "waitress", "WebHelpers2", - "WuttaWeb>=0.10.1", + "WuttaWeb>=0.10.2", "zope.sqlalchemy>=1.5", ] From d29b8403435237effd5ca2d122a9fb00ff6896b2 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 19 Aug 2024 14:38:41 -0500 Subject: [PATCH 03/85] fix: avoid deprecated reference to app db engine --- tailbone/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tailbone/app.py b/tailbone/app.py index 626c9206..ad9663cf 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -63,8 +63,8 @@ def make_rattail_config(settings): settings['wutta_config'] = rattail_config # configure database sessions - if hasattr(rattail_config, 'rattail_engine'): - tailbone.db.Session.configure(bind=rattail_config.rattail_engine) + if hasattr(rattail_config, 'appdb_engine'): + tailbone.db.Session.configure(bind=rattail_config.appdb_engine) if hasattr(rattail_config, 'trainwreck_engine'): tailbone.db.TrainwreckSession.configure(bind=rattail_config.trainwreck_engine) if hasattr(rattail_config, 'tempmon_engine'): From 1ec1eba49681867aac1e24e11d3b89ed8bba060e Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 19 Aug 2024 21:30:58 -0500 Subject: [PATCH 04/85] feat: refactor templates to simplify base/page/form structure to mimic what has been done in wuttaweb --- tailbone/templates/appinfo/configure.mako | 9 +- tailbone/templates/appinfo/index.mako | 11 +- tailbone/templates/appsettings.mako | 20 +- tailbone/templates/base.mako | 164 +++++---- tailbone/templates/batch/index.mako | 36 +- .../batch/inventory/desktop_form.mako | 11 +- tailbone/templates/batch/pos/view.mako | 10 +- .../batch/vendorcatalog/configure.mako | 11 +- .../templates/batch/vendorcatalog/create.mako | 9 +- tailbone/templates/batch/view.mako | 58 ++-- tailbone/templates/configure-menus.mako | 9 +- tailbone/templates/configure.mako | 9 +- tailbone/templates/customers/configure.mako | 9 +- .../templates/customers/pending/view.mako | 8 +- tailbone/templates/customers/view.mako | 8 +- tailbone/templates/custorders/create.mako | 18 +- tailbone/templates/custorders/items/view.mako | 8 +- .../templates/datasync/changes/index.mako | 9 +- tailbone/templates/datasync/configure.mako | 9 +- tailbone/templates/datasync/status.mako | 8 +- tailbone/templates/departments/view.mako | 10 +- tailbone/templates/form.mako | 20 +- tailbone/templates/generate_feature.mako | 9 +- tailbone/templates/importing/configure.mako | 9 +- tailbone/templates/importing/runjob.mako | 8 +- tailbone/templates/login.mako | 8 +- tailbone/templates/luigi/configure.mako | 9 +- tailbone/templates/luigi/index.mako | 9 +- tailbone/templates/master/clone.mako | 9 +- tailbone/templates/master/delete.mako | 7 +- tailbone/templates/master/form.mako | 9 +- tailbone/templates/master/index.mako | 44 +-- tailbone/templates/master/merge.mako | 23 +- tailbone/templates/master/versions.mako | 31 +- tailbone/templates/master/view.mako | 54 ++- tailbone/templates/members/configure.mako | 9 +- tailbone/templates/messages/create.mako | 13 +- tailbone/templates/messages/index.mako | 17 +- tailbone/templates/messages/view.mako | 15 +- tailbone/templates/ordering/view.mako | 21 +- tailbone/templates/ordering/worksheet.mako | 25 +- tailbone/templates/page.mako | 96 +++--- tailbone/templates/people/index.mako | 8 +- .../templates/people/merge-requests/view.mako | 8 +- tailbone/templates/people/view.mako | 30 +- tailbone/templates/people/view_profile.mako | 317 +++++++++--------- tailbone/templates/poser/reports/view.mako | 20 +- tailbone/templates/poser/setup.mako | 11 +- .../templates/principal/find_by_perm.mako | 53 ++- tailbone/templates/products/batch.mako | 9 +- tailbone/templates/products/configure.mako | 9 +- tailbone/templates/products/index.mako | 9 +- tailbone/templates/products/pending/view.mako | 23 +- tailbone/templates/products/view.mako | 9 +- .../templates/purchases/credits/index.mako | 9 +- tailbone/templates/receiving/view.mako | 26 +- tailbone/templates/receiving/view_row.mako | 9 +- .../templates/reports/generated/choose.mako | 13 +- .../templates/reports/generated/delete.mako | 11 +- .../templates/reports/generated/view.mako | 11 +- tailbone/templates/reports/inventory.mako | 11 +- tailbone/templates/reports/ordering.mako | 9 +- tailbone/templates/reports/problems/view.mako | 9 +- tailbone/templates/roles/create.mako | 12 +- tailbone/templates/roles/edit.mako | 12 +- tailbone/templates/roles/view.mako | 8 +- .../templates/settings/email/configure.mako | 9 +- tailbone/templates/settings/email/index.mako | 8 +- tailbone/templates/settings/email/view.mako | 21 +- tailbone/templates/tables/create.mako | 9 +- .../templates/tempmon/appliances/view.mako | 11 +- tailbone/templates/tempmon/clients/view.mako | 11 +- tailbone/templates/tempmon/dashboard.mako | 9 +- tailbone/templates/tempmon/probes/graph.mako | 9 +- .../templates/themes/butterball/base.mako | 100 ++++-- .../trainwreck/transactions/configure.mako | 11 +- .../trainwreck/transactions/rollover.mako | 11 +- .../trainwreck/transactions/view.mako | 10 +- .../trainwreck/transactions/view_row.mako | 11 +- .../templates/units-of-measure/index.mako | 19 +- tailbone/templates/upgrades/configure.mako | 9 +- tailbone/templates/upgrades/view.mako | 21 +- tailbone/templates/users/preferences.mako | 11 +- tailbone/templates/users/view.mako | 9 +- tailbone/templates/vendors/configure.mako | 11 +- tailbone/templates/views/model/create.mako | 9 +- tailbone/templates/workorders/view.mako | 9 +- 87 files changed, 818 insertions(+), 1045 deletions(-) diff --git a/tailbone/templates/appinfo/configure.mako b/tailbone/templates/appinfo/configure.mako index aab180c4..4794f00b 100644 --- a/tailbone/templates/appinfo/configure.mako +++ b/tailbone/templates/appinfo/configure.mako @@ -213,9 +213,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.weblibs = ${json.dumps(weblibs)|n} @@ -245,6 +245,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/appinfo/index.mako b/tailbone/templates/appinfo/index.mako index 73f53920..68244300 100644 --- a/tailbone/templates/appinfo/index.mako +++ b/tailbone/templates/appinfo/index.mako @@ -114,14 +114,9 @@ </${b}-collapse> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(request.rattail_config.prioritized_files, 1)])|n} - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/appsettings.mako b/tailbone/templates/appsettings.mako index 4f935956..ba667e0e 100644 --- a/tailbone/templates/appsettings.mako +++ b/tailbone/templates/appsettings.mako @@ -15,8 +15,8 @@ <app-settings :groups="groups" :showing-group="showingGroup"></app-settings> </%def> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} <script type="text/x-template" id="app-settings-template"> <div class="form"> @@ -150,19 +150,18 @@ </script> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.groups = ${json.dumps(settings_data)|n} ThisPageData.showingGroup = ${json.dumps(current_group or '')|n} - </script> </%def> -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - <script type="text/javascript"> +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + <script> Vue.component('app-settings', { template: '#app-settings-template', @@ -193,6 +192,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index 8e3b7785..a0e58e22 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -34,17 +34,21 @@ </head> <body> - ${declare_formposter_mixin()} - - ${self.body()} - - <div id="whole-page-app"> + <div id="app" style="height: 100%;"> <whole-page></whole-page> </div> - ${self.render_whole_page_template()} - ${self.make_whole_page_component()} - ${self.make_whole_page_app()} + ## TODO: this must come before the self.body() call..but why? + ${declare_formposter_mixin()} + + ## content body from derived/child template + ${self.body()} + + ## Vue app + ${self.render_vue_templates()} + ${self.modify_vue_vars()} + ${self.make_vue_components()} + ${self.make_vue_app()} </body> </html> @@ -181,7 +185,7 @@ <%def name="head_tags()"></%def> -<%def name="render_whole_page_template()"> +<%def name="render_vue_template_whole_page()"> <script type="text/x-template" id="whole-page-template"> <div> <header> @@ -749,11 +753,8 @@ % endif </%def> -<%def name="declare_whole_page_vars()"> - ${page_help.declare_vars()} - ${multi_file_upload.declare_vars()} - ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))} - <script type="text/javascript"> +<%def name="render_vue_script_whole_page()"> + <script> let WholePage = { template: '#whole-page-template', @@ -889,57 +890,6 @@ </script> </%def> -<%def name="modify_whole_page_vars()"> - <script type="text/javascript"> - - % if request.user: - FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n} - FeedbackFormData.userName = ${json.dumps(str(request.user))|n} - % endif - - </script> -</%def> - -<%def name="finalize_whole_page_vars()"> - ## NOTE: if you override this, must use <script> tags -</%def> - -<%def name="make_whole_page_component()"> - - ${make_grid_filter_components()} - - ${self.declare_whole_page_vars()} - ${self.modify_whole_page_vars()} - ${self.finalize_whole_page_vars()} - - ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))} - - ${page_help.make_component()} - ${multi_file_upload.make_component()} - - <script type="text/javascript"> - - FeedbackForm.data = function() { return FeedbackFormData } - - Vue.component('feedback-form', FeedbackForm) - - WholePage.data = function() { return WholePageData } - - Vue.component('whole-page', WholePage) - - </script> -</%def> - -<%def name="make_whole_page_app()"> - <script type="text/javascript"> - - new Vue({ - el: '#whole-page-app' - }) - - </script> -</%def> - <%def name="wtfield(form, name, **kwargs)"> <div class="field-wrapper${' error' if form[name].errors else ''}"> <label for="${name}">${form[name].label}</label> @@ -961,3 +911,87 @@ </div> </div> </%def> + +############################## +## vue components + app +############################## + +<%def name="render_vue_templates()"> + ${page_help.declare_vars()} + ${multi_file_upload.declare_vars()} + ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))} + ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))} + + ## DEPRECATED; called for back-compat + ${self.render_whole_page_template()} +</%def> + +## DEPRECATED; remains for back-compat +<%def name="render_whole_page_template()"> + ${self.render_vue_template_whole_page()} + ${self.declare_whole_page_vars()} +</%def> + +## DEPRECATED; remains for back-compat +<%def name="declare_whole_page_vars()"> + ${self.render_vue_script_whole_page()} +</%def> + +<%def name="modify_vue_vars()"> + ## DEPRECATED; called for back-compat + ${self.modify_whole_page_vars()} +</%def> + +## DEPRECATED; remains for back-compat +<%def name="modify_whole_page_vars()"> + <script> + + % if request.user: + FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n} + FeedbackFormData.userName = ${json.dumps(str(request.user))|n} + % endif + + </script> +</%def> + +<%def name="make_vue_components()"> + ${make_grid_filter_components()} + ${page_help.make_component()} + ${multi_file_upload.make_component()} + <script> + FeedbackForm.data = function() { return FeedbackFormData } + Vue.component('feedback-form', FeedbackForm) + </script> + + ## DEPRECATED; called for back-compat + ${self.finalize_whole_page_vars()} + ${self.make_whole_page_component()} +</%def> + +## DEPRECATED; remains for back-compat +<%def name="make_whole_page_component()"> + <script> + WholePage.data = function() { return WholePageData } + Vue.component('whole-page', WholePage) + </script> +</%def> + +<%def name="make_vue_app()"> + ## DEPRECATED; called for back-compat + ${self.make_whole_page_app()} +</%def> + +## DEPRECATED; remains for back-compat +<%def name="make_whole_page_app()"> + <script> + new Vue({ + el: '#app' + }) + </script> +</%def> + +############################## +## DEPRECATED +############################## + +<%def name="finalize_whole_page_vars()"></%def> diff --git a/tailbone/templates/batch/index.mako b/tailbone/templates/batch/index.mako index a7808590..a1b11b89 100644 --- a/tailbone/templates/batch/index.mako +++ b/tailbone/templates/batch/index.mako @@ -64,10 +64,17 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + % if master.results_executable and master.has_perm('execute_multiple'): + ${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n} + % endif +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.results_refreshable and master.has_perm('refresh'): - <script type="text/javascript"> + <script> TailboneGridData.refreshResultsButtonText = "Refresh Results" TailboneGridData.refreshResultsButtonDisabled = false @@ -81,7 +88,7 @@ </script> % endif % if master.results_executable and master.has_perm('execute_multiple'): - <script type="text/javascript"> + <script> ${execute_form.vue_component}.methods.submit = function() { this.$refs.actualExecuteForm.submit() @@ -118,25 +125,12 @@ % endif </%def> -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} % if master.results_executable and master.has_perm('execute_multiple'): - <script type="text/javascript"> - + <script> ${execute_form.vue_component}.data = function() { return ${execute_form.vue_component}Data } - - Vue.component('${execute_form.component}', ${execute_form.vue_component}) - + Vue.component('${execute_form.vue_tagname}', ${execute_form.vue_component}) </script> % endif </%def> - -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - % if master.results_executable and master.has_perm('execute_multiple'): - ${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n} - % endif -</%def> - - -${parent.body()} diff --git a/tailbone/templates/batch/inventory/desktop_form.mako b/tailbone/templates/batch/inventory/desktop_form.mako index 8ca32ce0..cddaa2c5 100644 --- a/tailbone/templates/batch/inventory/desktop_form.mako +++ b/tailbone/templates/batch/inventory/desktop_form.mako @@ -297,14 +297,9 @@ </script> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.toggleCompleteSubmitting = false - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/batch/pos/view.mako b/tailbone/templates/batch/pos/view.mako index bdb8709d..5ecabd4d 100644 --- a/tailbone/templates/batch/pos/view.mako +++ b/tailbone/templates/batch/pos/view.mako @@ -1,13 +1,9 @@ ## -*- coding: utf-8; -*- <%inherit file="/batch/view.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ${form.vue_component}Data.taxesData = ${json.dumps(taxes_data)|n} - </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/batch/vendorcatalog/configure.mako b/tailbone/templates/batch/vendorcatalog/configure.mako index 0d57053e..4f91cb02 100644 --- a/tailbone/templates/batch/vendorcatalog/configure.mako +++ b/tailbone/templates/batch/vendorcatalog/configure.mako @@ -39,14 +39,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.catalogParsers = ${json.dumps(catalog_parsers_data)|n} - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/batch/vendorcatalog/create.mako b/tailbone/templates/batch/vendorcatalog/create.mako index 63865bd5..d9d62bd1 100644 --- a/tailbone/templates/batch/vendorcatalog/create.mako +++ b/tailbone/templates/batch/vendorcatalog/create.mako @@ -1,9 +1,9 @@ ## -*- coding: utf-8; -*- <%inherit file="/batch/create.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ${form.vue_component}Data.parsers = ${json.dumps(parsers_data)|n} @@ -37,6 +37,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako index bef18cd4..cdfa9ba7 100644 --- a/tailbone/templates/batch/view.mako +++ b/tailbone/templates/batch/view.mako @@ -149,12 +149,6 @@ </nav> </%def> -<%def name="render_form_template()"> - ## TODO: should use self.render_form_buttons() - ## ${form.render_deform(form_id='batch-form', buttons=capture(self.render_form_buttons))|n} - ${form.render_deform(form_id='batch-form', buttons=capture(buttons))|n} -</%def> - <%def name="render_this_page()"> ${parent.render_this_page()} @@ -197,16 +191,6 @@ </%def> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - % if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'): - ${upload_worksheet_form.render_deform(buttons=False, form_kwargs={'ref': 'actualUploadForm'})|n} - % endif - % if master.handler.executable(batch) and master.has_perm('execute'): - ${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n} - % endif -</%def> - <%def name="render_form()"> <div class="form"> <${form.component} @show-upload="showUploadDialog = true"> @@ -267,9 +251,27 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + % if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'): + ${upload_worksheet_form.render_deform(buttons=False, form_kwargs={'ref': 'actualUploadForm'})|n} + % endif + % if master.handler.executable(batch) and master.has_perm('execute'): + ${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n} + % endif +</%def> + +## DEPRECATED; remains for back-compat +## nb. this is called by parent template, /form.mako +<%def name="render_form_template()"> + ## TODO: should use self.render_form_buttons() + ## ${form.render_deform(form_id='batch-form', buttons=capture(self.render_form_buttons))|n} + ${form.render_deform(form_id='batch-form', buttons=capture(buttons))|n} +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.statusBreakdownData = ${json.dumps(status_breakdown_data)|n} @@ -340,28 +342,18 @@ </script> </%def> -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} % if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'): - <script type="text/javascript"> - - ## UploadForm + <script> ${upload_worksheet_form.vue_component}.data = function() { return ${upload_worksheet_form.vue_component}Data } Vue.component('${upload_worksheet_form.component}', ${upload_worksheet_form.vue_component}) - </script> % endif - % if execute_enabled and master.has_perm('execute'): - <script type="text/javascript"> - - ## ExecuteForm + <script> ${execute_form.vue_component}.data = function() { return ${execute_form.vue_component}Data } Vue.component('${execute_form.component}', ${execute_form.vue_component}) - </script> % endif </%def> - - -${parent.body()} diff --git a/tailbone/templates/configure-menus.mako b/tailbone/templates/configure-menus.mako index c0200912..c7f46d21 100644 --- a/tailbone/templates/configure-menus.mako +++ b/tailbone/templates/configure-menus.mako @@ -208,9 +208,9 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.menuSequence = ${json.dumps([m['key'] for m in menus])|n} @@ -443,6 +443,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/configure.mako b/tailbone/templates/configure.mako index f33779c8..272aadce 100644 --- a/tailbone/templates/configure.mako +++ b/tailbone/templates/configure.mako @@ -205,9 +205,9 @@ ${h.end_form()} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if simple_settings is not Undefined: ThisPageData.simpleSettings = ${json.dumps(simple_settings)|n} @@ -293,6 +293,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/customers/configure.mako b/tailbone/templates/customers/configure.mako index e68f4543..1a6dca8b 100644 --- a/tailbone/templates/customers/configure.mako +++ b/tailbone/templates/customers/configure.mako @@ -88,9 +88,9 @@ </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPage.methods.getLabelForKey = function(key) { switch (key) { @@ -111,6 +111,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/customers/pending/view.mako b/tailbone/templates/customers/pending/view.mako index e9e54c99..1cea9d1f 100644 --- a/tailbone/templates/customers/pending/view.mako +++ b/tailbone/templates/customers/pending/view.mako @@ -106,9 +106,9 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.resolvePersonShowDialog = false ThisPageData.resolvePersonUUID = null @@ -139,5 +139,3 @@ </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/customers/view.mako b/tailbone/templates/customers/view.mako index bbca9580..490e4757 100644 --- a/tailbone/templates/customers/view.mako +++ b/tailbone/templates/customers/view.mako @@ -16,9 +16,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if expose_shoppers: ${form.vue_component}Data.shoppers = ${json.dumps(shoppers_data)|n} @@ -36,5 +36,3 @@ </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/custorders/create.mako b/tailbone/templates/custorders/create.mako index 63505422..382a121f 100644 --- a/tailbone/templates/custorders/create.mako +++ b/tailbone/templates/custorders/create.mako @@ -47,10 +47,9 @@ </div> </%def> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} ${product_lookup.tailbone_product_lookup_template()} - <script type="text/x-template" id="customer-order-creator-template"> <div> @@ -1265,12 +1264,7 @@ </div> </script> -</%def> - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - ${product_lookup.tailbone_product_lookup_component()} - <script type="text/javascript"> + <script> const CustomerOrderCreator = { template: '#customer-order-creator-template', @@ -2406,5 +2400,7 @@ </script> </%def> - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + ${product_lookup.tailbone_product_lookup_component()} +</%def> diff --git a/tailbone/templates/custorders/items/view.mako b/tailbone/templates/custorders/items/view.mako index 8eaee69a..4cc92bbf 100644 --- a/tailbone/templates/custorders/items/view.mako +++ b/tailbone/templates/custorders/items/view.mako @@ -291,9 +291,9 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ${form.vue_component}Data.eventsData = ${json.dumps(events_data)|n} @@ -448,5 +448,3 @@ </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/datasync/changes/index.mako b/tailbone/templates/datasync/changes/index.mako index 6d171619..86f5c121 100644 --- a/tailbone/templates/datasync/changes/index.mako +++ b/tailbone/templates/datasync/changes/index.mako @@ -26,9 +26,9 @@ </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if request.has_perm('datasync.restart'): TailboneGridData.restartDatasyncFormSubmitting = false @@ -50,6 +50,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/datasync/configure.mako b/tailbone/templates/datasync/configure.mako index 7922d189..3651d0c4 100644 --- a/tailbone/templates/datasync/configure.mako +++ b/tailbone/templates/datasync/configure.mako @@ -599,9 +599,9 @@ </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.showConfigFilesNote = false ThisPageData.profilesData = ${json.dumps(profiles_data)|n} @@ -982,6 +982,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/datasync/status.mako b/tailbone/templates/datasync/status.mako index c782dec6..e14686f8 100644 --- a/tailbone/templates/datasync/status.mako +++ b/tailbone/templates/datasync/status.mako @@ -115,8 +115,9 @@ </${b}-table> </%def> -<%def name="modify_this_page_vars()"> - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.processInfo = ${json.dumps(process_info)|n} @@ -171,6 +172,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/departments/view.mako b/tailbone/templates/departments/view.mako index f892f333..c5c39cbb 100644 --- a/tailbone/templates/departments/view.mako +++ b/tailbone/templates/departments/view.mako @@ -1,13 +1,9 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/view.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ${form.vue_component}Data.employeesData = ${json.dumps(employees_data)|n} - </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/form.mako b/tailbone/templates/form.mako index fec721fd..3bb04257 100644 --- a/tailbone/templates/form.mako +++ b/tailbone/templates/form.mako @@ -90,15 +90,15 @@ <%def name="before_object_helpers()"></%def> -<%def name="render_this_page_template()"> +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} % if form is not Undefined: ${self.render_form_template()} % endif - ${parent.render_this_page_template()} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if main_form_collapsible: <script> ThisPageData.mainFormPanelOpen = ${'false' if main_form_autocollapse else 'true'} @@ -106,18 +106,12 @@ % endif </%def> -<%def name="finalize_this_page_vars()"> - ${parent.finalize_this_page_vars()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} % if form is not Undefined: - <script type="text/javascript"> - + <script> ${form.vue_component}.data = function() { return ${form.vue_component}Data } - Vue.component('${form.vue_tagname}', ${form.vue_component}) - </script> % endif </%def> - - -${parent.body()} diff --git a/tailbone/templates/generate_feature.mako b/tailbone/templates/generate_feature.mako index 18a26f58..0f2a9f7b 100644 --- a/tailbone/templates/generate_feature.mako +++ b/tailbone/templates/generate_feature.mako @@ -276,9 +276,9 @@ </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.featureType = ${json.dumps(feature_type)|n} ThisPageData.resultGenerated = ${json.dumps(bool(result))|n} @@ -385,6 +385,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/importing/configure.mako b/tailbone/templates/importing/configure.mako index 0396745a..2445341d 100644 --- a/tailbone/templates/importing/configure.mako +++ b/tailbone/templates/importing/configure.mako @@ -144,9 +144,9 @@ </b-modal> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.handlersData = ${json.dumps(handlers_data)|n} @@ -203,6 +203,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/importing/runjob.mako b/tailbone/templates/importing/runjob.mako index 23526ed2..a9625bc3 100644 --- a/tailbone/templates/importing/runjob.mako +++ b/tailbone/templates/importing/runjob.mako @@ -63,9 +63,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ${form.vue_component}Data.submittingRun = false ${form.vue_component}Data.submittingExplain = false @@ -86,5 +86,3 @@ </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/login.mako b/tailbone/templates/login.mako index f898660f..3eb46403 100644 --- a/tailbone/templates/login.mako +++ b/tailbone/templates/login.mako @@ -57,8 +57,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ${form.vue_component}Data.usernameInput = null @@ -81,6 +82,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/luigi/configure.mako b/tailbone/templates/luigi/configure.mako index 49060ceb..de364828 100644 --- a/tailbone/templates/luigi/configure.mako +++ b/tailbone/templates/luigi/configure.mako @@ -297,9 +297,9 @@ </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.overnightTasks = ${json.dumps(overnight_tasks)|n} ThisPageData.overnightTaskShowDialog = false @@ -425,6 +425,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/luigi/index.mako b/tailbone/templates/luigi/index.mako index b5134c25..0dd72d01 100644 --- a/tailbone/templates/luigi/index.mako +++ b/tailbone/templates/luigi/index.mako @@ -255,9 +255,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if master.has_perm('restart_scheduler'): @@ -374,6 +374,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/master/clone.mako b/tailbone/templates/master/clone.mako index 59d6aea2..4c7e4662 100644 --- a/tailbone/templates/master/clone.mako +++ b/tailbone/templates/master/clone.mako @@ -34,9 +34,9 @@ ${h.end_form()} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> TailboneFormData.formSubmitting = false TailboneFormData.submitButtonText = "Yes, please clone away" @@ -48,6 +48,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/master/delete.mako b/tailbone/templates/master/delete.mako index c6187d55..d2f517d9 100644 --- a/tailbone/templates/master/delete.mako +++ b/tailbone/templates/master/delete.mako @@ -33,8 +33,8 @@ ${h.end_form()} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} <script> ${form.vue_component}Data.formSubmitting = false @@ -45,6 +45,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/master/form.mako b/tailbone/templates/master/form.mako index fac18ee2..17063c21 100644 --- a/tailbone/templates/master/form.mako +++ b/tailbone/templates/master/form.mako @@ -1,9 +1,9 @@ ## -*- coding: utf-8; -*- <%inherit file="/form.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ## declare extra data needed by form % if form is not Undefined and getattr(form, 'json_data', None): @@ -28,6 +28,3 @@ % endif </%def> - - -${parent.body()} diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako index 81c11213..a2d26c60 100644 --- a/tailbone/templates/master/index.mako +++ b/tailbone/templates/master/index.mako @@ -265,6 +265,11 @@ </%def> +## DEPRECATED; remains for back-compat +<%def name="render_this_page()"> + ${self.page_content()} +</%def> + <%def name="page_content()"> % if download_results_path: @@ -290,34 +295,28 @@ % endif </%def> -<%def name="make_grid_component()"> - ${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())} -</%def> - <%def name="render_grid_component()"> ${grid.render_vue_tag()} </%def> -<%def name="make_this_page_component()"> +############################## +## vue components +############################## - ## define grid +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + + ## DEPRECATED; called for back-compat ${self.make_grid_component()} - - ${parent.make_this_page_component()} - - ## finalize grid - <script> - ${grid.vue_component}.data = function() { return ${grid.vue_component}Data } - Vue.component('${grid.vue_tagname}', ${grid.vue_component}) - </script> </%def> -<%def name="render_this_page()"> - ${self.page_content()} +## DEPRECATED; remains for back-compat +<%def name="make_grid_component()"> + ${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} <script type="text/javascript"> % if getattr(master, 'supports_grid_totals', False): @@ -624,5 +623,10 @@ </script> </%def> - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + <script> + ${grid.vue_component}.data = function() { return ${grid.vue_component}Data } + Vue.component('${grid.vue_tagname}', ${grid.vue_component}) + </script> +</%def> diff --git a/tailbone/templates/master/merge.mako b/tailbone/templates/master/merge.mako index 5d90043f..487d258d 100644 --- a/tailbone/templates/master/merge.mako +++ b/tailbone/templates/master/merge.mako @@ -109,8 +109,8 @@ <merge-buttons></merge-buttons> </%def> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} <script type="text/x-template" id="merge-buttons-template"> <div class="level" style="margin-top: 2em;"> @@ -147,11 +147,7 @@ </div> </div> </script> -</%def> - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - <script type="text/javascript"> + <script> const MergeButtons = { template: '#merge-buttons-template', @@ -175,12 +171,13 @@ } } - Vue.component('merge-buttons', MergeButtons) - - <% request.register_component('merge-buttons', 'MergeButtons') %> - </script> </%def> - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + <script> + Vue.component('merge-buttons', MergeButtons) + <% request.register_component('merge-buttons', 'MergeButtons') %> + </script> +</%def> diff --git a/tailbone/templates/master/versions.mako b/tailbone/templates/master/versions.mako index 307674b8..a6bb14f0 100644 --- a/tailbone/templates/master/versions.mako +++ b/tailbone/templates/master/versions.mako @@ -16,27 +16,16 @@ ${self.page_content()} </%def> -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - <script type="text/javascript"> - - TailboneGrid.data = function() { return TailboneGridData } - - Vue.component('tailbone-grid', TailboneGrid) - - </script> -</%def> - -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - - ## TODO: stop using |n filter - ${grid.render_complete()|n} -</%def> - <%def name="page_content()"> - <tailbone-grid :csrftoken="csrftoken"> - </tailbone-grid> + ${grid.render_vue_tag(**{':csrftoken': 'csrftoken'})} </%def> -${parent.body()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + ${grid.render_vue_template()} +</%def> + +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + ${grid.render_vue_finalize()} +</%def> diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako index 37f57237..0a1f9c62 100644 --- a/tailbone/templates/master/view.mako +++ b/tailbone/templates/master/view.mako @@ -238,21 +238,34 @@ ${rows_grid.render_vue_tag(id='rowGrid', ref='rowGrid')} </%def> -<%def name="render_this_page_template()"> +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} % if getattr(master, 'has_rows', False): ${rows_grid.render_vue_template(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))} % endif - ${parent.render_this_page_template()} % if expose_versions: ${versions_grid.render_vue_template()} % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - % if expose_versions: - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> + % if getattr(master, 'touchable', False) and master.has_perm('touch'): + + WholePageData.touchSubmitting = false + + WholePage.methods.touchRecord = function() { + this.touchSubmitting = true + location.href = '${master.get_action_url('touch', instance)}' + } + + % endif + + % if expose_versions: + + WholePageData.viewingHistory = false ThisPage.props.viewingHistory = Boolean ThisPageData.gettingRevisions = false @@ -307,34 +320,12 @@ this.viewVersionShowAllFields = !this.viewVersionShowAllFields } - </script> - % endif -</%def> - -<%def name="modify_whole_page_vars()"> - ${parent.modify_whole_page_vars()} - <script type="text/javascript"> - - % if getattr(master, 'touchable', False) and master.has_perm('touch'): - - WholePageData.touchSubmitting = false - - WholePage.methods.touchRecord = function() { - this.touchSubmitting = true - location.href = '${master.get_action_url('touch', instance)}' - } - % endif - - % if expose_versions: - WholePageData.viewingHistory = false - % endif - </script> </%def> -<%def name="finalize_this_page_vars()"> - ${parent.finalize_this_page_vars()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} % if getattr(master, 'has_rows', False): ${rows_grid.render_vue_finalize()} % endif @@ -342,6 +333,3 @@ ${versions_grid.render_vue_finalize()} % endif </%def> - - -${parent.body()} diff --git a/tailbone/templates/members/configure.mako b/tailbone/templates/members/configure.mako index 465bf611..f1f0e39f 100644 --- a/tailbone/templates/members/configure.mako +++ b/tailbone/templates/members/configure.mako @@ -52,9 +52,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPage.methods.getLabelForKey = function(key) { switch (key) { @@ -75,6 +75,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/messages/create.mako b/tailbone/templates/messages/create.mako index 4a15573b..39236f75 100644 --- a/tailbone/templates/messages/create.mako +++ b/tailbone/templates/messages/create.mako @@ -32,14 +32,14 @@ % endif </%def> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} ${message_recipients_template()} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> TailboneFormData.possibleRecipients = new Map(${json.dumps(available_recipients)|n}) TailboneFormData.recipientDisplayMap = ${json.dumps(recipient_display_map)|n} @@ -59,6 +59,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/messages/index.mako b/tailbone/templates/messages/index.mako index 3fc82fd3..eaa4b6c9 100644 --- a/tailbone/templates/messages/index.mako +++ b/tailbone/templates/messages/index.mako @@ -22,15 +22,15 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if request.matched_route.name in ('messages.inbox', 'messages.archive'): - <script type="text/javascript"> + <script> - TailboneGridData.moveMessagesSubmitting = false - TailboneGridData.moveMessagesText = null + ${grid.vue_component}Data.moveMessagesSubmitting = false + ${grid.vue_component}Data.moveMessagesText = null - TailboneGrid.computed.moveMessagesTextCurrent = function() { + ${grid.vue_component}.computed.moveMessagesTextCurrent = function() { if (this.moveMessagesText) { return this.moveMessagesText } @@ -38,7 +38,7 @@ return "Move " + count.toString() + " selected to ${'Archive' if request.matched_route.name == 'messages.inbox' else 'Inbox'}" } - TailboneGrid.methods.moveMessagesSubmit = function() { + ${grid.vue_component}.methods.moveMessagesSubmit = function() { this.moveMessagesSubmitting = true this.moveMessagesText = "Working, please wait..." } @@ -46,6 +46,3 @@ </script> % endif </%def> - - -${parent.body()} diff --git a/tailbone/templates/messages/view.mako b/tailbone/templates/messages/view.mako index 2e2baa60..36418698 100644 --- a/tailbone/templates/messages/view.mako +++ b/tailbone/templates/messages/view.mako @@ -82,22 +82,19 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> - TailboneFormData.showingAllRecipients = false + ${form.vue_component}Data.showingAllRecipients = false - TailboneForm.methods.showMoreRecipients = function() { + ${form.vue_component}.methods.showMoreRecipients = function() { this.showingAllRecipients = true } - TailboneForm.methods.hideMoreRecipients = function() { + ${form.vue_component}.methods.hideMoreRecipients = function() { this.showingAllRecipients = false } </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/ordering/view.mako b/tailbone/templates/ordering/view.mako index aed6fd75..584559c1 100644 --- a/tailbone/templates/ordering/view.mako +++ b/tailbone/templates/ordering/view.mako @@ -21,8 +21,8 @@ % endif </%def> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} % if not batch.executed and not batch.complete and master.has_perm('edit_row'): <script type="text/x-template" id="ordering-scanner-template"> <div> @@ -185,10 +185,10 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if not batch.executed and not batch.complete and master.has_perm('edit_row'): - <script type="text/javascript"> + <script> let OrderingScanner = { template: '#ordering-scanner-template', @@ -408,16 +408,11 @@ % endif </%def> -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} % if not batch.executed and not batch.complete and master.has_perm('edit_row'): - <script type="text/javascript"> - + <script> Vue.component('ordering-scanner', OrderingScanner) - </script> % endif </%def> - - -${parent.body()} diff --git a/tailbone/templates/ordering/worksheet.mako b/tailbone/templates/ordering/worksheet.mako index ca1abf6e..cb98c48f 100644 --- a/tailbone/templates/ordering/worksheet.mako +++ b/tailbone/templates/ordering/worksheet.mako @@ -199,9 +199,8 @@ <ordering-worksheet></ordering-worksheet> </%def> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} <script type="text/x-template" id="ordering-worksheet-template"> <div> <div class="form-wrapper"> @@ -239,11 +238,7 @@ ${self.order_form_grid()} </div> </script> -</%def> - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - <script type="text/javascript"> + <script> const OrderingWorksheet = { template: '#ordering-worksheet-template', @@ -298,14 +293,12 @@ }, } - Vue.component('ordering-worksheet', OrderingWorksheet) - </script> </%def> - -############################## -## page body -############################## - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + <script> + Vue.component('ordering-worksheet', OrderingWorksheet) + </script> +</%def> diff --git a/tailbone/templates/page.mako b/tailbone/templates/page.mako index 17d87c9a..54b47278 100644 --- a/tailbone/templates/page.mako +++ b/tailbone/templates/page.mako @@ -1,42 +1,26 @@ ## -*- coding: utf-8; -*- <%inherit file="/base.mako" /> -<%def name="context_menu_items()"> - % if context_menu_list_items is not Undefined: - % for item in context_menu_list_items: - <li>${item}</li> - % endfor - % endif +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + ${self.render_vue_template_this_page()} </%def> -<%def name="page_content()"></%def> - -<%def name="render_this_page()"> - <div style="display: flex;"> - - <div class="this-page-content" style="flex-grow: 1;"> - ${self.page_content()} - </div> - - <ul id="context-menu"> - ${self.context_menu_items()} - </ul> - - </div> +<%def name="render_vue_template_this_page()"> + ## DEPRECATED; called for back-compat + ${self.render_this_page_template()} </%def> <%def name="render_this_page_template()"> <script type="text/x-template" id="this-page-template"> <div> + ## DEPRECATED; called for back-compat ${self.render_this_page()} </div> </script> -</%def> + <script> -<%def name="declare_this_page_vars()"> - <script type="text/javascript"> - - let ThisPage = { + const ThisPage = { template: '#this-page-template', mixins: [SimpleRequestMixin], props: { @@ -52,7 +36,7 @@ }, } - let ThisPageData = { + const ThisPageData = { ## TODO: should find a better way to handle CSRF token csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}, } @@ -60,29 +44,63 @@ </script> </%def> -<%def name="modify_this_page_vars()"> - ## NOTE: if you override this, must use <script> tags +## DEPRECATED; remains for back-compat +<%def name="render_this_page()"> + <div style="display: flex;"> + + <div class="this-page-content" style="flex-grow: 1;"> + ${self.page_content()} + </div> + + ## DEPRECATED; remains for back-compat + <ul id="context-menu"> + ${self.context_menu_items()} + </ul> + </div> </%def> -<%def name="finalize_this_page_vars()"> - ## NOTE: if you override this, must use <script> tags +## nb. this is the canonical block for page content! +<%def name="page_content()"></%def> + +## DEPRECATED; remains for back-compat +<%def name="context_menu_items()"> + % if context_menu_list_items is not Undefined: + % for item in context_menu_list_items: + <li>${item}</li> + % endfor + % endif +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + + ## DEPRECATED; called for back-compat + ${self.declare_this_page_vars()} + ${self.modify_this_page_vars()} +</%def> + +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + ## DEPRECATED; called for back-compat + ${self.make_this_page_component()} </%def> <%def name="make_this_page_component()"> - ${self.declare_this_page_vars()} - ${self.modify_this_page_vars()} ${self.finalize_this_page_vars()} - - <script type="text/javascript"> - + <script> ThisPage.data = function() { return ThisPageData } - Vue.component('this-page', ThisPage) <% request.register_component('this-page', 'ThisPage') %> - </script> </%def> +############################## +## DEPRECATED +############################## -${self.render_this_page_template()} -${self.make_this_page_component()} +<%def name="declare_this_page_vars()"></%def> + +<%def name="modify_this_page_vars()"></%def> + +<%def name="finalize_this_page_vars()"></%def> diff --git a/tailbone/templates/people/index.mako b/tailbone/templates/people/index.mako index 6ce14633..cd6fddf1 100644 --- a/tailbone/templates/people/index.mako +++ b/tailbone/templates/people/index.mako @@ -61,9 +61,9 @@ ${parent.grid_tools()} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if getattr(master, 'mergeable', False) and master.has_perm('request_merge'): @@ -100,5 +100,3 @@ </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/people/merge-requests/view.mako b/tailbone/templates/people/merge-requests/view.mako index 9e8905cf..e2db1476 100644 --- a/tailbone/templates/people/merge-requests/view.mako +++ b/tailbone/templates/people/merge-requests/view.mako @@ -18,10 +18,10 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if not instance.merged and request.has_perm('people.merge'): - <script type="text/javascript"> + <script> ThisPageData.mergeFormButtonText = "Perform Merge" ThisPageData.mergeFormSubmitting = false @@ -34,5 +34,3 @@ </script> % endif </%def> - -${parent.body()} diff --git a/tailbone/templates/people/view.mako b/tailbone/templates/people/view.mako index d28d7558..15c669fa 100644 --- a/tailbone/templates/people/view.mako +++ b/tailbone/templates/people/view.mako @@ -2,6 +2,16 @@ <%inherit file="/master/view.mako" /> <%namespace file="/util.mako" import="view_profiles_helper" /> +<%def name="page_content()"> + ${parent.page_content()} + % if not instance.users and request.has_perm('users.create'): + ${h.form(url('people.make_user'), ref='makeUserForm')} + ${h.csrf_token(request)} + ${h.hidden('person_uuid', value=instance.uuid)} + ${h.end_form()} + % endif +</%def> + <%def name="object_helpers()"> ${parent.object_helpers()} ${view_profiles_helper([instance])} @@ -13,9 +23,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ${form.vue_component}.methods.clickMakeUser = function(event) { this.$emit('make-user') @@ -29,17 +39,3 @@ </script> </%def> - -<%def name="page_content()"> - ${parent.page_content()} - % if not instance.users and request.has_perm('users.create'): - ${h.form(url('people.make_user'), ref='makeUserForm')} - ${h.csrf_token(request)} - ${h.hidden('person_uuid', value=instance.uuid)} - ${h.end_form()} - % endif -</%def> - - -${parent.body()} - diff --git a/tailbone/templates/people/view_profile.mako b/tailbone/templates/people/view_profile.mako index cdb6c5cc..6ca5a84c 100644 --- a/tailbone/templates/people/view_profile.mako +++ b/tailbone/templates/people/view_profile.mako @@ -1966,30 +1966,97 @@ </div> </script> -</%def> + <script> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - ${self.render_personal_tab_template()} + let ProfileInfoData = { + activeTab: location.hash ? location.hash.substring(1) : 'personal', + tabchecks: ${json.dumps(tabchecks or {})|n}, + today: '${rattail_app.today()}', + profileLastChanged: Date.now(), + person: ${json.dumps(person_data or {})|n}, + phoneTypeOptions: ${json.dumps(phone_type_options or [])|n}, + emailTypeOptions: ${json.dumps(email_type_options or [])|n}, + maxLengths: ${json.dumps(max_lengths or {})|n}, - % if expose_members: - ${self.render_member_tab_template()} - % endif + % if request.has_perm('people_profile.view_versions'): + loadingRevisions: false, + showingRevisionDialog: false, + revision: {}, + revisionShowAllFields: false, + % endif + } - ${self.render_customer_tab_template()} - % if expose_customer_shoppers: - ${self.render_shopper_tab_template()} - % endif - ${self.render_employee_tab_template()} - ${self.render_notes_tab_template()} + let ProfileInfo = { + template: '#profile-info-template', + props: { + % if request.has_perm('people_profile.view_versions'): + viewingHistory: Boolean, + gettingRevisions: Boolean, + revisions: Array, + revisionVersionMap: null, + % endif + }, + computed: {}, + mounted() { - % if expose_transactions: - ${transactions_grid.render_complete(allow_save_defaults=False)|n} - ${self.render_transactions_tab_template()} - % endif + // auto-refresh whichever tab is shown first + ## TODO: how to not assume 'personal' is the default tab? + let tab = this.$refs['tab_' + (this.activeTab || 'personal')] + if (tab && tab.refreshTab) { + tab.refreshTab() + } + }, + methods: { - ${self.render_user_tab_template()} - ${self.render_profile_info_template()} + profileChanged(data) { + this.$emit('change-content-title', data.person.dynamic_content_title) + this.person = data.person + this.tabchecks = data.tabchecks + this.profileLastChanged = Date.now() + }, + + activeTabChanged(value) { + location.hash = value + this.refreshTabIfNeeded(value) + this.activeTabChangedExtra(value) + }, + + refreshTabIfNeeded(key) { + // TODO: this is *always* refreshing, should be more selective (?) + let tab = this.$refs['tab_' + key] + if (tab && tab.refreshIfNeeded) { + tab.refreshIfNeeded(this.profileLastChanged) + } + }, + + activeTabChangedExtra(value) {}, + + % if request.has_perm('people_profile.view_versions'): + + viewRevision(row) { + this.revision = this.revisionVersionMap[row.txnid] + this.showingRevisionDialog = true + }, + + viewPrevRevision() { + let txnid = this.revision.prev_txnid + this.revision = this.revisionVersionMap[txnid] + }, + + viewNextRevision() { + let txnid = this.revision.next_txnid + this.revision = this.revisionVersionMap[txnid] + }, + + toggleVersionFields() { + this.revisionShowAllFields = !this.revisionShowAllFields + }, + + % endif + }, + } + + </script> </%def> <%def name="declare_personal_tab_vars()"> @@ -3022,114 +3089,46 @@ </script> </%def> -<%def name="declare_profile_info_vars()"> - <script type="text/javascript"> - - let ProfileInfoData = { - activeTab: location.hash ? location.hash.substring(1) : 'personal', - tabchecks: ${json.dumps(tabchecks or {})|n}, - today: '${rattail_app.today()}', - profileLastChanged: Date.now(), - person: ${json.dumps(person_data or {})|n}, - phoneTypeOptions: ${json.dumps(phone_type_options or [])|n}, - emailTypeOptions: ${json.dumps(email_type_options or [])|n}, - maxLengths: ${json.dumps(max_lengths or {})|n}, - - % if request.has_perm('people_profile.view_versions'): - loadingRevisions: false, - showingRevisionDialog: false, - revision: {}, - revisionShowAllFields: false, - % endif - } - - let ProfileInfo = { - template: '#profile-info-template', - props: { - % if request.has_perm('people_profile.view_versions'): - viewingHistory: Boolean, - gettingRevisions: Boolean, - revisions: Array, - revisionVersionMap: null, - % endif - }, - computed: {}, - mounted() { - - // auto-refresh whichever tab is shown first - ## TODO: how to not assume 'personal' is the default tab? - let tab = this.$refs['tab_' + (this.activeTab || 'personal')] - if (tab && tab.refreshTab) { - tab.refreshTab() - } - }, - methods: { - - profileChanged(data) { - this.$emit('change-content-title', data.person.dynamic_content_title) - this.person = data.person - this.tabchecks = data.tabchecks - this.profileLastChanged = Date.now() - }, - - activeTabChanged(value) { - location.hash = value - this.refreshTabIfNeeded(value) - this.activeTabChangedExtra(value) - }, - - refreshTabIfNeeded(key) { - // TODO: this is *always* refreshing, should be more selective (?) - let tab = this.$refs['tab_' + key] - if (tab && tab.refreshIfNeeded) { - tab.refreshIfNeeded(this.profileLastChanged) - } - }, - - activeTabChangedExtra(value) {}, - - % if request.has_perm('people_profile.view_versions'): - - viewRevision(row) { - this.revision = this.revisionVersionMap[row.txnid] - this.showingRevisionDialog = true - }, - - viewPrevRevision() { - let txnid = this.revision.prev_txnid - this.revision = this.revisionVersionMap[txnid] - }, - - viewNextRevision() { - let txnid = this.revision.next_txnid - this.revision = this.revisionVersionMap[txnid] - }, - - toggleVersionFields() { - this.revisionShowAllFields = !this.revisionShowAllFields - }, - - % endif - }, - } - - </script> -</%def> - <%def name="make_profile_info_component()"> - ${self.declare_profile_info_vars()} - <script type="text/javascript"> + ## DEPRECATED; called for back-compat + ${self.declare_profile_info_vars()} + + <script> ProfileInfo.data = function() { return ProfileInfoData } Vue.component('profile-info', ProfileInfo) <% request.register_component('profile-info', 'ProfileInfo') %> - </script> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + + ${self.render_personal_tab_template()} + + % if expose_members: + ${self.render_member_tab_template()} + % endif + + ${self.render_customer_tab_template()} + % if expose_customer_shoppers: + ${self.render_shopper_tab_template()} + % endif + ${self.render_employee_tab_template()} + ${self.render_notes_tab_template()} + + % if expose_transactions: + ${transactions_grid.render_complete(allow_save_defaults=False)|n} + ${self.render_transactions_tab_template()} + % endif + + ${self.render_user_tab_template()} + ${self.render_profile_info_template()} +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if request.has_perm('people_profile.view_versions'): ThisPage.props.viewingHistory = Boolean @@ -3177,45 +3176,8 @@ }, } - </script> -</%def> -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - ${self.make_personal_tab_component()} - - % if expose_members: - ${self.make_member_tab_component()} - % endif - - ${self.make_customer_tab_component()} - % if expose_customer_shoppers: - ${self.make_shopper_tab_component()} - % endif - ${self.make_employee_tab_component()} - ${self.make_notes_tab_component()} - - % if expose_transactions: - <script type="text/javascript"> - - TransactionsGrid.data = function() { return TransactionsGridData } - Vue.component('transactions-grid', TransactionsGrid) - ## TODO: why is this line not needed? - ## <% request.register_component('transactions-grid', 'TransactionsGrid') %> - - </script> - ${self.make_transactions_tab_component()} - % endif - - ${self.make_user_tab_component()} - ${self.make_profile_info_component()} -</%def> - -<%def name="modify_whole_page_vars()"> - ${parent.modify_whole_page_vars()} - - % if request.has_perm('people_profile.view_versions'): - <script type="text/javascript"> + % if request.has_perm('people_profile.view_versions'): WholePageData.viewingHistory = false WholePageData.gettingRevisions = false @@ -3251,9 +3213,44 @@ }) } - </script> - % endif + % endif + </script> </%def> +<%def name="make_vue_components()"> + ${parent.make_vue_components()} -${parent.body()} + ${self.make_personal_tab_component()} + + % if expose_members: + ${self.make_member_tab_component()} + % endif + + ${self.make_customer_tab_component()} + % if expose_customer_shoppers: + ${self.make_shopper_tab_component()} + % endif + ${self.make_employee_tab_component()} + ${self.make_notes_tab_component()} + + % if expose_transactions: + <script type="text/javascript"> + + TransactionsGrid.data = function() { return TransactionsGridData } + Vue.component('transactions-grid', TransactionsGrid) + ## TODO: why is this line not needed? + ## <% request.register_component('transactions-grid', 'TransactionsGrid') %> + + </script> + ${self.make_transactions_tab_component()} + % endif + + ${self.make_user_tab_component()} + ${self.make_profile_info_component()} +</%def> + +############################## +## DEPRECATED +############################## + +<%def name="declare_profile_info_vars()"></%def> diff --git a/tailbone/templates/poser/reports/view.mako b/tailbone/templates/poser/reports/view.mako index 274a8806..cb8b51aa 100644 --- a/tailbone/templates/poser/reports/view.mako +++ b/tailbone/templates/poser/reports/view.mako @@ -62,19 +62,13 @@ <br /> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.has_perm('replace'): - <script type="text/javascript"> - - ${form.vue_component}Data.showUploadForm = false - - ${form.vue_component}Data.uploadFile = null - - ${form.vue_component}Data.uploadSubmitting = false - - </script> + <script> + ${form.vue_component}Data.showUploadForm = false + ${form.vue_component}Data.uploadFile = null + ${form.vue_component}Data.uploadSubmitting = false + </script> % endif </%def> - -${parent.body()} diff --git a/tailbone/templates/poser/setup.mako b/tailbone/templates/poser/setup.mako index 8d01bb33..239e7db2 100644 --- a/tailbone/templates/poser/setup.mako +++ b/tailbone/templates/poser/setup.mako @@ -118,14 +118,9 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.setupSubmitting = false - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/principal/find_by_perm.mako b/tailbone/templates/principal/find_by_perm.mako index 2ea289c8..ddc44e3d 100644 --- a/tailbone/templates/principal/find_by_perm.mako +++ b/tailbone/templates/principal/find_by_perm.mako @@ -10,8 +10,16 @@ </find-principals> </%def> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="principal_table()"> + <div + style="width: 50%;" + > + ${grid.render_table_element(data_prop='principalsData')|n} + </div> +</%def> + +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} <script type="text/x-template" id="find-principals-template"> <div> @@ -90,28 +98,6 @@ </div> </script> -</%def> - -<%def name="principal_table()"> - <div - style="width: 50%;" - > - ${grid.render_table_element(data_prop='principalsData')|n} - </div> -</%def> - -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - - ThisPageData.permissionGroups = ${json.dumps(perms_data)|n} - ThisPageData.sortedGroups = ${json.dumps(sorted_groups_data)|n} - - </script> -</%def> - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} <script type="text/javascript"> const FindPrincipals = { @@ -240,12 +226,21 @@ } } - Vue.component('find-principals', FindPrincipals) - - <% request.register_component('find-principals', 'FindPrincipals') %> - </script> </%def> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> + ThisPageData.permissionGroups = ${json.dumps(perms_data)|n} + ThisPageData.sortedGroups = ${json.dumps(sorted_groups_data)|n} + </script> +</%def> -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + <script> + Vue.component('find-principals', FindPrincipals) + <% request.register_component('find-principals', 'FindPrincipals') %> + </script> +</%def> diff --git a/tailbone/templates/products/batch.mako b/tailbone/templates/products/batch.mako index 66e38028..9f969468 100644 --- a/tailbone/templates/products/batch.mako +++ b/tailbone/templates/products/batch.mako @@ -60,9 +60,9 @@ </script> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako) @@ -114,6 +114,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/products/configure.mako b/tailbone/templates/products/configure.mako index 6121af67..a43a85d4 100644 --- a/tailbone/templates/products/configure.mako +++ b/tailbone/templates/products/configure.mako @@ -95,9 +95,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPage.methods.getTitleForKey = function(key) { switch (key) { @@ -118,6 +118,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/products/index.mako b/tailbone/templates/products/index.mako index b4731dee..5ffa9512 100644 --- a/tailbone/templates/products/index.mako +++ b/tailbone/templates/products/index.mako @@ -36,10 +36,10 @@ </${grid.component}> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if label_profiles and master.has_perm('print_labels'): - <script type="text/javascript"> + <script> ${grid.vue_component}Data.quickLabelProfile = ${json.dumps(label_profiles[0].uuid)|n} ${grid.vue_component}Data.quickLabelQuantity = 1 @@ -83,6 +83,3 @@ </script> % endif </%def> - - -${parent.body()} diff --git a/tailbone/templates/products/pending/view.mako b/tailbone/templates/products/pending/view.mako index 765c8838..72c9c76d 100644 --- a/tailbone/templates/products/pending/view.mako +++ b/tailbone/templates/products/pending/view.mako @@ -2,11 +2,6 @@ <%inherit file="/master/view.mako" /> <%namespace name="product_lookup" file="/products/lookup.mako" /> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - ${product_lookup.tailbone_product_lookup_template()} -</%def> - <%def name="page_content()"> ${parent.page_content()} @@ -67,9 +62,14 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + ${product_lookup.tailbone_product_lookup_template()} +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if master.has_perm('ignore_product') and instance.status_code in (enum.PENDING_PRODUCT_STATUS_PENDING, enum.PENDING_PRODUCT_STATUS_READY): @@ -124,10 +124,7 @@ </script> </%def> -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} ${product_lookup.tailbone_product_lookup_component()} </%def> - - -${parent.body()} diff --git a/tailbone/templates/products/view.mako b/tailbone/templates/products/view.mako index bd4afc7f..66ca3128 100644 --- a/tailbone/templates/products/view.mako +++ b/tailbone/templates/products/view.mako @@ -282,9 +282,9 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.vendorSourcesData = ${json.dumps(vendor_sources['data'])|n} ThisPageData.lookupCodesData = ${json.dumps(lookup_codes['data'])|n} @@ -411,6 +411,3 @@ % endif </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/purchases/credits/index.mako b/tailbone/templates/purchases/credits/index.mako index 0cfbc031..94028bdb 100644 --- a/tailbone/templates/purchases/credits/index.mako +++ b/tailbone/templates/purchases/credits/index.mako @@ -59,9 +59,9 @@ </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ${grid.vue_component}Data.changeStatusShowDialog = false ${grid.vue_component}Data.changeStatusOptions = ${json.dumps(status_options)|n} @@ -80,6 +80,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/receiving/view.mako b/tailbone/templates/receiving/view.mako index 45a8d66b..710dec4a 100644 --- a/tailbone/templates/receiving/view.mako +++ b/tailbone/templates/receiving/view.mako @@ -139,9 +139,15 @@ % endif </%def> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="object_helpers()"> + ${self.render_status_breakdown()} + ${self.render_po_vs_invoice_helper()} + ${self.render_execute_helper()} + ${self.render_tools_helper()} +</%def> +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} % if allow_edit_catalog_unit_cost or allow_edit_invoice_unit_cost: <script type="text/x-template" id="receiving-cost-editor-template"> <div> @@ -162,16 +168,9 @@ % endif </%def> -<%def name="object_helpers()"> - ${self.render_status_breakdown()} - ${self.render_po_vs_invoice_helper()} - ${self.render_execute_helper()} - ${self.render_tools_helper()} -</%def> - -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if allow_confirm_all_costs: @@ -389,6 +388,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/receiving/view_row.mako b/tailbone/templates/receiving/view_row.mako index 5077539c..086754c6 100644 --- a/tailbone/templates/receiving/view_row.mako +++ b/tailbone/templates/receiving/view_row.mako @@ -484,9 +484,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ## ThisPage.methods.editUnitCost = function() { ## alert("TODO: not yet implemented") @@ -720,6 +720,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/reports/generated/choose.mako b/tailbone/templates/reports/generated/choose.mako index a952fb6a..0921530c 100644 --- a/tailbone/templates/reports/generated/choose.mako +++ b/tailbone/templates/reports/generated/choose.mako @@ -53,13 +53,13 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> - TailboneFormData.reportDescriptions = ${json.dumps(report_descriptions)|n} + ${form.vue_component}Data.reportDescriptions = ${json.dumps(report_descriptions)|n} - TailboneForm.methods.reportTypeChanged = function(reportType) { + ${form.vue_component}.methods.reportTypeChanged = function(reportType) { this.$emit('report-change', this.reportDescriptions[reportType]) } @@ -71,6 +71,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/reports/generated/delete.mako b/tailbone/templates/reports/generated/delete.mako index bce54662..f60a9819 100644 --- a/tailbone/templates/reports/generated/delete.mako +++ b/tailbone/templates/reports/generated/delete.mako @@ -1,16 +1,11 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/delete.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if params_data is not Undefined: ${form.vue_component}Data.paramsData = ${json.dumps(params_data)|n} % endif - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/reports/generated/view.mako b/tailbone/templates/reports/generated/view.mako index e5bcc9e4..cce6f346 100644 --- a/tailbone/templates/reports/generated/view.mako +++ b/tailbone/templates/reports/generated/view.mako @@ -23,16 +23,11 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if params_data is not Undefined: ${form.vue_component}Data.paramsData = ${json.dumps(params_data)|n} % endif - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/reports/inventory.mako b/tailbone/templates/reports/inventory.mako index f051959f..cc5adc10 100644 --- a/tailbone/templates/reports/inventory.mako +++ b/tailbone/templates/reports/inventory.mako @@ -48,15 +48,10 @@ ${h.end_form()} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.departments = ${json.dumps([{'uuid': d.uuid, 'name': d.name} for d in departments])|n} ThisPageData.excludeNotForSale = true - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/reports/ordering.mako b/tailbone/templates/reports/ordering.mako index 1e526792..61ccdb16 100644 --- a/tailbone/templates/reports/ordering.mako +++ b/tailbone/templates/reports/ordering.mako @@ -81,9 +81,9 @@ <%def name="extra_fields()"></%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.vendorUUID = null ThisPageData.departments = [] @@ -127,6 +127,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/reports/problems/view.mako b/tailbone/templates/reports/problems/view.mako index 1d5cb14f..00ac1503 100644 --- a/tailbone/templates/reports/problems/view.mako +++ b/tailbone/templates/reports/problems/view.mako @@ -62,9 +62,9 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if weekdays_data is not Undefined: ${form.vue_component}Data.weekdaysData = ${json.dumps(weekdays_data)|n} @@ -75,6 +75,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/roles/create.mako b/tailbone/templates/roles/create.mako index 625b2675..89dd56c3 100644 --- a/tailbone/templates/roles/create.mako +++ b/tailbone/templates/roles/create.mako @@ -6,15 +6,11 @@ ${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> // TODO: this variable name should be more dynamic (?) since this is // connected to (and only here b/c of) the permissions field - TailboneFormData.showingPermissionGroup = '' - + ${form.vue_component}Data.showingPermissionGroup = '' </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/roles/edit.mako b/tailbone/templates/roles/edit.mako index 67f63013..e77cca33 100644 --- a/tailbone/templates/roles/edit.mako +++ b/tailbone/templates/roles/edit.mako @@ -6,15 +6,11 @@ ${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> // TODO: this variable name should be more dynamic (?) since this is // connected to (and only here b/c of) the permissions field - TailboneFormData.showingPermissionGroup = '' - + ${form.vue_component}Data.showingPermissionGroup = '' </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/roles/view.mako b/tailbone/templates/roles/view.mako index 0dc2956f..f5588695 100644 --- a/tailbone/templates/roles/view.mako +++ b/tailbone/templates/roles/view.mako @@ -6,9 +6,9 @@ ${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if users_data is not Undefined: ${form.vue_component}Data.usersData = ${json.dumps(users_data)|n} @@ -23,5 +23,3 @@ </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/settings/email/configure.mako b/tailbone/templates/settings/email/configure.mako index ef487809..f9c815c2 100644 --- a/tailbone/templates/settings/email/configure.mako +++ b/tailbone/templates/settings/email/configure.mako @@ -86,9 +86,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.testRecipient = ${json.dumps(user_email_address)|n} ThisPageData.sendingTest = false @@ -137,6 +137,3 @@ % endif </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/settings/email/index.mako b/tailbone/templates/settings/email/index.mako index 050a5833..ab8d6fa4 100644 --- a/tailbone/templates/settings/email/index.mako +++ b/tailbone/templates/settings/email/index.mako @@ -15,10 +15,10 @@ ${parent.render_grid_component()} </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.has_perm('configure'): - <script type="text/javascript"> + <script> ThisPageData.showEmails = 'available' @@ -65,5 +65,3 @@ </script> % endif </%def> - -${parent.body()} diff --git a/tailbone/templates/settings/email/view.mako b/tailbone/templates/settings/email/view.mako index c1bc5ed4..73ad7066 100644 --- a/tailbone/templates/settings/email/view.mako +++ b/tailbone/templates/settings/email/view.mako @@ -6,8 +6,8 @@ <email-preview-tools></email-preview-tools> </%def> -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} <script type="text/x-template" id="email-preview-tools-template"> ${h.form(url('email.preview'), **{'@submit': 'submitPreviewForm'})} @@ -72,10 +72,6 @@ ${h.end_form()} </script> -</%def> - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} <script type="text/javascript"> const EmailPreviewTools = { @@ -100,12 +96,13 @@ } } - Vue.component('email-preview-tools', EmailPreviewTools) - - <% request.register_component('email-preview-tools', 'EmailPreviewTools') %> - </script> </%def> - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + <script> + Vue.component('email-preview-tools', EmailPreviewTools) + <% request.register_component('email-preview-tools', 'EmailPreviewTools') %> + </script> +</%def> diff --git a/tailbone/templates/tables/create.mako b/tailbone/templates/tables/create.mako index 4fc2eb96..34844c5c 100644 --- a/tailbone/templates/tables/create.mako +++ b/tailbone/templates/tables/create.mako @@ -695,9 +695,9 @@ </b-steps> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> // nb. for warning user they may lose changes if leaving page ThisPageData.dirty = false @@ -983,6 +983,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/tempmon/appliances/view.mako b/tailbone/templates/tempmon/appliances/view.mako index 7dd9314a..a55af922 100644 --- a/tailbone/templates/tempmon/appliances/view.mako +++ b/tailbone/templates/tempmon/appliances/view.mako @@ -8,14 +8,9 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ${form.vue_component}Data.probesData = ${json.dumps(probes_data)|n} - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/tempmon/clients/view.mako b/tailbone/templates/tempmon/clients/view.mako index b1db423b..434da4c8 100644 --- a/tailbone/templates/tempmon/clients/view.mako +++ b/tailbone/templates/tempmon/clients/view.mako @@ -22,14 +22,9 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ${form.vue_component}Data.probesData = ${json.dumps(probes_data)|n} - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/tempmon/dashboard.mako b/tailbone/templates/tempmon/dashboard.mako index 396b0e68..befaf8b4 100644 --- a/tailbone/templates/tempmon/dashboard.mako +++ b/tailbone/templates/tempmon/dashboard.mako @@ -59,9 +59,9 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.appliances = ${json.dumps(appliances_data)|n} ThisPageData.applianceUUID = ${json.dumps(appliance.uuid if appliance else None)|n} @@ -118,6 +118,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/tempmon/probes/graph.mako b/tailbone/templates/tempmon/probes/graph.mako index 412f25dd..94a440e0 100644 --- a/tailbone/templates/tempmon/probes/graph.mako +++ b/tailbone/templates/tempmon/probes/graph.mako @@ -66,9 +66,9 @@ <canvas ref="tempchart" width="400" height="150"></canvas> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.currentTimeRange = ${json.dumps(current_time_range)|n} ThisPageData.chart = null @@ -128,6 +128,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/themes/butterball/base.mako b/tailbone/templates/themes/butterball/base.mako index 306b3430..14616474 100644 --- a/tailbone/templates/themes/butterball/base.mako +++ b/tailbone/templates/themes/butterball/base.mako @@ -20,38 +20,21 @@ </head> <body> - <div id="app" style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;"> + <div id="app" style="height: 100%;"> <whole-page></whole-page> </div> ## TODO: this must come before the self.body() call..but why? ${declare_formposter_mixin()} - ## global components used by various (but not all) pages - ${make_field_components()} - ${make_grid_filter_components()} - - ## global components for buefy-based template compatibility - ${make_http_plugin()} - ${make_buefy_plugin()} - ${make_buefy_components()} - - ## special global components, used by WholePage - ${self.make_menu_search_component()} - ${page_help.render_template()} - ${page_help.declare_vars()} - % if request.has_perm('common.feedback'): - ${self.make_feedback_component()} - % endif - - ## WholePage component - ${self.make_whole_page_component()} - ## content body from derived/child template ${self.body()} ## Vue app - ${self.make_whole_page_app()} + ${self.render_vue_templates()} + ${self.modify_vue_vars()} + ${self.make_vue_components()} + ${self.make_vue_app()} </body> </html> @@ -596,7 +579,7 @@ </script> </%def> -<%def name="render_whole_page_template()"> +<%def name="render_vue_template_whole_page()"> <script type="text/x-template" id="whole-page-template"> <div id="whole-page" style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;"> @@ -896,8 +879,6 @@ </footer> </div> </script> - -## ${multi_file_upload.render_template()} </%def> <%def name="render_this_page_component()"> @@ -1068,9 +1049,7 @@ % endif </%def> -<%def name="declare_whole_page_vars()"> -## ${multi_file_upload.declare_vars()} - +<%def name="render_vue_script_whole_page()"> <script> const WholePage = { @@ -1172,26 +1151,71 @@ </script> </%def> -<%def name="modify_whole_page_vars()"></%def> +############################## +## vue components + app +############################## -## TODO: do we really need this? -## <%def name="finalize_whole_page_vars()"></%def> +<%def name="render_vue_templates()"> +## ${multi_file_upload.render_template()} +## ${multi_file_upload.declare_vars()} -<%def name="make_whole_page_component()"> + ## global components used by various (but not all) pages + ${make_field_components()} + ${make_grid_filter_components()} + + ## global components for buefy-based template compatibility + ${make_http_plugin()} + ${make_buefy_plugin()} + ${make_buefy_components()} + + ## special global components, used by WholePage + ${self.make_menu_search_component()} + ${page_help.render_template()} + ${page_help.declare_vars()} + % if request.has_perm('common.feedback'): + ${self.make_feedback_component()} + % endif + + ## DEPRECATED; called for back-compat ${self.render_whole_page_template()} + + ## DEPRECATED; called for back-compat ${self.declare_whole_page_vars()} +</%def> + +## DEPRECATED; remains for back-compat +<%def name="render_whole_page_template()"> + ${self.render_vue_template_whole_page()} + ${self.render_vue_script_whole_page()} +</%def> + +<%def name="modify_vue_vars()"> + ## DEPRECATED; called for back-compat ${self.modify_whole_page_vars()} -## ${self.finalize_whole_page_vars()} +</%def> +<%def name="make_vue_components()"> ${page_help.make_component()} -## ${multi_file_upload.make_component()} + ## ${multi_file_upload.make_component()} + ## DEPRECATED; called for back-compat (?) + ${self.make_whole_page_component()} +</%def> + +## DEPRECATED; remains for back-compat +<%def name="make_whole_page_component()"> <script> WholePage.data = () => { return WholePageData } </script> <% request.register_component('whole-page', 'WholePage') %> </%def> +<%def name="make_vue_app()"> + ## DEPRECATED; called for back-compat + ${self.make_whole_page_app()} +</%def> + +## DEPRECATED; remains for back-compat <%def name="make_whole_page_app()"> <script type="module"> import {createApp} from 'vue' @@ -1223,3 +1247,11 @@ app.mount('#app') </script> </%def> + +############################## +## DEPRECATED +############################## + +<%def name="declare_whole_page_vars()"></%def> + +<%def name="modify_whole_page_vars()"></%def> diff --git a/tailbone/templates/trainwreck/transactions/configure.mako b/tailbone/templates/trainwreck/transactions/configure.mako index 4569759b..10c57e18 100644 --- a/tailbone/templates/trainwreck/transactions/configure.mako +++ b/tailbone/templates/trainwreck/transactions/configure.mako @@ -62,14 +62,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.hiddenDatabases = ${json.dumps(hidden_databases)|n} - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/trainwreck/transactions/rollover.mako b/tailbone/templates/trainwreck/transactions/rollover.mako index b36e7bc3..f26515b5 100644 --- a/tailbone/templates/trainwreck/transactions/rollover.mako +++ b/tailbone/templates/trainwreck/transactions/rollover.mako @@ -48,14 +48,9 @@ </b-table> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.engines = ${json.dumps(engines_data)|n} - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/trainwreck/transactions/view.mako b/tailbone/templates/trainwreck/transactions/view.mako index 02950941..630950cf 100644 --- a/tailbone/templates/trainwreck/transactions/view.mako +++ b/tailbone/templates/trainwreck/transactions/view.mako @@ -1,15 +1,11 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/view.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if custorder_xref_markers_data is not Undefined: ${form.vue_component}Data.custorderXrefMarkersData = ${json.dumps(custorder_xref_markers_data)|n} % endif - </script> </%def> - -${parent.body()} diff --git a/tailbone/templates/trainwreck/transactions/view_row.mako b/tailbone/templates/trainwreck/transactions/view_row.mako index 9c76f7bd..2507492e 100644 --- a/tailbone/templates/trainwreck/transactions/view_row.mako +++ b/tailbone/templates/trainwreck/transactions/view_row.mako @@ -1,16 +1,11 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/view_row.mako" /> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> % if discounts_data is not Undefined: ${form.vue_component}Data.discountsData = ${json.dumps(discounts_data)|n} % endif - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/units-of-measure/index.mako b/tailbone/templates/units-of-measure/index.mako index 597cabfd..4815fc79 100644 --- a/tailbone/templates/units-of-measure/index.mako +++ b/tailbone/templates/units-of-measure/index.mako @@ -51,20 +51,17 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.has_perm('collect_wild_uoms'): - <script type="text/javascript"> + <script> - TailboneGridData.showingCollectWildDialog = false + ${grid.vue_component}Data.showingCollectWildDialog = false - TailboneGrid.methods.collectFromWild = function() { - this.$refs['collect-wild-uoms-form'].submit() - } + ${grid.vue_component}.methods.collectFromWild = function() { + this.$refs['collect-wild-uoms-form'].submit() + } - </script> + </script> % endif </%def> - - -${parent.body()} diff --git a/tailbone/templates/upgrades/configure.mako b/tailbone/templates/upgrades/configure.mako index f7af685c..9439f830 100644 --- a/tailbone/templates/upgrades/configure.mako +++ b/tailbone/templates/upgrades/configure.mako @@ -111,9 +111,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.upgradeSystems = ${json.dumps(upgrade_systems)|n} ThisPageData.upgradeSystemShowDialog = false @@ -161,6 +161,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/upgrades/view.mako b/tailbone/templates/upgrades/view.mako index 6ae110e0..c3fca81d 100644 --- a/tailbone/templates/upgrades/view.mako +++ b/tailbone/templates/upgrades/view.mako @@ -137,11 +137,11 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> - TailboneFormData.showingPackages = 'diffs' + ${form.vue_component}Data.showingPackages = 'diffs' % if master.has_perm('execute'): @@ -153,7 +153,7 @@ // execute upgrade ////////////////////////////// - TailboneForm.props.upgradeExecuting = { + ${form.vue_component}.props.upgradeExecuting = { type: Boolean, default: false, } @@ -253,9 +253,9 @@ // execute upgrade ////////////////////////////// - TailboneFormData.formSubmitting = false + ${form.vue_component}Data.formSubmitting = false - TailboneForm.methods.submitForm = function() { + ${form.vue_component}.methods.submitForm = function() { this.formSubmitting = true } @@ -265,12 +265,12 @@ // declare failure ////////////////////////////// - TailboneForm.props.declareFailureSubmitting = { + ${form.vue_component}.props.declareFailureSubmitting = { type: Boolean, default: false, } - TailboneForm.methods.declareFailureClick = function() { + ${form.vue_component}.methods.declareFailureClick = function() { this.$emit('declare-failure-click') } @@ -287,6 +287,3 @@ </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/users/preferences.mako b/tailbone/templates/users/preferences.mako index c2e17396..ecfdd1c7 100644 --- a/tailbone/templates/users/preferences.mako +++ b/tailbone/templates/users/preferences.mako @@ -42,14 +42,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.themeStyleOptions = ${json.dumps(theme_style_options)|n} - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/users/view.mako b/tailbone/templates/users/view.mako index 06087927..d1afd218 100644 --- a/tailbone/templates/users/view.mako +++ b/tailbone/templates/users/view.mako @@ -76,10 +76,10 @@ % endif </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.has_perm('manage_api_tokens'): - <script type="text/javascript"> + <script> ${form.vue_component}.props.apiTokens = null @@ -134,6 +134,3 @@ </script> % endif </%def> - - -${parent.body()} diff --git a/tailbone/templates/vendors/configure.mako b/tailbone/templates/vendors/configure.mako index 79dad455..6b135346 100644 --- a/tailbone/templates/vendors/configure.mako +++ b/tailbone/templates/vendors/configure.mako @@ -44,14 +44,9 @@ </div> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> - +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.supportedVendorSettings = ${json.dumps(supported_vendor_settings)|n} - </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/views/model/create.mako b/tailbone/templates/views/model/create.mako index c5e22cfb..e902fd48 100644 --- a/tailbone/templates/views/model/create.mako +++ b/tailbone/templates/views/model/create.mako @@ -259,9 +259,9 @@ def includeme(config): </b-steps> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.activeStep = 'enter-details' @@ -334,6 +334,3 @@ def includeme(config): </script> </%def> - - -${parent.body()} diff --git a/tailbone/templates/workorders/view.mako b/tailbone/templates/workorders/view.mako index 8740b4c9..432e011d 100644 --- a/tailbone/templates/workorders/view.mako +++ b/tailbone/templates/workorders/view.mako @@ -145,9 +145,9 @@ </nav> </%def> -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - <script type="text/javascript"> +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> ThisPageData.receiveButtonDisabled = false ThisPageData.receiveButtonText = "I've received the order from customer" @@ -216,6 +216,3 @@ </script> </%def> - - -${parent.body()} From 59bd58aca768f9e18a1e3db7447a576c48d29191 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 20 Aug 2024 13:46:40 -0500 Subject: [PATCH 05/85] feat: add new 'waterpark' theme, based on wuttaweb w/ vue2 + buefy hoping to eventually replace the 'default' view with this one, if all goes well. definitely needs more testing and is not exposed as an option yet, unless configured --- tailbone/app.py | 3 +- tailbone/forms/core.py | 15 +- tailbone/grids/core.py | 14 +- tailbone/static/__init__.py | 5 +- tailbone/templates/appinfo/index.mako | 4 +- tailbone/templates/base.mako | 2 + tailbone/templates/batch/index.mako | 9 +- tailbone/templates/batch/view.mako | 20 +- tailbone/templates/form.mako | 5 +- tailbone/templates/themes/waterpark/base.mako | 486 ++++++++++++++++++ .../templates/themes/waterpark/configure.mako | 2 + tailbone/templates/themes/waterpark/form.mako | 2 + .../themes/waterpark/master/configure.mako | 2 + .../themes/waterpark/master/create.mako | 2 + .../themes/waterpark/master/delete.mako | 46 ++ .../themes/waterpark/master/edit.mako | 2 + .../themes/waterpark/master/form.mako | 2 + .../themes/waterpark/master/index.mako | 294 +++++++++++ .../themes/waterpark/master/view.mako | 2 + tailbone/templates/themes/waterpark/page.mako | 48 ++ tailbone/views/master.py | 12 +- tailbone/views/people.py | 2 +- tests/util.py | 2 +- 23 files changed, 937 insertions(+), 44 deletions(-) create mode 100644 tailbone/templates/themes/waterpark/base.mako create mode 100644 tailbone/templates/themes/waterpark/configure.mako create mode 100644 tailbone/templates/themes/waterpark/form.mako create mode 100644 tailbone/templates/themes/waterpark/master/configure.mako create mode 100644 tailbone/templates/themes/waterpark/master/create.mako create mode 100644 tailbone/templates/themes/waterpark/master/delete.mako create mode 100644 tailbone/templates/themes/waterpark/master/edit.mako create mode 100644 tailbone/templates/themes/waterpark/master/form.mako create mode 100644 tailbone/templates/themes/waterpark/master/index.mako create mode 100644 tailbone/templates/themes/waterpark/master/view.mako create mode 100644 tailbone/templates/themes/waterpark/page.mako diff --git a/tailbone/app.py b/tailbone/app.py index ad9663cf..b7262866 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -321,7 +321,8 @@ def main(global_config, **settings): """ This function returns a Pyramid WSGI application. """ - settings.setdefault('mako.directories', ['tailbone:templates']) + settings.setdefault('mako.directories', ['tailbone:templates', + 'wuttaweb:templates']) rattail_config = make_rattail_config(settings) pyramid_config = make_pyramid_config(settings) pyramid_config.include('tailbone') diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 2f1c9370..059b212a 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -905,7 +905,8 @@ class Form(object): def render_vue_template(self, template='/forms/deform.mako', **context): """ """ - return self.render_deform(template=template, **context) + output = self.render_deform(template=template, **context) + return HTML.literal(output) def render_deform(self, dform=None, template=None, **kwargs): if not template: @@ -1220,6 +1221,18 @@ class Form(object): # TODO: again, why does serialize() not return literal? return HTML.literal(field.serialize()) + # TODO: this was copied from wuttaweb; can remove when we align + # Form class structure + def render_vue_finalize(self): + """ """ + set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}" + make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})" + return HTML.tag('script', c=['\n', + HTML.literal(set_data), + '\n', + HTML.literal(make_component), + '\n']) + def render_field_readonly(self, field_name, **kwargs): """ Render the given field completely, but in read-only fashion. diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 6ec55987..eada1041 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -216,39 +216,39 @@ class Grid(WuttaGrid): expose_direct_link=False, **kwargs, ): - if kwargs.get('component'): + if 'component' in kwargs: warnings.warn("component param is deprecated for Grid(); " "please use vue_tagname param instead", DeprecationWarning, stacklevel=2) kwargs.setdefault('vue_tagname', kwargs.pop('component')) - if kwargs.get('default_sortkey'): + if 'default_sortkey' in kwargs: warnings.warn("default_sortkey param is deprecated for Grid(); " "please use sort_defaults param instead", DeprecationWarning, stacklevel=2) - if kwargs.get('default_sortdir'): + if 'default_sortdir' in kwargs: warnings.warn("default_sortdir param is deprecated for Grid(); " "please use sort_defaults param instead", DeprecationWarning, stacklevel=2) - if kwargs.get('default_sortkey') or kwargs.get('default_sortdir'): + if 'default_sortkey' in kwargs or 'default_sortdir' in kwargs: sortkey = kwargs.pop('default_sortkey', None) sortdir = kwargs.pop('default_sortdir', 'asc') if sortkey: kwargs.setdefault('sort_defaults', [(sortkey, sortdir)]) - if kwargs.get('pageable'): + if 'pageable' in kwargs: warnings.warn("pageable param is deprecated for Grid(); " "please use vue_tagname param instead", DeprecationWarning, stacklevel=2) kwargs.setdefault('paginated', kwargs.pop('pageable')) - if kwargs.get('default_pagesize'): + if 'default_pagesize' in kwargs: warnings.warn("default_pagesize param is deprecated for Grid(); " "please use pagesize param instead", DeprecationWarning, stacklevel=2) kwargs.setdefault('pagesize', kwargs.pop('default_pagesize')) - if kwargs.get('default_page'): + if 'default_page' in kwargs: warnings.warn("default_page param is deprecated for Grid(); " "please use page param instead", DeprecationWarning, stacklevel=2) diff --git a/tailbone/static/__init__.py b/tailbone/static/__init__.py index 2ad5161a..57700b80 100644 --- a/tailbone/static/__init__.py +++ b/tailbone/static/__init__.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2017 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -24,9 +24,8 @@ Static Assets """ -from __future__ import unicode_literals, absolute_import - def includeme(config): + config.include('wuttaweb.static') config.add_static_view('tailbone', 'tailbone:static') config.add_static_view('deform', 'deform:static') diff --git a/tailbone/templates/appinfo/index.mako b/tailbone/templates/appinfo/index.mako index 68244300..75032c1f 100644 --- a/tailbone/templates/appinfo/index.mako +++ b/tailbone/templates/appinfo/index.mako @@ -1,7 +1,7 @@ ## -*- coding: utf-8; -*- <%inherit file="/master/index.mako" /> -<%def name="render_grid_component()"> +<%def name="page_content()"> <div class="buttons"> @@ -108,7 +108,7 @@ <div class="panel-block"> <div style="width: 100%;"> - ${parent.render_grid_component()} + ${grid.render_vue_tag()} </div> </div> </${b}-collapse> diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index a0e58e22..eb950011 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -1,4 +1,5 @@ ## -*- coding: utf-8; -*- +<%namespace file="/wutta-components.mako" import="make_wutta_components" /> <%namespace file="/grids/nav.mako" import="grid_index_nav" /> <%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" /> <%namespace name="base_meta" file="/base_meta.mako" /> @@ -955,6 +956,7 @@ </%def> <%def name="make_vue_components()"> + ${make_wutta_components()} ${make_grid_filter_components()} ${page_help.make_component()} ${multi_file_upload.make_component()} diff --git a/tailbone/templates/batch/index.mako b/tailbone/templates/batch/index.mako index a1b11b89..bea10a97 100644 --- a/tailbone/templates/batch/index.mako +++ b/tailbone/templates/batch/index.mako @@ -43,7 +43,7 @@ <br /> <div class="form-wrapper"> <div class="form"> - <${execute_form.component} ref="executeResultsForm"></${execute_form.component}> + ${execute_form.render_vue_tag(ref='executeResultsForm')} </div> </div> </section> @@ -67,7 +67,7 @@ <%def name="render_vue_templates()"> ${parent.render_vue_templates()} % if master.results_executable and master.has_perm('execute_multiple'): - ${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n} + ${execute_form.render_vue_template(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)} % endif </%def> @@ -128,9 +128,6 @@ <%def name="make_vue_components()"> ${parent.make_vue_components()} % if master.results_executable and master.has_perm('execute_multiple'): - <script> - ${execute_form.vue_component}.data = function() { return ${execute_form.vue_component}Data } - Vue.component('${execute_form.vue_tagname}', ${execute_form.vue_component}) - </script> + ${execute_form.render_vue_finalize()} % endif </%def> diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako index cdfa9ba7..7c81ab0e 100644 --- a/tailbone/templates/batch/view.mako +++ b/tailbone/templates/batch/view.mako @@ -119,8 +119,7 @@ <div class="markdown"> ${execution_described|n} </div> - <${execute_form.component} ref="executeBatchForm"> - </${execute_form.component}> + ${execute_form.render_vue_tag(ref='executeBatchForm')} </section> <footer class="modal-card-foot"> @@ -168,8 +167,7 @@ Please be certain to use the right one! </p> <br /> - <${upload_worksheet_form.component} ref="uploadForm"> - </${upload_worksheet_form.component}> + ${upload_worksheet_form.render_vue_tag(ref='uploadForm')} </section> <footer class="modal-card-foot"> @@ -254,10 +252,10 @@ <%def name="render_vue_templates()"> ${parent.render_vue_templates()} % if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'): - ${upload_worksheet_form.render_deform(buttons=False, form_kwargs={'ref': 'actualUploadForm'})|n} + ${upload_worksheet_form.render_vue_template(buttons=False, form_kwargs={'ref': 'actualUploadForm'})} % endif % if master.handler.executable(batch) and master.has_perm('execute'): - ${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n} + ${execute_form.render_vue_template(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)} % endif </%def> @@ -345,15 +343,9 @@ <%def name="make_vue_components()"> ${parent.make_vue_components()} % if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'): - <script> - ${upload_worksheet_form.vue_component}.data = function() { return ${upload_worksheet_form.vue_component}Data } - Vue.component('${upload_worksheet_form.component}', ${upload_worksheet_form.vue_component}) - </script> + ${upload_worksheet_form.render_vue_finalize()} % endif % if execute_enabled and master.has_perm('execute'): - <script> - ${execute_form.vue_component}.data = function() { return ${execute_form.vue_component}Data } - Vue.component('${execute_form.component}', ${execute_form.vue_component}) - </script> + ${execute_form.render_vue_finalize()} % endif </%def> diff --git a/tailbone/templates/form.mako b/tailbone/templates/form.mako index 3bb04257..e3a4d5dc 100644 --- a/tailbone/templates/form.mako +++ b/tailbone/templates/form.mako @@ -109,9 +109,6 @@ <%def name="make_vue_components()"> ${parent.make_vue_components()} % if form is not Undefined: - <script> - ${form.vue_component}.data = function() { return ${form.vue_component}Data } - Vue.component('${form.vue_tagname}', ${form.vue_component}) - </script> + ${form.render_vue_finalize()} % endif </%def> diff --git a/tailbone/templates/themes/waterpark/base.mako b/tailbone/templates/themes/waterpark/base.mako new file mode 100644 index 00000000..15184f6e --- /dev/null +++ b/tailbone/templates/themes/waterpark/base.mako @@ -0,0 +1,486 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/base.mako" /> +<%namespace name="base_meta" file="/base_meta.mako" /> +<%namespace file="/formposter.mako" import="declare_formposter_mixin" /> +<%namespace file="/grids/filter-components.mako" import="make_grid_filter_components" /> +<%namespace name="page_help" file="/page_help.mako" /> + +<%def name="base_styles()"> + ${parent.base_styles()} + <style> + + .filters .filter-fieldname .field, + .filters .filter-fieldname .field label { + width: 100%; + } + + .filters .filter-fieldname, + .filters .filter-fieldname .field label, + .filters .filter-fieldname .button { + justify-content: left; + } + + .filters .filter-verb .select, + .filters .filter-verb .select select { + width: 100%; + } + + % if filter_fieldname_width is not Undefined: + + .filters .filter-fieldname, + .filters .filter-fieldname .button { + min-width: ${filter_fieldname_width}; + } + + .filters .filter-verb { + min-width: ${filter_verb_width}; + } + + % endif + + </style> +</%def> + +<%def name="before_content()"> + ## TODO: this must come before the self.body() call..but why? + ${declare_formposter_mixin()} +</%def> + +<%def name="render_navbar_brand()"> + <div class="navbar-brand"> + <a class="navbar-item" href="${url('home')}" + v-show="!menuSearchActive"> + ${base_meta.header_logo()} + <div id="global-header-title"> + ${base_meta.global_title()} + </div> + </a> + <div v-show="menuSearchActive" + class="navbar-item"> + <b-autocomplete ref="menuSearchAutocomplete" + v-model="menuSearchTerm" + :data="menuSearchFilteredData" + field="label" + open-on-focus + keep-first + icon-pack="fas" + clearable + @keydown.native="menuSearchKeydown" + @select="menuSearchSelect"> + </b-autocomplete> + </div> + <a role="button" class="navbar-burger" data-target="navbar-menu" aria-label="menu" aria-expanded="false"> + <span aria-hidden="true"></span> + <span aria-hidden="true"></span> + <span aria-hidden="true"></span> + </a> + </div> +</%def> + +<%def name="render_navbar_start()"> + <div class="navbar-start"> + + <div v-if="menuSearchData.length" + class="navbar-item"> + <b-button type="is-primary" + size="is-small" + @click="menuSearchInit()"> + <span><i class="fa fa-search"></i></span> + </b-button> + </div> + + % for topitem in menus: + % if topitem['is_link']: + ${h.link_to(topitem['title'], topitem['url'], target=topitem['target'], class_='navbar-item')} + % else: + <div class="navbar-item has-dropdown is-hoverable"> + <a class="navbar-link">${topitem['title']}</a> + <div class="navbar-dropdown"> + % for item in topitem['items']: + % if item['is_menu']: + <% item_hash = id(item) %> + <% toggle = 'menu_{}_shown'.format(item_hash) %> + <div> + <a class="navbar-link" @click.prevent="toggleNestedMenu('${item_hash}')"> + ${item['title']} + </a> + </div> + % for subitem in item['items']: + % if subitem['is_sep']: + <hr class="navbar-divider" v-show="${toggle}"> + % else: + ${h.link_to("{}".format(subitem['title']), subitem['url'], class_='navbar-item nested', target=subitem['target'], **{'v-show': toggle})} + % endif + % endfor + % else: + % if item['is_sep']: + <hr class="navbar-divider"> + % else: + ${h.link_to(item['title'], item['url'], class_='navbar-item', target=item['target'])} + % endif + % endif + % endfor + </div> + </div> + % endif + % endfor + + </div> +</%def> + +<%def name="render_theme_picker()"> + % if expose_theme_picker and request.has_perm('common.change_app_theme'): + <div class="level-item"> + ${h.form(url('change_theme'), method="post", ref='themePickerForm')} + ${h.csrf_token(request)} + <input type="hidden" name="referrer" :value="referrer" /> + <div style="display: flex; align-items: center; gap: 0.5rem;"> + <span>Theme:</span> + <b-select name="theme" + v-model="globalTheme" + @input="changeTheme()"> + % for option in theme_picker_options: + <option value="${option.value}"> + ${option.label} + </option> + % endfor + </b-select> + </div> + ${h.end_form()} + </div> + % endif +</%def> + +<%def name="render_feedback_button()"> + + <div class="level-item"> + <page-help + % if can_edit_help: + @configure-fields-help="configureFieldsHelp = true" + % endif + /> + </div> + + % if request.has_perm('common.feedback'): + <feedback-form + action="${url('feedback')}" + :message="feedbackMessage"> + </feedback-form> + % endif +</%def> + +<%def name="render_this_page_component()"> + <this-page @change-content-title="changeContentTitle" + % if can_edit_help: + :configure-fields-help="configureFieldsHelp" + % endif + /> +</%def> + +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + + ${page_help.render_template()} + ${page_help.declare_vars()} + + % if request.has_perm('common.feedback'): + <script type="text/x-template" id="feedback-template"> + <div> + + <div class="level-item"> + <b-button type="is-primary" + @click="showFeedback()" + icon-pack="fas" + icon-left="comment"> + Feedback + </b-button> + </div> + + <b-modal has-modal-card + :active.sync="showDialog"> + <div class="modal-card"> + + <header class="modal-card-head"> + <p class="modal-card-title">User Feedback</p> + </header> + + <section class="modal-card-body"> + <p class="block"> + Questions, suggestions, comments, complaints, etc. + <span class="red">regarding this website</span> are + welcome and may be submitted below. + </p> + + <b-field label="User Name"> + <b-input v-model="userName" + % if request.user: + disabled + % endif + > + </b-input> + </b-field> + + <b-field label="Referring URL"> + <b-input + v-model="referrer" + disabled="true"> + </b-input> + </b-field> + + <b-field label="Message"> + <b-input type="textarea" + v-model="message" + ref="textarea"> + </b-input> + </b-field> + + % if config.get_bool('tailbone.feedback_allows_reply'): + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <b-checkbox v-model="pleaseReply" + @input="pleaseReplyChanged"> + Please email me back{{ pleaseReply ? " at: " : "" }} + </b-checkbox> + </div> + <div class="level-item" v-show="pleaseReply"> + <b-input v-model="userEmail" + ref="userEmail"> + </b-input> + </div> + </div> + </div> + % endif + + </section> + + <footer class="modal-card-foot"> + <b-button @click="showDialog = false"> + Cancel + </b-button> + <b-button type="is-primary" + icon-pack="fas" + icon-left="paper-plane" + @click="sendFeedback()" + :disabled="sendingFeedback || !message.trim()"> + {{ sendingFeedback ? "Working, please wait..." : "Send Message" }} + </b-button> + </footer> + </div> + </b-modal> + + </div> + </script> + <script> + + const FeedbackForm = { + template: '#feedback-template', + mixins: [SimpleRequestMixin], + props: [ + 'action', + 'message', + ], + methods: { + + showFeedback() { + this.referrer = location.href + this.showDialog = true + this.$nextTick(function() { + this.$refs.textarea.focus() + }) + }, + + % if config.get_bool('tailbone.feedback_allows_reply'): + pleaseReplyChanged(value) { + this.$nextTick(() => { + this.$refs.userEmail.focus() + }) + }, + % endif + + sendFeedback() { + this.sendingFeedback = true + + const params = { + referrer: this.referrer, + user: this.userUUID, + user_name: this.userName, + % if config.get_bool('tailbone.feedback_allows_reply'): + please_reply_to: this.pleaseReply ? this.userEmail : null, + % endif + message: this.message.trim(), + } + + this.simplePOST(this.action, params, response => { + + this.$buefy.toast.open({ + message: "Message sent! Thank you for your feedback.", + type: 'is-info', + duration: 4000, // 4 seconds + }) + + this.showDialog = false + // clear out message, in case they need to send another + this.message = "" + this.sendingFeedback = false + + }, response => { // failure + this.sendingFeedback = false + }) + }, + } + } + + const FeedbackFormData = { + referrer: null, + userUUID: null, + userName: null, + userEmail: null, + % if config.get_bool('tailbone.feedback_allows_reply'): + pleaseReply: false, + % endif + showDialog: false, + sendingFeedback: false, + } + + </script> + % endif +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> + + ############################## + ## menu search + ############################## + + WholePageData.menuSearchActive = false + WholePageData.menuSearchTerm = '' + WholePageData.menuSearchData = ${json.dumps(global_search_data or [])|n} + + WholePage.computed.menuSearchFilteredData = function() { + if (!this.menuSearchTerm.length) { + return this.menuSearchData + } + + const terms = [] + for (let term of this.menuSearchTerm.toLowerCase().split(' ')) { + term = term.trim() + if (term) { + terms.push(term) + } + } + if (!terms.length) { + return this.menuSearchData + } + + // all terms must match + return this.menuSearchData.filter((option) => { + const label = option.label.toLowerCase() + for (const term of terms) { + if (label.indexOf(term) < 0) { + return false + } + } + return true + }) + } + + WholePage.methods.globalKey = function(event) { + + // Ctrl+8 opens menu search + if (event.target.tagName == 'BODY') { + if (event.ctrlKey && event.key == '8') { + this.menuSearchInit() + } + } + } + + WholePage.mounted = function() { + window.addEventListener('keydown', this.globalKey) + for (let hook of this.mountedHooks) { + hook(this) + } + } + + WholePage.beforeDestroy = function() { + window.removeEventListener('keydown', this.globalKey) + } + + WholePage.methods.menuSearchInit = function() { + this.menuSearchTerm = '' + this.menuSearchActive = true + this.$nextTick(() => { + this.$refs.menuSearchAutocomplete.focus() + }) + } + + WholePage.methods.menuSearchKeydown = function(event) { + + // ESC will dismiss searchbox + if (event.which == 27) { + this.menuSearchActive = false + } + } + + WholePage.methods.menuSearchSelect = function(option) { + location.href = option.url + } + + ############################## + ## theme picker + ############################## + + % if expose_theme_picker and request.has_perm('common.change_app_theme'): + + WholePageData.globalTheme = ${json.dumps(theme or None)|n} + ## WholePageData.referrer = location.href + + WholePage.methods.changeTheme = function() { + this.$refs.themePickerForm.submit() + } + + % endif + + ############################## + ## feedback + ############################## + + % if request.has_perm('common.feedback'): + + WholePageData.feedbackMessage = "" + + % if request.user: + FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n} + FeedbackFormData.userName = ${json.dumps(str(request.user))|n} + % endif + + % endif + + ############################## + ## edit fields help + ############################## + + % if can_edit_help: + WholePageData.configureFieldsHelp = false + % endif + + </script> +</%def> + +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.datepicker.js') + f'?ver={tailbone.__version__}')} + ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.numericinput.js') + f'?ver={tailbone.__version__}')} + ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.oncebutton.js') + f'?ver={tailbone.__version__}')} + ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + f'?ver={tailbone.__version__}')} + ${make_grid_filter_components()} + ${page_help.make_component()} + % if request.has_perm('common.feedback'): + <script> + FeedbackForm.data = function() { return FeedbackFormData } + Vue.component('feedback-form', FeedbackForm) + </script> + % endif +</%def> diff --git a/tailbone/templates/themes/waterpark/configure.mako b/tailbone/templates/themes/waterpark/configure.mako new file mode 100644 index 00000000..9ac9a5cd --- /dev/null +++ b/tailbone/templates/themes/waterpark/configure.mako @@ -0,0 +1,2 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/configure.mako" /> diff --git a/tailbone/templates/themes/waterpark/form.mako b/tailbone/templates/themes/waterpark/form.mako new file mode 100644 index 00000000..cf1ddb8a --- /dev/null +++ b/tailbone/templates/themes/waterpark/form.mako @@ -0,0 +1,2 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/form.mako" /> diff --git a/tailbone/templates/themes/waterpark/master/configure.mako b/tailbone/templates/themes/waterpark/master/configure.mako new file mode 100644 index 00000000..51da5b0a --- /dev/null +++ b/tailbone/templates/themes/waterpark/master/configure.mako @@ -0,0 +1,2 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/master/configure.mako" /> diff --git a/tailbone/templates/themes/waterpark/master/create.mako b/tailbone/templates/themes/waterpark/master/create.mako new file mode 100644 index 00000000..23399b9e --- /dev/null +++ b/tailbone/templates/themes/waterpark/master/create.mako @@ -0,0 +1,2 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/master/create.mako" /> diff --git a/tailbone/templates/themes/waterpark/master/delete.mako b/tailbone/templates/themes/waterpark/master/delete.mako new file mode 100644 index 00000000..a15dfaf8 --- /dev/null +++ b/tailbone/templates/themes/waterpark/master/delete.mako @@ -0,0 +1,46 @@ +## -*- coding: utf-8; -*- +<%inherit file="tailbone:templates/form.mako" /> + +<%def name="title()">Delete ${model_title}: ${instance_title}</%def> + +<%def name="render_form()"> + <br /> + <b-notification type="is-danger" :closable="false"> + You are about to delete the following ${model_title} and all associated data: + </b-notification> + ${parent.render_form()} +</%def> + +<%def name="render_form_buttons()"> + <br /> + <b-notification type="is-danger" :closable="false"> + Are you sure about this? + </b-notification> + <br /> + + ${h.form(request.current_route_url(), **{'@submit': 'submitForm'})} + ${h.csrf_token(request)} + <div class="buttons"> + <wutta-button once tag="a" href="${form.cancel_url}" + label="Whoops, nevermind..." /> + <b-button type="is-primary is-danger" + native-type="submit" + :disabled="formSubmitting"> + {{ formSubmitting ? "Working, please wait..." : "${form.button_label_submit}" }} + </b-button> + </div> + ${h.end_form()} +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> + + ${form.vue_component}Data.formSubmitting = false + + ${form.vue_component}.methods.submitForm = function() { + this.formSubmitting = true + } + + </script> +</%def> diff --git a/tailbone/templates/themes/waterpark/master/edit.mako b/tailbone/templates/themes/waterpark/master/edit.mako new file mode 100644 index 00000000..18a2fa2f --- /dev/null +++ b/tailbone/templates/themes/waterpark/master/edit.mako @@ -0,0 +1,2 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/master/edit.mako" /> diff --git a/tailbone/templates/themes/waterpark/master/form.mako b/tailbone/templates/themes/waterpark/master/form.mako new file mode 100644 index 00000000..db56843b --- /dev/null +++ b/tailbone/templates/themes/waterpark/master/form.mako @@ -0,0 +1,2 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/master/form.mako" /> diff --git a/tailbone/templates/themes/waterpark/master/index.mako b/tailbone/templates/themes/waterpark/master/index.mako new file mode 100644 index 00000000..e3b5b42d --- /dev/null +++ b/tailbone/templates/themes/waterpark/master/index.mako @@ -0,0 +1,294 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/master/index.mako" /> + +<%def name="grid_tools()"> + + ## grid totals + % if getattr(master, 'supports_grid_totals', False): + <div style="display: flex; align-items: center;"> + <b-button v-if="gridTotalsDisplay == null" + :disabled="gridTotalsFetching" + @click="gridTotalsFetch()"> + {{ gridTotalsFetching ? "Working, please wait..." : "Show Totals" }} + </b-button> + <div v-if="gridTotalsDisplay != null" + class="control"> + Totals: {{ gridTotalsDisplay }} + </div> + </div> + % endif + + ## download search results + % if getattr(master, 'results_downloadable', False) and master.has_perm('download_results'): + <div> + <b-button type="is-primary" + icon-pack="fas" + icon-left="download" + @click="showDownloadResultsDialog = true" + :disabled="!total"> + Download Results + </b-button> + + ${h.form(url('{}.download_results'.format(route_prefix)), ref='download_results_form')} + ${h.csrf_token(request)} + <input type="hidden" name="fmt" :value="downloadResultsFormat" /> + <input type="hidden" name="fields" :value="downloadResultsFieldsIncluded" /> + ${h.end_form()} + + <b-modal :active.sync="showDownloadResultsDialog"> + <div class="card"> + + <div class="card-content"> + <p> + There are + <span class="is-size-4 has-text-weight-bold"> + {{ total.toLocaleString('en') }} ${model_title_plural} + </span> + matching your current filters. + </p> + <p> + You may download this set as a single data file if you like. + </p> + <br /> + + <b-notification type="is-warning" :closable="false" + v-if="downloadResultsFormat == 'xlsx' && total >= 1000"> + Excel downloads for large data sets can take a long time to + generate, and bog down the server in the meantime. You are + encouraged to choose CSV for a large data set, even though + the end result (file size) may be larger with CSV. + </b-notification> + + <div style="display: flex; justify-content: space-between"> + + <div> + <b-field label="Format"> + <b-select v-model="downloadResultsFormat"> + % for key, label in master.download_results_supported_formats().items(): + <option value="${key}">${label}</option> + % endfor + </b-select> + </b-field> + </div> + + <div> + + <div v-show="downloadResultsFieldsMode != 'choose'" + class="has-text-right"> + <p v-if="downloadResultsFieldsMode == 'default'"> + Will use DEFAULT fields. + </p> + <p v-if="downloadResultsFieldsMode == 'all'"> + Will use ALL fields. + </p> + <br /> + </div> + + <div class="buttons is-right"> + <b-button type="is-primary" + v-show="downloadResultsFieldsMode != 'default'" + @click="downloadResultsUseDefaultFields()"> + Use Default Fields + </b-button> + <b-button type="is-primary" + v-show="downloadResultsFieldsMode != 'all'" + @click="downloadResultsUseAllFields()"> + Use All Fields + </b-button> + <b-button type="is-primary" + v-show="downloadResultsFieldsMode != 'choose'" + @click="downloadResultsFieldsMode = 'choose'"> + Choose Fields + </b-button> + </div> + + <div v-show="downloadResultsFieldsMode == 'choose'"> + <div style="display: flex;"> + <div> + <b-field label="Excluded Fields"> + <b-select multiple native-size="8" + expanded + v-model="downloadResultsExcludedFieldsSelected" + ref="downloadResultsExcludedFields"> + <option v-for="field in downloadResultsFieldsExcluded" + :key="field" + :value="field"> + {{ field }} + </option> + </b-select> + </b-field> + </div> + <div> + <br /><br /> + <b-button style="margin: 0.5rem;" + @click="downloadResultsExcludeFields()"> + < + </b-button> + <br /> + <b-button style="margin: 0.5rem;" + @click="downloadResultsIncludeFields()"> + > + </b-button> + </div> + <div> + <b-field label="Included Fields"> + <b-select multiple native-size="8" + expanded + v-model="downloadResultsIncludedFieldsSelected" + ref="downloadResultsIncludedFields"> + <option v-for="field in downloadResultsFieldsIncluded" + :key="field" + :value="field"> + {{ field }} + </option> + </b-select> + </b-field> + </div> + </div> + </div> + + </div> + </div> + </div> <!-- card-content --> + + <footer class="modal-card-foot"> + <b-button @click="showDownloadResultsDialog = false"> + Cancel + </b-button> + <once-button type="is-primary" + @click="downloadResultsSubmit()" + icon-pack="fas" + icon-left="download" + :disabled="!downloadResultsFieldsIncluded.length" + text="Download Results"> + </once-button> + </footer> + </div> + </b-modal> + </div> + % endif + + ## download rows for search results + % if getattr(master, 'has_rows', False) and master.results_rows_downloadable and master.has_perm('download_results_rows'): + <b-button type="is-primary" + icon-pack="fas" + icon-left="download" + @click="downloadResultsRows()" + :disabled="downloadResultsRowsButtonDisabled"> + {{ downloadResultsRowsButtonText }} + </b-button> + ${h.form(url('{}.download_results_rows'.format(route_prefix)), ref='downloadResultsRowsForm')} + ${h.csrf_token(request)} + ${h.end_form()} + % endif + + ## merge 2 objects + % if getattr(master, 'mergeable', False) and request.has_perm('{}.merge'.format(permission_prefix)): + + ${h.form(url('{}.merge'.format(route_prefix)), class_='control', **{'@submit': 'submitMergeForm'})} + ${h.csrf_token(request)} + <input type="hidden" + name="uuids" + :value="checkedRowUUIDs()" /> + <b-button type="is-primary" + native-type="submit" + icon-pack="fas" + icon-left="object-ungroup" + :disabled="mergeFormSubmitting || checkedRows.length != 2"> + {{ mergeFormButtonText }} + </b-button> + ${h.end_form()} + % endif + + ## enable / disable selected objects + % if getattr(master, 'supports_set_enabled_toggle', False) and master.has_perm('enable_disable_set'): + + ${h.form(url('{}.enable_set'.format(route_prefix)), class_='control', ref='enable_selected_form')} + ${h.csrf_token(request)} + ${h.hidden('uuids', v_model='selected_uuids')} + <b-button :disabled="enableSelectedDisabled" + @click="enableSelectedSubmit()"> + {{ enableSelectedText }} + </b-button> + ${h.end_form()} + + ${h.form(url('{}.disable_set'.format(route_prefix)), ref='disable_selected_form', class_='control')} + ${h.csrf_token(request)} + ${h.hidden('uuids', v_model='selected_uuids')} + <b-button :disabled="disableSelectedDisabled" + @click="disableSelectedSubmit()"> + {{ disableSelectedText }} + </b-button> + ${h.end_form()} + % endif + + ## delete selected objects + % if getattr(master, 'set_deletable', False) and master.has_perm('delete_set'): + ${h.form(url('{}.delete_set'.format(route_prefix)), ref='delete_selected_form', class_='control')} + ${h.csrf_token(request)} + ${h.hidden('uuids', v_model='selected_uuids')} + <b-button type="is-danger" + :disabled="deleteSelectedDisabled" + @click="deleteSelectedSubmit()" + icon-pack="fas" + icon-left="trash"> + {{ deleteSelectedText }} + </b-button> + ${h.end_form()} + % endif + + ## delete search results + % if getattr(master, 'bulk_deletable', False) and request.has_perm('{}.bulk_delete'.format(permission_prefix)): + ${h.form(url('{}.bulk_delete'.format(route_prefix)), ref='delete_results_form', class_='control')} + ${h.csrf_token(request)} + <b-button type="is-danger" + :disabled="deleteResultsDisabled" + :title="total ? null : 'There are no results to delete'" + @click="deleteResultsSubmit()" + icon-pack="fas" + icon-left="trash"> + {{ deleteResultsText }} + </b-button> + ${h.end_form()} + % endif + +</%def> + +<%def name="render_vue_template_grid()"> + ${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())} +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> + + % if getattr(master, 'bulk_deletable', False) and master.has_perm('bulk_delete'): + + ${grid.vue_component}Data.deleteResultsSubmitting = false + ${grid.vue_component}Data.deleteResultsText = "Delete Results" + + ${grid.vue_component}.computed.deleteResultsDisabled = function() { + if (this.deleteResultsSubmitting) { + return true + } + if (!this.total) { + return true + } + return false + } + + ${grid.vue_component}.methods.deleteResultsSubmit = function() { + // TODO: show "plural model title" here? + if (!confirm("You are about to delete " + this.total.toLocaleString('en') + " objects.\n\nAre you sure?")) { + return + } + + this.deleteResultsSubmitting = true + this.deleteResultsText = "Working, please wait..." + this.$refs.delete_results_form.submit() + } + + % endif + + </script> +</%def> diff --git a/tailbone/templates/themes/waterpark/master/view.mako b/tailbone/templates/themes/waterpark/master/view.mako new file mode 100644 index 00000000..99194469 --- /dev/null +++ b/tailbone/templates/themes/waterpark/master/view.mako @@ -0,0 +1,2 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/master/view.mako" /> diff --git a/tailbone/templates/themes/waterpark/page.mako b/tailbone/templates/themes/waterpark/page.mako new file mode 100644 index 00000000..7e6851a7 --- /dev/null +++ b/tailbone/templates/themes/waterpark/page.mako @@ -0,0 +1,48 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/page.mako" /> + +<%def name="render_vue_template_this_page()"> + <script type="text/x-template" id="this-page-template"> + <div style="height: 100%;"> + ## DEPRECATED; called for back-compat + ${self.render_this_page()} + </div> + </script> +</%def> + +## DEPRECATED; remains for back-compat +<%def name="render_this_page()"> + <div style="display: flex;"> + + <div class="this-page-content" style="flex-grow: 1;"> + ${self.page_content()} + </div> + + ## DEPRECATED; remains for back-compat + <ul id="context-menu"> + ${self.context_menu_items()} + </ul> + </div> +</%def> + +## DEPRECATED; remains for back-compat +<%def name="context_menu_items()"> + % if context_menu_list_items is not Undefined: + % for item in context_menu_list_items: + <li>${item}</li> + % endfor + % endif +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> + + ThisPageData.csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n} + + % if can_edit_help: + ThisPage.props.configureFieldsHelp = Boolean + % endif + + </script> +</%def> diff --git a/tailbone/views/master.py b/tailbone/views/master.py index ac74a070..a8365482 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -137,6 +137,7 @@ class MasterView(View): deleting = False executing = False cloning = False + configuring = False has_pk_fields = False has_image = False has_thumbnail = False @@ -350,6 +351,7 @@ class MasterView(View): return self.json_response(context) context = { + 'index_url': None, # nb. avoid title link since this *is* the index 'grid': grid, } @@ -380,7 +382,7 @@ class MasterView(View): grid contents etc. """ - def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): + def make_grid(self, factory=None, key=None, data=None, columns=None, session=None, **kwargs): """ Creates a new grid instance """ @@ -389,7 +391,7 @@ class MasterView(View): if key is None: key = self.get_grid_key() if data is None: - data = self.get_data(session=kwargs.get('session')) + data = self.get_data(session=session) if columns is None: columns = self.get_grid_columns() @@ -407,7 +409,7 @@ class MasterView(View): """ if session is None: session = self.Session() - kwargs.setdefault('pageable', False) + kwargs.setdefault('paginated', False) grid = self.make_grid(session=session, **kwargs) return grid.make_visible_data() @@ -1701,7 +1703,7 @@ class MasterView(View): """ if session is None: session = self.Session() - kwargs.setdefault('pageable', False) + kwargs.setdefault('paginated', False) kwargs.setdefault('sortable', sort) grid = self.make_row_grid(session=session, **kwargs) return grid.make_visible_data() @@ -1879,6 +1881,7 @@ class MasterView(View): return self.redirect(self.get_action_url('view', instance)) form = self.make_form(instance) + form.save_label = "DELETE Forever" # TODO: Add better validation, ideally CSRF etc. if self.request.method == 'POST': @@ -5119,6 +5122,7 @@ class MasterView(View): """ Generic view for configuring some aspect of the software. """ + self.configuring = True app = self.get_rattail_app() if self.request.method == 'POST': if self.request.POST.get('remove_settings'): diff --git a/tailbone/views/people.py b/tailbone/views/people.py index 020babc5..b6a4c0b9 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -543,7 +543,7 @@ class PersonView(MasterView): }, filterable=True, sortable=True, - pageable=True, + paginated=True, default_sortkey='end_time', default_sortdir='desc', component='transactions-grid', diff --git a/tests/util.py b/tests/util.py index 3aa04f5e..4277a7c3 100644 --- a/tests/util.py +++ b/tests/util.py @@ -24,7 +24,7 @@ class WebTestCase(DataTestCase): self.pyramid_config = testing.setUp(request=self.request, settings={ 'wutta_config': self.config, 'rattail_config': self.config, - 'mako.directories': ['tailbone:templates'], + 'mako.directories': ['tailbone:templates', 'wuttaweb:templates'], # 'pyramid_deform.template_search_path': 'wuttaweb:templates/deform', }) From 83586ef90fd3c8acae6eda85bd7d44a5992464f5 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 20 Aug 2024 15:06:09 -0500 Subject: [PATCH 06/85] =?UTF-8?q?bump:=20version=200.19.3=20=E2=86=92=200.?= =?UTF-8?q?20.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ pyproject.toml | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8017445..5840f59f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.20.0 (2024-08-20) + +### Feat + +- add new 'waterpark' theme, based on wuttaweb w/ vue2 + buefy +- refactor templates to simplify base/page/form structure + +### Fix + +- avoid deprecated reference to app db engine + ## v0.19.3 (2024-08-19) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 3e07abaa..150544ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.19.3" +version = "0.20.0" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -59,7 +59,7 @@ dependencies = [ "transaction", "waitress", "WebHelpers2", - "WuttaWeb>=0.10.2", + "WuttaWeb>=0.11.0", "zope.sqlalchemy>=1.5", ] From 21f90f3f32f76d509b75348388445cc1a6dccd85 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 20 Aug 2024 16:02:35 -0500 Subject: [PATCH 07/85] fix: fix default filter verbs logic for workorder status --- tailbone/views/workorders.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tailbone/views/workorders.py b/tailbone/views/workorders.py index a53037bc..d8094e4b 100644 --- a/tailbone/views/workorders.py +++ b/tailbone/views/workorders.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -83,12 +83,12 @@ class WorkOrderView(MasterView): ] def __init__(self, request): - super(WorkOrderView, self).__init__(request) + super().__init__(request) app = self.get_rattail_app() self.workorder_handler = app.get_workorder_handler() def configure_grid(self, g): - super(WorkOrderView, self).configure_grid(g) + super().configure_grid(g) model = self.model # customer @@ -113,7 +113,7 @@ class WorkOrderView(MasterView): return 'warning' def configure_form(self, f): - super(WorkOrderView, self).configure_form(f) + super().configure_form(f) model = self.model SelectWidget = forms.widgets.JQuerySelectWidget @@ -208,7 +208,7 @@ class WorkOrderView(MasterView): return event.workorder def configure_row_grid(self, g): - super(WorkOrderView, self).configure_row_grid(g) + super().configure_row_grid(g) g.set_enum('type_code', self.enum.WORKORDER_EVENT) g.set_sort_defaults('occurred') @@ -353,7 +353,7 @@ class WorkOrderView(MasterView): class StatusFilter(grids.filters.AlchemyIntegerFilter): def __init__(self, *args, **kwargs): - super(StatusFilter, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) from drild import enum @@ -369,14 +369,14 @@ class StatusFilter(grids.filters.AlchemyIntegerFilter): @property def verb_labels(self): - labels = dict(super(StatusFilter, self).verb_labels) + labels = dict(super().verb_labels) labels['is_active'] = "Is Active" labels['not_active'] = "Is Not Active" return labels @property def valueless_verbs(self): - verbs = list(super(StatusFilter, self).valueless_verbs) + verbs = list(super().valueless_verbs) verbs.extend([ 'is_active', 'not_active', @@ -385,7 +385,11 @@ class StatusFilter(grids.filters.AlchemyIntegerFilter): @property def default_verbs(self): - verbs = list(super(StatusFilter, self).default_verbs) + verbs = super().default_verbs + if callable(verbs): + verbs = verbs() + + verbs = list(verbs or []) verbs.insert(0, 'is_active') verbs.insert(1, 'not_active') return verbs From 526c84dfa62cc88d2cd4ec28861e6caef70205e4 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 20 Aug 2024 16:05:52 -0500 Subject: [PATCH 08/85] =?UTF-8?q?bump:=20version=200.20.0=20=E2=86=92=200.?= =?UTF-8?q?20.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5840f59f..4e2b348a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.20.1 (2024-08-20) + +### Fix + +- fix default filter verbs logic for workorder status + ## v0.20.0 (2024-08-20) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 150544ba..90ecd953 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.20.0" +version = "0.20.1" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From c8dc60cb68c72530b04df13fdc012a3ba382ba01 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 20 Aug 2024 16:37:58 -0500 Subject: [PATCH 09/85] fix: fix spacing for navbar logo/title in waterpark theme --- tailbone/templates/themes/waterpark/base.mako | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tailbone/templates/themes/waterpark/base.mako b/tailbone/templates/themes/waterpark/base.mako index 15184f6e..878090dc 100644 --- a/tailbone/templates/themes/waterpark/base.mako +++ b/tailbone/templates/themes/waterpark/base.mako @@ -50,9 +50,11 @@ <div class="navbar-brand"> <a class="navbar-item" href="${url('home')}" v-show="!menuSearchActive"> - ${base_meta.header_logo()} - <div id="global-header-title"> - ${base_meta.global_title()} + <div style="display: flex; align-items: center;"> + ${base_meta.header_logo()} + <div id="navbar-brand-title"> + ${base_meta.global_title()} + </div> </div> </a> <div v-show="menuSearchActive" From 07871188aa323331a4464c80021b4f25057dd54d Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 20 Aug 2024 17:03:57 -0500 Subject: [PATCH 10/85] fix: fix master/index template rendering for waterpark theme --- tailbone/templates/themes/waterpark/master/index.mako | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tailbone/templates/themes/waterpark/master/index.mako b/tailbone/templates/themes/waterpark/master/index.mako index e3b5b42d..e6702599 100644 --- a/tailbone/templates/themes/waterpark/master/index.mako +++ b/tailbone/templates/themes/waterpark/master/index.mako @@ -254,6 +254,11 @@ </%def> +## DEPRECATED; remains for back-compat +<%def name="render_this_page()"> + ${self.page_content()} +</%def> + <%def name="render_vue_template_grid()"> ${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())} </%def> From 1def26a35bc36b399ff6783198a4687af206482e Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 20 Aug 2024 19:09:56 -0500 Subject: [PATCH 11/85] feat: add "has output file templates" config option for master view this is a bit hacky, a quick copy/paste job from the equivalent feature for input file templates. i assume this will get cleaned up when moved to wuttaweb.. --- tailbone/templates/configure.mako | 107 +++++++++- .../templates/themes/waterpark/configure.mako | 76 +++++++ tailbone/views/master.py | 202 +++++++++++++++++- 3 files changed, 381 insertions(+), 4 deletions(-) diff --git a/tailbone/templates/configure.mako b/tailbone/templates/configure.mako index 272aadce..6d9c2261 100644 --- a/tailbone/templates/configure.mako +++ b/tailbone/templates/configure.mako @@ -143,6 +143,68 @@ </div> </%def> +<%def name="output_file_template_field(key)"> + <% tmpl = output_file_templates[key] %> + <b-field grouped> + + <b-field label="${tmpl['label']}"> + <b-select name="${tmpl['setting_mode']}" + v-model="outputFileTemplateSettings['${tmpl['setting_mode']}']" + @input="settingsNeedSaved = true"> + <option value="default">use default</option> + <option value="hosted">use uploaded file</option> + </b-select> + </b-field> + + <b-field label="File" + v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted'" + :message="outputFileTemplateSettings['${tmpl['setting_file']}'] ? 'This file lives on disk at: ${output_file_option_dirs[tmpl['key']]}' : null"> + <b-select name="${tmpl['setting_file']}" + v-model="outputFileTemplateSettings['${tmpl['setting_file']}']" + @input="settingsNeedSaved = true"> + <option :value="null">-new-</option> + <option v-for="option in outputFileTemplateFileOptions['${tmpl['key']}']" + :key="option" + :value="option"> + {{ option }} + </option> + </b-select> + </b-field> + + <b-field label="Upload" + v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !outputFileTemplateSettings['${tmpl['setting_file']}']"> + + <b-field class="file is-primary" + :class="{'has-name': !!outputFileTemplateSettings['${tmpl['setting_file']}']}"> + <b-upload name="${tmpl['setting_file']}.upload" + v-model="outputFileTemplateUploads['${tmpl['key']}']" + class="file-label" + @input="settingsNeedSaved = true"> + <span class="file-cta"> + <b-icon class="file-icon" pack="fas" icon="upload"></b-icon> + <span class="file-label">Click to upload</span> + </span> + </b-upload> + <span v-if="outputFileTemplateUploads['${tmpl['key']}']" + class="file-name"> + {{ outputFileTemplateUploads['${tmpl['key']}'].name }} + </span> + </b-field> + + </b-field> + + </b-field> +</%def> + +<%def name="output_file_templates_section()"> + <h3 class="block is-size-3">Output File Templates</h3> + <div class="block" style="padding-left: 2rem;"> + % for key in output_file_templates: + ${self.output_file_template_field(key)} + % endfor + </div> +</%def> + <%def name="form_content()"></%def> <%def name="page_content()"> @@ -229,6 +291,7 @@ ThisPageData.settingsNeedSaved = false ThisPageData.undoChanges = false ThisPageData.savingSettings = false + ThisPageData.validators = [] ThisPage.methods.purgeSettingsInit = function() { this.purgeSettingsShowDialog = true @@ -260,7 +323,19 @@ } ThisPage.methods.saveSettings = function() { - let msg = this.validateSettings() + let msg + + // nb. this is the future + for (let validator of this.validators) { + msg = validator.call(this) + if (msg) { + alert(msg) + return + } + } + + // nb. legacy method + msg = this.validateSettings() if (msg) { alert(msg) return @@ -291,5 +366,35 @@ window.addEventListener('beforeunload', this.beforeWindowUnload) } + ############################## + ## output file templates + ############################## + + % if output_file_template_settings is not Undefined: + + ThisPageData.outputFileTemplateSettings = ${json.dumps(output_file_template_settings)|n} + ThisPageData.outputFileTemplateFileOptions = ${json.dumps(output_file_options)|n} + ThisPageData.outputFileTemplateUploads = { + % for key in output_file_templates: + '${key}': null, + % endfor + } + + ThisPage.methods.validateOutputFileTemplateSettings = function() { + % for tmpl in output_file_templates.values(): + if (this.outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') { + if (!this.outputFileTemplateSettings['${tmpl['setting_file']}']) { + if (!this.outputFileTemplateUploads['${tmpl['key']}']) { + return "You must provide a file to upload for the ${tmpl['label']} template." + } + } + } + % endfor + } + + ThisPageData.validators.push(ThisPage.methods.validateOutputFileTemplateSettings) + + % endif + </script> </%def> diff --git a/tailbone/templates/themes/waterpark/configure.mako b/tailbone/templates/themes/waterpark/configure.mako index 9ac9a5cd..7a3e5261 100644 --- a/tailbone/templates/themes/waterpark/configure.mako +++ b/tailbone/templates/themes/waterpark/configure.mako @@ -1,2 +1,78 @@ ## -*- coding: utf-8; -*- <%inherit file="wuttaweb:templates/configure.mako" /> +<%namespace name="tailbone_base" file="tailbone:templates/configure.mako" /> + +<%def name="input_file_templates_section()"> + ${tailbone_base.input_file_templates_section()} +</%def> + +<%def name="output_file_templates_section()"> + ${tailbone_base.output_file_templates_section()} +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> + + ############################## + ## input file templates + ############################## + + % if input_file_template_settings is not Undefined: + + ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n} + ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n} + ThisPageData.inputFileTemplateUploads = { + % for key in input_file_templates: + '${key}': null, + % endfor + } + + ThisPage.methods.validateInputFileTemplateSettings = function() { + % for tmpl in input_file_templates.values(): + if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') { + if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) { + if (!this.inputFileTemplateUploads['${tmpl['key']}']) { + return "You must provide a file to upload for the ${tmpl['label']} template." + } + } + } + % endfor + } + + ThisPageData.validators.push(ThisPage.methods.validateInputFileTemplateSettings) + + % endif + + ############################## + ## output file templates + ############################## + + % if output_file_template_settings is not Undefined: + + ThisPageData.outputFileTemplateSettings = ${json.dumps(output_file_template_settings)|n} + ThisPageData.outputFileTemplateFileOptions = ${json.dumps(output_file_options)|n} + ThisPageData.outputFileTemplateUploads = { + % for key in output_file_templates: + '${key}': null, + % endfor + } + + ThisPage.methods.validateOutputFileTemplateSettings = function() { + % for tmpl in output_file_templates.values(): + if (this.outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') { + if (!this.outputFileTemplateSettings['${tmpl['setting_file']}']) { + if (!this.outputFileTemplateUploads['${tmpl['key']}']) { + return "You must provide a file to upload for the ${tmpl['label']} template." + } + } + } + % endfor + } + + ThisPageData.validators.push(ThisPage.methods.validateOutputFileTemplateSettings) + + % endif + + </script> +</%def> diff --git a/tailbone/views/master.py b/tailbone/views/master.py index a8365482..e4d6c3f6 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -117,6 +117,7 @@ class MasterView(View): supports_prev_next = False supports_import_batch_from_file = False has_input_file_templates = False + has_output_file_templates = False configurable = False # set to True to add "View *global* Objects" permission, and @@ -1820,6 +1821,26 @@ class MasterView(View): path = os.path.join(basedir, filespec) return self.file_response(path) + def download_output_file_template(self): + """ + View for downloading an output file template. + """ + key = self.request.GET['key'] + filespec = self.request.GET['file'] + + matches = [tmpl for tmpl in self.get_output_file_templates() + if tmpl['key'] == key] + if not matches: + raise self.notfound() + + template = matches[0] + templatesdir = os.path.join(self.rattail_config.datadir(), + 'templates', 'output_files', + self.get_route_prefix()) + basedir = os.path.join(templatesdir, template['key']) + path = os.path.join(basedir, filespec) + return self.file_response(path) + def edit(self): """ View for editing an existing model record. @@ -2848,6 +2869,12 @@ class MasterView(View): kwargs['input_file_templates'] = OrderedDict([(tmpl['key'], tmpl) for tmpl in templates]) + # add info for downloadable output file templates, if any + if self.has_output_file_templates: + templates = self.normalize_output_file_templates() + kwargs['output_file_templates'] = OrderedDict([(tmpl['key'], tmpl) + for tmpl in templates]) + return kwargs def get_input_file_templates(self): @@ -2922,6 +2949,81 @@ class MasterView(View): return templates + def get_output_file_templates(self): + return [] + + def normalize_output_file_templates(self, templates=None, + include_file_options=False): + if templates is None: + templates = self.get_output_file_templates() + + route_prefix = self.get_route_prefix() + + if include_file_options: + templatesdir = os.path.join(self.rattail_config.datadir(), + 'templates', 'output_files', + route_prefix) + + for template in templates: + + if 'config_section' not in template: + if hasattr(self, 'output_file_template_config_section'): + template['config_section'] = self.output_file_template_config_section + else: + template['config_section'] = route_prefix + section = template['config_section'] + + if 'config_prefix' not in template: + template['config_prefix'] = '{}.{}'.format( + self.output_file_template_config_prefix, + template['key']) + prefix = template['config_prefix'] + + for key in ('mode', 'file', 'url'): + + if 'option_{}'.format(key) not in template: + template['option_{}'.format(key)] = '{}.{}'.format(prefix, key) + + if 'setting_{}'.format(key) not in template: + template['setting_{}'.format(key)] = '{}.{}'.format( + section, + template['option_{}'.format(key)]) + + if key not in template: + value = self.rattail_config.get( + section, + template['option_{}'.format(key)]) + if value is not None: + template[key] = value + + template.setdefault('mode', 'default') + template.setdefault('file', None) + template.setdefault('url', template['default_url']) + + if include_file_options: + options = [] + basedir = os.path.join(templatesdir, template['key']) + if os.path.exists(basedir): + for name in sorted(os.listdir(basedir)): + if len(name) == 4 and name.isdigit(): + files = os.listdir(os.path.join(basedir, name)) + if len(files) == 1: + options.append(os.path.join(name, files[0])) + template['file_options'] = options + template['file_options_dir'] = basedir + + if template['mode'] == 'external': + template['effective_url'] = template['url'] + elif template['mode'] == 'hosted': + template['effective_url'] = self.request.route_url( + '{}.download_output_file_template'.format(route_prefix), + _query={'key': template['key'], + 'file': template['file']}) + else: + template['effective_url'] = template['default_url'] + + return templates + def template_kwargs_index(self, **kwargs): """ Method stub, so subclass can always invoke super() for it. @@ -2969,6 +3071,12 @@ class MasterView(View): items.append(tags.link_to(f"Download {template['label']} Template", template['effective_url'])) + if self.has_output_file_templates and self.has_perm('configure'): + templates = self.normalize_output_file_templates() + for template in templates: + items.append(tags.link_to(f"Download {template['label']} Template", + template['effective_url'])) + # if self.viewing: # # # TODO: either make this configurable, or just lose it. @@ -5204,6 +5312,39 @@ class MasterView(View): data[template['setting_file']] = os.path.join(numdir, info['filename']) + if self.has_output_file_templates: + templatesdir = os.path.join(self.rattail_config.datadir(), + 'templates', 'output_files', + self.get_route_prefix()) + + def get_next_filedir(basedir): + nextid = 1 + while True: + path = os.path.join(basedir, '{:04d}'.format(nextid)) + if not os.path.exists(path): + # this should fail if there happens to be a race + # condition and someone else got to this id first + os.mkdir(path) + return path + nextid += 1 + + for template in self.normalize_output_file_templates(): + key = '{}.upload'.format(template['setting_file']) + if key in uploads: + assert self.request.POST[template['setting_mode']] == 'hosted' + assert not self.request.POST[template['setting_file']] + info = uploads[key] + basedir = os.path.join(templatesdir, template['key']) + if not os.path.exists(basedir): + os.makedirs(basedir) + filedir = get_next_filedir(basedir) + filepath = os.path.join(filedir, info['filename']) + shutil.copyfile(info['filepath'], filepath) + shutil.rmtree(info['filedir']) + numdir = os.path.basename(filedir) + data[template['setting_file']] = os.path.join(numdir, + info['filename']) + def configure_get_simple_settings(self): """ If you have some "simple" settings, each of which basically @@ -5248,7 +5389,8 @@ class MasterView(View): simple['option']) def configure_get_context(self, simple_settings=None, - input_file_templates=True): + input_file_templates=True, + output_file_templates=True): """ Returns the full context dict, for rendering the configure page template. @@ -5305,10 +5447,27 @@ class MasterView(View): context['input_file_options'] = file_options context['input_file_option_dirs'] = file_option_dirs + # add settings for output file templates, if any + if output_file_templates and self.has_output_file_templates: + settings = {} + file_options = {} + file_option_dirs = {} + for template in self.normalize_output_file_templates( + include_file_options=True): + settings[template['setting_mode']] = template['mode'] + settings[template['setting_file']] = template['file'] + settings[template['setting_url']] = template['url'] + file_options[template['key']] = template['file_options'] + file_option_dirs[template['key']] = template['file_options_dir'] + context['output_file_template_settings'] = settings + context['output_file_options'] = file_options + context['output_file_option_dirs'] = file_option_dirs + return context def configure_gather_settings(self, data, simple_settings=None, - input_file_templates=True): + input_file_templates=True, + output_file_templates=True): settings = [] # maybe collect "simple" settings @@ -5354,10 +5513,30 @@ class MasterView(View): settings.append({'name': template['setting_url'], 'value': data.get(template['setting_url'])}) + # maybe also collect output file template settings + if output_file_templates and self.has_output_file_templates: + for template in self.normalize_output_file_templates(): + + # mode + settings.append({'name': template['setting_mode'], + 'value': data.get(template['setting_mode'])}) + + # file + value = data.get(template['setting_file']) + if value: + # nb. avoid saving if empty, so can remain "null" + settings.append({'name': template['setting_file'], + 'value': value}) + + # url + settings.append({'name': template['setting_url'], + 'value': data.get(template['setting_url'])}) + return settings def configure_remove_settings(self, simple_settings=None, - input_file_templates=True): + input_file_templates=True, + output_file_templates=True): app = self.get_rattail_app() model = self.model names = [] @@ -5376,6 +5555,14 @@ class MasterView(View): template['setting_url'], ]) + if output_file_templates and self.has_output_file_templates: + for template in self.normalize_output_file_templates(): + names.extend([ + template['setting_mode'], + template['setting_file'], + template['setting_url'], + ]) + if names: # nb. using thread-local session here; we do not use # self.Session b/c it may not point to Rattail @@ -5638,6 +5825,15 @@ class MasterView(View): route_name='{}.download_input_file_template'.format(route_prefix), permission='{}.create'.format(permission_prefix)) + # download output file template + if cls.has_output_file_templates and cls.configurable: + config.add_route(f'{route_prefix}.download_output_file_template', + f'{url_prefix}/download-output-file-template') + config.add_view(cls, attr='download_output_file_template', + route_name=f'{route_prefix}.download_output_file_template', + # TODO: this is different from input file, should change? + permission=f'{permission_prefix}.configure') + # view if cls.viewable: cls._defaults_view(config) From b6a8e508bf2629d528b1bba3e1b12d6da83b1abf Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 20 Aug 2024 22:16:01 -0500 Subject: [PATCH 12/85] fix: prefer wuttaweb config for "home redirect to login" feature --- tailbone/views/common.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tailbone/views/common.py b/tailbone/views/common.py index 7e9ddb09..26ef2626 100644 --- a/tailbone/views/common.py +++ b/tailbone/views/common.py @@ -25,6 +25,7 @@ Various common views """ import os +import warnings from collections import OrderedDict from rattail.batch import consume_batch_id @@ -50,9 +51,21 @@ class CommonView(View): Home page view. """ app = self.get_rattail_app() + + # maybe auto-redirect anons to login if not self.request.user: - if self.rattail_config.getbool('tailbone', 'login_is_home', default=True): - raise self.redirect(self.request.route_url('login')) + redirect = self.config.get_bool('wuttaweb.home_redirect_to_login') + if redirect is None: + redirect = self.config.get_bool('tailbone.login_is_home') + if redirect is not None: + warnings.warn("tailbone.login_is_home setting is deprecated; " + "please set wuttaweb.home_redirect_to_login instead", + DeprecationWarning) + else: + # TODO: this is opposite of upstream default, should change + redirect = True + if redirect: + return self.redirect(self.request.route_url('login')) image_url = self.rattail_config.get( 'tailbone', 'main_image_url', From 2ffc067097a7c979c4935eee1da4d697e7774845 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 20 Aug 2024 22:27:11 -0500 Subject: [PATCH 13/85] fix: inherit from wuttaweb for appinfo/index template although for now, still must override for some link buttons --- tailbone/templates/appinfo/index.mako | 95 +------------------------- tailbone/templates/grids/complete.mako | 14 ++++ tailbone/views/settings.py | 10 +++ 3 files changed, 26 insertions(+), 93 deletions(-) diff --git a/tailbone/templates/appinfo/index.mako b/tailbone/templates/appinfo/index.mako index 75032c1f..faaea935 100644 --- a/tailbone/templates/appinfo/index.mako +++ b/tailbone/templates/appinfo/index.mako @@ -1,8 +1,7 @@ ## -*- coding: utf-8; -*- -<%inherit file="/master/index.mako" /> +<%inherit file="wuttaweb:templates/appinfo/index.mako" /> <%def name="page_content()"> - <div class="buttons"> <once-button type="is-primary" @@ -28,95 +27,5 @@ </div> - <${b}-collapse class="panel" open> - - <template #trigger="props"> - <div class="panel-heading" - style="cursor: pointer;" - role="button"> - - ## TODO: for some reason buefy will "reuse" the icon - ## element in such a way that its display does not - ## refresh. so to work around that, we use different - ## structure for the two icons, so buefy is forced to - ## re-draw - - <b-icon v-if="props.open" - pack="fas" - icon="angle-down"> - </b-icon> - - <span v-if="!props.open"> - <b-icon pack="fas" - icon="angle-right"> - </b-icon> - </span> - - <span>Configuration Files</span> - </div> - </template> - - <div class="panel-block"> - <div style="width: 100%;"> - <${b}-table :data="configFiles"> - - <${b}-table-column field="priority" - label="Priority" - v-slot="props"> - {{ props.row.priority }} - </${b}-table-column> - - <${b}-table-column field="path" - label="File Path" - v-slot="props"> - {{ props.row.path }} - </${b}-table-column> - - </${b}-table> - </div> - </div> - </${b}-collapse> - - <${b}-collapse class="panel" - :open="false"> - - <template #trigger="props"> - <div class="panel-heading" - style="cursor: pointer;" - role="button"> - - ## TODO: for some reason buefy will "reuse" the icon - ## element in such a way that its display does not - ## refresh. so to work around that, we use different - ## structure for the two icons, so buefy is forced to - ## re-draw - - <b-icon v-if="props.open" - pack="fas" - icon="angle-down"> - </b-icon> - - <span v-if="!props.open"> - <b-icon pack="fas" - icon="angle-right"> - </b-icon> - </span> - - <strong>Installed Packages</strong> - </div> - </template> - - <div class="panel-block"> - <div style="width: 100%;"> - ${grid.render_vue_tag()} - </div> - </div> - </${b}-collapse> -</%def> - -<%def name="modify_vue_vars()"> - ${parent.modify_vue_vars()} - <script> - ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(request.rattail_config.prioritized_files, 1)])|n} - </script> + ${parent.page_content()} </%def> diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index c136273b..5d406512 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -257,6 +257,9 @@ loading: false, ajaxDataUrl: ${json.dumps(getattr(grid, 'ajax_data_url', request.path_url))|n}, + ## nb. this tracks whether grid.fetchFirstData() happened + fetchedFirstData: false, + savingDefaults: false, data: ${grid.vue_component}CurrentData, @@ -519,6 +522,17 @@ ...this.getFilterParams()} }, + ## nb. this is meant to call for a grid which is hidden at + ## first, when it is first being shown to the user. and if + ## it was initialized with empty data set. + async fetchFirstData() { + if (this.fetchedFirstData) { + return + } + await this.loadAsyncData() + this.fetchedFirstData = true + }, + ## TODO: i noticed buefy docs show using `async` keyword here, ## so now i am too. knowing nothing at all of if/how this is ## supposed to improve anything. we shall see i guess diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py index bda62ccc..4d99cb2a 100644 --- a/tailbone/views/settings.py +++ b/tailbone/views/settings.py @@ -71,10 +71,20 @@ class AppInfoView(MasterView): app.get_title()) def get_data(self, session=None): + """ """ + + # nb. init with empty data, only load it upon user request + if not self.request.GET.get('partial'): + return [] + + # TODO: pretty sure this is not cross-platform. probably some + # sort of pip methods belong on the app handler? or it should + # have a pip handler for all that? pip = os.path.join(sys.prefix, 'bin', 'pip') output = subprocess.check_output([pip, 'list', '--format=json']) data = json.loads(output.decode('utf_8').strip()) + # must avoid null values for sort to work right for pkg in data: pkg.setdefault('editable_project_location', '') From f7554602420eceb62d98fbde600c86aba0a944a3 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 20 Aug 2024 23:23:23 -0500 Subject: [PATCH 14/85] feat: inherit from wuttaweb for AppInfoView, appinfo/configure template --- tailbone/menus.py | 2 +- tailbone/templates/appinfo/configure.mako | 247 +----------------- .../themes/butterball/buefy-components.mako | 9 + tailbone/views/settings.py | 202 +++----------- 4 files changed, 48 insertions(+), 412 deletions(-) diff --git a/tailbone/menus.py b/tailbone/menus.py index abd0b58b..3ddee095 100644 --- a/tailbone/menus.py +++ b/tailbone/menus.py @@ -703,7 +703,7 @@ class TailboneMenuHandler(WuttaMenuHandler): }, {'type': 'sep'}, { - 'title': "App Details", + 'title': "App Info", 'route': 'appinfo', 'perm': 'appinfo.list', }, diff --git a/tailbone/templates/appinfo/configure.mako b/tailbone/templates/appinfo/configure.mako index 4794f00b..9d866cea 100644 --- a/tailbone/templates/appinfo/configure.mako +++ b/tailbone/templates/appinfo/configure.mako @@ -1,247 +1,2 @@ ## -*- coding: utf-8; -*- -<%inherit file="/configure.mako" /> - -<%def name="form_content()"> - - <h3 class="block is-size-3">Basics</h3> - <div class="block" style="padding-left: 2rem;"> - - <b-field grouped> - - <b-field label="App Title"> - <b-input name="rattail.app_title" - v-model="simpleSettings['rattail.app_title']" - @input="settingsNeedSaved = true"> - </b-input> - </b-field> - - <b-field label="Node Type"> - ## TODO: should be a dropdown, app handler defines choices - <b-input name="rattail.node_type" - v-model="simpleSettings['rattail.node_type']" - @input="settingsNeedSaved = true"> - </b-input> - </b-field> - - <b-field label="Node Title"> - <b-input name="rattail.node_title" - v-model="simpleSettings['rattail.node_title']" - @input="settingsNeedSaved = true"> - </b-input> - </b-field> - - </b-field> - - <b-field> - <b-checkbox name="rattail.production" - v-model="simpleSettings['rattail.production']" - native-value="true" - @input="settingsNeedSaved = true"> - Production Mode - </b-checkbox> - </b-field> - - <div class="level-left"> - <div class="level-item"> - <b-field> - <b-checkbox name="rattail.running_from_source" - v-model="simpleSettings['rattail.running_from_source']" - native-value="true" - @input="settingsNeedSaved = true"> - Running from Source - </b-checkbox> - </b-field> - </div> - <div class="level-item"> - <b-field label="Top-Level Package" horizontal - v-if="simpleSettings['rattail.running_from_source']"> - <b-input name="rattail.running_from_source.rootpkg" - v-model="simpleSettings['rattail.running_from_source.rootpkg']" - @input="settingsNeedSaved = true"> - </b-input> - </b-field> - </div> - </div> - - </div> - - <h3 class="block is-size-3">Display</h3> - <div class="block" style="padding-left: 2rem;"> - - <b-field grouped> - - <b-field label="Background Color"> - <b-input name="tailbone.background_color" - v-model="simpleSettings['tailbone.background_color']" - @input="settingsNeedSaved = true"> - </b-input> - </b-field> - - </b-field> - - </div> - - <h3 class="block is-size-3">Grids</h3> - <div class="block" style="padding-left: 2rem;"> - - <b-field grouped> - - <b-field label="Default Page Size"> - <b-input name="tailbone.grid.default_pagesize" - v-model="simpleSettings['tailbone.grid.default_pagesize']" - @input="settingsNeedSaved = true"> - </b-input> - </b-field> - - </b-field> - - </div> - - <h3 class="block is-size-3">Web Libraries</h3> - <div class="block" style="padding-left: 2rem;"> - - <${b}-table :data="weblibs"> - - <${b}-table-column field="title" - label="Name" - v-slot="props"> - {{ props.row.title }} - </${b}-table-column> - - <${b}-table-column field="configured_version" - label="Version" - v-slot="props"> - {{ props.row.configured_version || props.row.default_version }} - </${b}-table-column> - - <${b}-table-column field="configured_url" - label="URL Override" - v-slot="props"> - {{ props.row.configured_url }} - </${b}-table-column> - - <${b}-table-column field="live_url" - label="Effective (Live) URL" - v-slot="props"> - <span v-if="props.row.modified" - class="has-text-warning"> - save settings and refresh page to see new URL - </span> - <span v-if="!props.row.modified"> - {{ props.row.live_url }} - </span> - </${b}-table-column> - - <${b}-table-column field="actions" - label="Actions" - v-slot="props"> - <a href="#" - @click.prevent="editWebLibraryInit(props.row)"> - % if request.use_oruga: - <o-icon icon="edit" /> - % else: - <i class="fas fa-edit"></i> - % endif - Edit - </a> - </${b}-table-column> - - </${b}-table> - - % for weblib in weblibs: - ${h.hidden('wuttaweb.libver.{}'.format(weblib['key']), **{':value': "simpleSettings['wuttaweb.libver.{}']".format(weblib['key'])})} - ${h.hidden('wuttaweb.liburl.{}'.format(weblib['key']), **{':value': "simpleSettings['wuttaweb.liburl.{}']".format(weblib['key'])})} - % endfor - - <${b}-modal has-modal-card - % if request.use_oruga: - v-model:active="editWebLibraryShowDialog" - % else: - :active.sync="editWebLibraryShowDialog" - % endif - > - <div class="modal-card"> - - <header class="modal-card-head"> - <p class="modal-card-title">Web Library: {{ editWebLibraryRecord.title }}</p> - </header> - - <section class="modal-card-body"> - - <b-field grouped> - - <b-field label="Default Version"> - <b-input v-model="editWebLibraryRecord.default_version" - disabled> - </b-input> - </b-field> - - <b-field label="Override Version"> - <b-input v-model="editWebLibraryVersion"> - </b-input> - </b-field> - - </b-field> - - <b-field label="Override URL"> - <b-input v-model="editWebLibraryURL" - expanded /> - </b-field> - - <b-field label="Effective URL (as of last page load)"> - <b-input v-model="editWebLibraryRecord.live_url" - disabled - expanded /> - </b-field> - - </section> - - <footer class="modal-card-foot"> - <b-button type="is-primary" - @click="editWebLibrarySave()" - icon-pack="fas" - icon-left="save"> - Save - </b-button> - <b-button @click="editWebLibraryShowDialog = false"> - Cancel - </b-button> - </footer> - </div> - </${b}-modal> - - </div> -</%def> - -<%def name="modify_vue_vars()"> - ${parent.modify_vue_vars()} - <script> - - ThisPageData.weblibs = ${json.dumps(weblibs)|n} - - ThisPageData.editWebLibraryShowDialog = false - ThisPageData.editWebLibraryRecord = {} - ThisPageData.editWebLibraryVersion = null - ThisPageData.editWebLibraryURL = null - - ThisPage.methods.editWebLibraryInit = function(row) { - this.editWebLibraryRecord = row - this.editWebLibraryVersion = row.configured_version - this.editWebLibraryURL = row.configured_url - this.editWebLibraryShowDialog = true - } - - ThisPage.methods.editWebLibrarySave = function() { - this.editWebLibraryRecord.configured_version = this.editWebLibraryVersion - this.editWebLibraryRecord.configured_url = this.editWebLibraryURL - this.editWebLibraryRecord.modified = true - - this.simpleSettings[`wuttaweb.libver.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryVersion - this.simpleSettings[`wuttaweb.liburl.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryURL - - this.settingsNeedSaved = true - this.editWebLibraryShowDialog = false - } - - </script> -</%def> +<%inherit file="wuttaweb:templates/appinfo/configure.mako" /> diff --git a/tailbone/templates/themes/butterball/buefy-components.mako b/tailbone/templates/themes/butterball/buefy-components.mako index 51a0deb9..3a2cd798 100644 --- a/tailbone/templates/themes/butterball/buefy-components.mako +++ b/tailbone/templates/themes/butterball/buefy-components.mako @@ -666,6 +666,7 @@ <%def name="make_b_tooltip_component()"> <script type="text/x-template" id="b-tooltip-template"> <o-tooltip :label="label" + :position="orugaPosition" :multiline="multilined"> <slot /> </o-tooltip> @@ -676,6 +677,14 @@ props: { label: String, multilined: Boolean, + position: String, + }, + computed: { + orugaPosition() { + if (this.position) { + return this.position.replace(/^is-/, '') + } + }, }, } </script> diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py index 4d99cb2a..099a77e1 100644 --- a/tailbone/views/settings.py +++ b/tailbone/views/settings.py @@ -25,11 +25,7 @@ Settings Views """ import json -import os import re -import subprocess -import sys -from collections import OrderedDict import colander @@ -37,201 +33,77 @@ from rattail.db.model import Setting from rattail.settings import Setting as AppSetting from rattail.util import import_module_path -from tailbone import forms +from tailbone import forms, grids from tailbone.db import Session from tailbone.views import MasterView, View from wuttaweb.util import get_libver, get_liburl +from wuttaweb.views.settings import AppInfoView as WuttaAppInfoView -class AppInfoView(MasterView): - """ - Master view for the overall app, to show/edit config etc. - """ - route_prefix = 'appinfo' - model_key = 'UNUSED' - model_title = "UNUSED" - model_title_plural = "App Details" - creatable = False - viewable = False - editable = False - deletable = False - filterable = False - pageable = False - configurable = True +class AppInfoView(WuttaAppInfoView): + """ """ + Session = Session + weblib_config_prefix = 'tailbone' - grid_columns = [ - 'name', - 'version', - 'editable_project_location', - ] - - def get_index_title(self): - app = self.get_rattail_app() - return "{} for {}".format(self.get_model_title_plural(), - app.get_title()) - - def get_data(self, session=None): + # TODO: for now we override to get tailbone searchable grid + def make_grid(self, **kwargs): """ """ - - # nb. init with empty data, only load it upon user request - if not self.request.GET.get('partial'): - return [] - - # TODO: pretty sure this is not cross-platform. probably some - # sort of pip methods belong on the app handler? or it should - # have a pip handler for all that? - pip = os.path.join(sys.prefix, 'bin', 'pip') - output = subprocess.check_output([pip, 'list', '--format=json']) - data = json.loads(output.decode('utf_8').strip()) - - # must avoid null values for sort to work right - for pkg in data: - pkg.setdefault('editable_project_location', '') - - return data + return grids.Grid(self.request, **kwargs) def configure_grid(self, g): + """ """ super().configure_grid(g) - # sort on frontend - g.sort_on_backend = False - g.sort_multiple = False - g.set_sort_defaults('name') - # name g.set_searchable('name') # editable_project_location g.set_searchable('editable_project_location') - def template_kwargs_index(self, **kwargs): - kwargs = super().template_kwargs_index(**kwargs) - kwargs['configure_button_title'] = "Configure App" - return kwargs - - def get_weblibs(self): - """ """ - return OrderedDict([ - ('vue', "Vue"), - ('vue_resource', "vue-resource"), - ('buefy', "Buefy"), - ('buefy.css', "Buefy CSS"), - ('fontawesome', "FontAwesome"), - ('bb_vue', "(BB) vue"), - ('bb_oruga', "(BB) @oruga-ui/oruga-next"), - ('bb_oruga_bulma', "(BB) @oruga-ui/theme-bulma (JS)"), - ('bb_oruga_bulma_css', "(BB) @oruga-ui/theme-bulma (CSS)"), - ('bb_fontawesome_svg_core', "(BB) @fortawesome/fontawesome-svg-core"), - ('bb_free_solid_svg_icons', "(BB) @fortawesome/free-solid-svg-icons"), - ('bb_vue_fontawesome', "(BB) @fortawesome/vue-fontawesome"), - ]) - def configure_get_context(self, **kwargs): """ """ context = super().configure_get_context(**kwargs) simple_settings = context['simple_settings'] - weblibs = self.get_weblibs() + weblibs = context['weblibs'] - for key in weblibs: - title = weblibs[key] - weblibs[key] = { - 'key': key, - 'title': title, - - # nb. these values are exactly as configured, and are - # used for editing the settings - 'configured_version': get_libver(self.request, key, - prefix='tailbone', - configured_only=True), - 'configured_url': get_liburl(self.request, key, - prefix='tailbone', - configured_only=True), - - # these are for informational purposes only - 'default_version': get_libver(self.request, key, - prefix='tailbone', - default_only=True), - 'live_url': get_liburl(self.request, key, - prefix='tailbone'), - } + for weblib in weblibs: + key = weblib['key'] # TODO: this is only needed to migrate legacy settings to - # use the newer wutaweb setting names + # use the newer wuttaweb setting names url = simple_settings[f'wuttaweb.liburl.{key}'] - if not url and weblibs[key]['configured_url']: - simple_settings[f'wuttaweb.liburl.{key}'] = weblibs[key]['configured_url'] + if not url and weblib['configured_url']: + simple_settings[f'wuttaweb.liburl.{key}'] = weblib['configured_url'] - context['weblibs'] = list(weblibs.values()) return context def configure_get_simple_settings(self): """ """ - simple_settings = [ + simple_settings = super().configure_get_simple_settings() - # basics - {'section': 'rattail', - 'option': 'app_title'}, - {'section': 'rattail', - 'option': 'node_type'}, - {'section': 'rattail', - 'option': 'node_title'}, - {'section': 'rattail', - 'option': 'production', - 'type': bool}, - {'section': 'rattail', - 'option': 'running_from_source', - 'type': bool}, - {'section': 'rattail', - 'option': 'running_from_source.rootpkg'}, + # TODO: the update home page redirect setting is off by + # default for wuttaweb, but on for tailbone + for setting in simple_settings: + if setting['name'] == 'wuttaweb.home_redirect_to_login': + value = self.config.get_bool('wuttaweb.home_redirect_to_login') + if value is None: + value = self.config.get_bool('tailbone.login_is_home', default=True) + setting['default'] = value + break - # display - {'section': 'tailbone', - 'option': 'background_color'}, + # nb. these are no longer used (deprecated), but we keep + # them defined here so the tool auto-deletes them - # grids - {'section': 'tailbone', - 'option': 'grid.default_pagesize', - # TODO: seems like should enforce this, but validation is - # not setup yet - # 'type': int - }, + simple_settings.extend([ + {'name': 'tailbone.buefy_version'}, + {'name': 'tailbone.vue_version'}, + ]) - # nb. these are no longer used (deprecated), but we keep - # them defined here so the tool auto-deletes them - {'section': 'tailbone', - 'option': 'buefy_version'}, - {'section': 'tailbone', - 'option': 'vue_version'}, - - ] - - def getval(key): - return self.config.get(f'tailbone.{key}') - - weblibs = self.get_weblibs() - for key, title in weblibs.items(): - - simple_settings.append({ - 'section': 'wuttaweb', - 'option': f"libver.{key}", - 'default': getval(f"libver.{key}"), - }) - simple_settings.append({ - 'section': 'wuttaweb', - 'option': f"liburl.{key}", - 'default': getval(f"liburl.{key}"), - }) - - # nb. these are no longer used (deprecated), but we keep - # them defined here so the tool auto-deletes them - simple_settings.append({ - 'section': 'tailbone', - 'option': f"libver.{key}", - }) - simple_settings.append({ - 'section': 'tailbone', - 'option': f"liburl.{key}", - }) + for key in self.get_weblibs(): + simple_settings.extend([ + {'name': f'tailbone.libver.{key}'}, + {'name': f'tailbone.liburl.{key}'}, + ]) return simple_settings From 71abbe06da0d08c4a285fbca2b583c570f3def4c Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 21 Aug 2024 00:07:03 -0500 Subject: [PATCH 15/85] feat: inherit from wuttaweb templates for home, login pages --- tailbone/templates/base_meta.mako | 13 +----- tailbone/templates/home.mako | 30 +----------- tailbone/templates/login.mako | 77 ++----------------------------- tailbone/views/common.py | 12 +++-- 4 files changed, 18 insertions(+), 114 deletions(-) diff --git a/tailbone/templates/base_meta.mako b/tailbone/templates/base_meta.mako index 00cfdfe9..b6376448 100644 --- a/tailbone/templates/base_meta.mako +++ b/tailbone/templates/base_meta.mako @@ -1,10 +1,7 @@ ## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/base_meta.mako" /> -<%def name="app_title()">${rattail_app.get_node_title()}</%def> - -<%def name="global_title()">${"[STAGE] " if not request.rattail_config.production() else ''}${self.app_title()}</%def> - -<%def name="extra_styles()"></%def> +<%def name="app_title()">${app.get_node_title()}</%def> <%def name="favicon()"> <link rel="icon" type="image/x-icon" href="${request.rattail_config.get('tailbone', 'favicon_url', default=request.static_url('tailbone:static/img/rattail.ico'))}" /> @@ -13,9 +10,3 @@ <%def name="header_logo()"> ${h.image(request.rattail_config.get('tailbone', 'header_image_url', default=request.static_url('tailbone:static/img/rattail.ico')), "Header Logo", style="height: 49px;")} </%def> - -<%def name="footer()"> - <p class="has-text-centered"> - powered by ${h.link_to("Rattail", url('about'))} - </p> -</%def> diff --git a/tailbone/templates/home.mako b/tailbone/templates/home.mako index e4f7d072..54e44d57 100644 --- a/tailbone/templates/home.mako +++ b/tailbone/templates/home.mako @@ -1,33 +1,7 @@ ## -*- coding: utf-8; -*- -<%inherit file="/page.mako" /> -<%namespace name="base_meta" file="/base_meta.mako" /> - -<%def name="title()">Home</%def> - -<%def name="extra_styles()"> - ${parent.extra_styles()} - <style type="text/css"> - .logo { - text-align: center; - } - .logo img { - margin: 3em auto; - max-height: 350px; - max-width: 800px; - } - </style> -</%def> +<%inherit file="wuttaweb:templates/home.mako" /> +## DEPRECATED; remains for back-compat <%def name="render_this_page()"> ${self.page_content()} </%def> - -<%def name="page_content()"> - <div class="logo"> - ${h.image(image_url, "{} logo".format(capture(base_meta.app_title)))} - <h1>Welcome to ${base_meta.app_title()}</h1> - </div> -</%def> - - -${parent.body()} diff --git a/tailbone/templates/login.mako b/tailbone/templates/login.mako index 3eb46403..d2ea7828 100644 --- a/tailbone/templates/login.mako +++ b/tailbone/templates/login.mako @@ -1,84 +1,17 @@ ## -*- coding: utf-8; -*- -<%inherit file="/form.mako" /> -<%namespace name="base_meta" file="/base_meta.mako" /> - -<%def name="title()">Login</%def> +<%inherit file="wuttaweb:templates/auth/login.mako" /> +## TODO: this will not be needed with wuttaform <%def name="extra_styles()"> ${parent.extra_styles()} - <style type="text/css"> - .logo img { - display: block; - margin: 3rem auto; - max-height: 350px; - max-width: 800px; - } - - /* must force a particular label with, in order to make sure */ - /* the username and password inputs are the same size */ - .field.is-horizontal .field-label .label { - text-align: left; - width: 6rem; - } - - .buttons { + <style> + .card-content .buttons { justify-content: right; } </style> </%def> -<%def name="logo()"> - ${h.image(image_url, "{} logo".format(capture(base_meta.app_title)))} -</%def> - -<%def name="login_form()"> - <div class="form"> - ${form.render_deform(form_kwargs={'data-ajax': 'false'})|n} - </div> -</%def> - +## DEPRECATED; remains for back-compat <%def name="render_this_page()"> ${self.page_content()} </%def> - -<%def name="page_content()"> - <div class="logo"> - ${self.logo()} - </div> - - <div class="columns is-centered"> - <div class="column is-narrow"> - <div class="card"> - <div class="card-content"> - <tailbone-form></tailbone-form> - </div> - </div> - </div> - </div> -</%def> - -<%def name="modify_vue_vars()"> - ${parent.modify_vue_vars()} - <script> - - ${form.vue_component}Data.usernameInput = null - - ${form.vue_component}.mounted = function() { - this.$refs.username.focus() - this.usernameInput = this.$refs.username.$el.querySelector('input') - this.usernameInput.addEventListener('keydown', this.usernameKeydown) - } - - ${form.vue_component}.beforeDestroy = function() { - this.usernameInput.removeEventListener('keydown', this.usernameKeydown) - } - - ${form.vue_component}.methods.usernameKeydown = function(event) { - if (event.which == 13) { - event.preventDefault() - this.$refs.password.focus() - } - } - - </script> -</%def> diff --git a/tailbone/views/common.py b/tailbone/views/common.py index 26ef2626..f4d98c05 100644 --- a/tailbone/views/common.py +++ b/tailbone/views/common.py @@ -67,9 +67,15 @@ class CommonView(View): if redirect: return self.redirect(self.request.route_url('login')) - image_url = self.rattail_config.get( - 'tailbone', 'main_image_url', - default=self.request.static_url('tailbone:static/img/home_logo.png')) + image_url = self.config.get('wuttaweb.logo_url') + if not image_url: + image_url = self.config.get('tailbone.main_image_url') + if image_url: + warnings.warn("tailbone.main_image_url setting is deprecated; " + "please set wuttaweb.logo_url instead", + DeprecationWarning) + else: + image_url = self.request.static_url('tailbone:static/img/home_logo.png') context = { 'image_url': image_url, From 1d00fe994a069e366d67558d4f5f3709e103e991 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 21 Aug 2024 09:44:32 -0500 Subject: [PATCH 16/85] fix: use wuttaweb to get/render csrf token --- tailbone/helpers.py | 12 ++++----- tailbone/templates/formposter.mako | 2 +- tailbone/templates/forms/deform.mako | 2 +- tailbone/templates/ordering/view.mako | 2 +- tailbone/templates/ordering/worksheet.mako | 2 +- tailbone/templates/page.mako | 2 +- tailbone/templates/themes/waterpark/page.mako | 2 +- tailbone/util.py | 27 +++++++++---------- 8 files changed, 24 insertions(+), 27 deletions(-) diff --git a/tailbone/helpers.py b/tailbone/helpers.py index 23988423..50b38c30 100644 --- a/tailbone/helpers.py +++ b/tailbone/helpers.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -24,6 +24,9 @@ Template Context Helpers """ +# start off with all from wuttaweb +from wuttaweb.helpers import * + import os import datetime from decimal import Decimal @@ -33,12 +36,7 @@ from rattail.time import localtime, make_utc from rattail.util import pretty_quantity, pretty_hours, hours_as_decimal from rattail.db.util import maxlen -from webhelpers2.html import * -from webhelpers2.html.tags import * - -from wuttaweb.util import get_liburl -from tailbone.util import (csrf_token, get_csrf_token, - pretty_datetime, raw_datetime, +from tailbone.util import (pretty_datetime, raw_datetime, render_markdown, route_exists) diff --git a/tailbone/templates/formposter.mako b/tailbone/templates/formposter.mako index ab9c720d..d566a467 100644 --- a/tailbone/templates/formposter.mako +++ b/tailbone/templates/formposter.mako @@ -39,7 +39,7 @@ simplePOST(action, params, success, failure) { - let csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n} + let csrftoken = ${json.dumps(h.get_csrf_token(request))|n} let headers = { '${csrf_header_name}': csrftoken, diff --git a/tailbone/templates/forms/deform.mako b/tailbone/templates/forms/deform.mako index 26c8b4ee..ea35ab17 100644 --- a/tailbone/templates/forms/deform.mako +++ b/tailbone/templates/forms/deform.mako @@ -180,7 +180,7 @@ let ${form.vue_component}Data = { ## TODO: should find a better way to handle CSRF token - csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}, + csrftoken: ${json.dumps(h.get_csrf_token(request))|n}, % if can_edit_help: fieldLabels: ${json.dumps(field_labels)|n}, diff --git a/tailbone/templates/ordering/view.mako b/tailbone/templates/ordering/view.mako index 584559c1..34a6085f 100644 --- a/tailbone/templates/ordering/view.mako +++ b/tailbone/templates/ordering/view.mako @@ -204,7 +204,7 @@ saving: false, ## TODO: should find a better way to handle CSRF token - csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}, + csrftoken: ${json.dumps(h.get_csrf_token(request))|n}, } }, computed: { diff --git a/tailbone/templates/ordering/worksheet.mako b/tailbone/templates/ordering/worksheet.mako index cb98c48f..eb2077e7 100644 --- a/tailbone/templates/ordering/worksheet.mako +++ b/tailbone/templates/ordering/worksheet.mako @@ -250,7 +250,7 @@ submitting: false, ## TODO: should find a better way to handle CSRF token - csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}, + csrftoken: ${json.dumps(h.get_csrf_token(request))|n}, } }, methods: { diff --git a/tailbone/templates/page.mako b/tailbone/templates/page.mako index 54b47278..43b0a266 100644 --- a/tailbone/templates/page.mako +++ b/tailbone/templates/page.mako @@ -38,7 +38,7 @@ const ThisPageData = { ## TODO: should find a better way to handle CSRF token - csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}, + csrftoken: ${json.dumps(h.get_csrf_token(request))|n}, } </script> diff --git a/tailbone/templates/themes/waterpark/page.mako b/tailbone/templates/themes/waterpark/page.mako index 7e6851a7..66ce47dc 100644 --- a/tailbone/templates/themes/waterpark/page.mako +++ b/tailbone/templates/themes/waterpark/page.mako @@ -38,7 +38,7 @@ ${parent.modify_vue_vars()} <script> - ThisPageData.csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n} + ThisPageData.csrftoken = ${json.dumps(h.get_csrf_token(request))|n} % if can_edit_help: ThisPage.props.configureFieldsHelp = Boolean diff --git a/tailbone/util.py b/tailbone/util.py index 594fd69b..71aa35e3 100644 --- a/tailbone/util.py +++ b/tailbone/util.py @@ -41,7 +41,9 @@ from webhelpers2.html import HTML, tags from wuttaweb.util import (get_form_data as wutta_get_form_data, get_libver as wutta_get_libver, - get_liburl as wutta_get_liburl) + get_liburl as wutta_get_liburl, + get_csrf_token as wutta_get_csrf_token, + render_csrf_token) log = logging.getLogger(__name__) @@ -59,22 +61,19 @@ class SortColumn(object): def get_csrf_token(request): - """ - Convenience function to retrieve the effective CSRF token for the given - request. - """ - token = request.session.get_csrf_token() - if token is None: - token = request.session.new_csrf_token() - return token + """ """ + warnings.warn("tailbone.util.get_csrf_token() is deprecated; " + "please use wuttaweb.util.get_csrf_token() instead", + DeprecationWarning, stacklevel=2) + return wutta_get_csrf_token(request) def csrf_token(request, name='_csrf'): - """ - Convenience function. Returns CSRF hidden tag inside hidden DIV. - """ - token = get_csrf_token(request) - return HTML.tag("div", tags.hidden(name, value=token), style="display:none;") + """ """ + warnings.warn("tailbone.util.csrf_token() is deprecated; " + "please use wuttaweb.util.render_csrf_token() instead", + DeprecationWarning, stacklevel=2) + return render_csrf_token(request, name=name) def get_form_data(request): From ffa724ef374ec59e90b51a2b14a83ee703bea5a0 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 21 Aug 2024 15:50:55 -0500 Subject: [PATCH 17/85] fix: move "searchable columns" grid feature to wuttaweb --- tailbone/grids/core.py | 19 +++++++------------ tailbone/templates/grids/complete.mako | 6 ++---- tests/grids/test_core.py | 6 ++++++ 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index eada1041..92452b31 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -200,7 +200,6 @@ class Grid(WuttaGrid): filterable=False, filters={}, use_byte_string_filters=False, - searchable={}, checkboxes=False, checked=None, check_handler=None, @@ -254,6 +253,12 @@ class Grid(WuttaGrid): DeprecationWarning, stacklevel=2) kwargs.setdefault('page', kwargs.pop('default_page')) + if 'searchable' in kwargs: + warnings.warn("searchable param is deprecated for Grid(); " + "please use searchable_columns param instead", + DeprecationWarning, stacklevel=2) + kwargs.setdefault('searchable_columns', kwargs.pop('searchable')) + # TODO: this should not be needed once all templates correctly # reference grid.vue_component etc. kwargs.setdefault('vue_tagname', 'tailbone-grid') @@ -287,8 +292,6 @@ class Grid(WuttaGrid): self.use_byte_string_filters = use_byte_string_filters self.filters = self.make_filters(filters) - self.searchable = searchable or {} - self.checkboxes = checkboxes self.checked = checked if self.checked is None: @@ -481,15 +484,6 @@ class Grid(WuttaGrid): kwargs['label'] = self.labels[key] self.filters[key] = self.make_filter(key, *args, **kwargs) - def set_searchable(self, key, searchable=True): - if searchable: - self.searchable[key] = True - else: - self.searchable.pop(key, None) - - def is_searchable(self, key): - return self.searchable.get(key, False) - def remove_filter(self, key): self.filters.pop(key, None) @@ -1587,6 +1581,7 @@ class Grid(WuttaGrid): 'field': name, 'label': self.get_label(name), 'sortable': self.is_sortable(name), + 'searchable': self.is_searchable(name), 'visible': name not in self.invisible, }) return columns diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index 5d406512..54ad0527 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -136,10 +136,8 @@ <${b}-table-column field="${column['field']}" label="${column['label']}" v-slot="props" - :sortable="${json.dumps(column.get('sortable', False))}" - % if hasattr(grid, 'is_searchable') and grid.is_searchable(column['field']): - searchable - % endif + :sortable="${json.dumps(column.get('sortable', False))|n}" + :searchable="${json.dumps(column.get('searchable', False))|n}" cell-class="c_${column['field']}" :visible="${json.dumps(column.get('visible', True))}"> % if hasattr(grid, 'raw_renderers') and column['field'] in grid.raw_renderers: diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index c621627a..5169e599 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -57,6 +57,12 @@ class TestGrid(WebTestCase): grid = self.make_grid(default_page=42) self.assertEqual(grid.page, 42) + # searchable + grid = self.make_grid() + self.assertEqual(grid.searchable_columns, set()) + grid = self.make_grid(searchable={'foo': True}) + self.assertEqual(grid.searchable_columns, {'foo'}) + def test_vue_tagname(self): # default From e52a83751e8b95c72917277214ff504a0ede13b6 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 21 Aug 2024 20:16:03 -0500 Subject: [PATCH 18/85] feat: move "most" filtering logic for grid class to wuttaweb we still define all filters, and the "most important" grid methods for filtering --- tailbone/grids/core.py | 295 +++++++++-------------------------------- 1 file changed, 62 insertions(+), 233 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 92452b31..969be50a 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -196,9 +196,6 @@ class Grid(WuttaGrid): raw_renderers={}, extra_row_class=None, url='#', - joiners={}, - filterable=False, - filters={}, use_byte_string_filters=False, checkboxes=False, checked=None, @@ -263,6 +260,8 @@ class Grid(WuttaGrid): # reference grid.vue_component etc. kwargs.setdefault('vue_tagname', 'tailbone-grid') + self.use_byte_string_filters = use_byte_string_filters + kwargs['key'] = key kwargs['data'] = data super().__init__(request, **kwargs) @@ -286,11 +285,6 @@ class Grid(WuttaGrid): self.invisible = invisible or [] self.extra_row_class = extra_row_class self.url = url - self.joiners = joiners or {} - - self.filterable = filterable - self.use_byte_string_filters = use_byte_string_filters - self.filters = self.make_filters(filters) self.checkboxes = checkboxes self.checked = checked @@ -446,10 +440,14 @@ class Grid(WuttaGrid): self.remove(oldfield) def set_joiner(self, key, joiner): + """ """ if joiner is None: - self.joiners.pop(key, None) + warnings.warn("specifying None is deprecated for Grid.set_joiner(); " + "please use Grid.remove_joiner() instead", + DeprecationWarning, stacklevel=2) + self.remove_joiner(key) else: - self.joiners[key] = joiner + super().set_joiner(key, joiner) def set_sorter(self, key, *args, **kwargs): """ """ @@ -477,33 +475,27 @@ class Grid(WuttaGrid): self.sorters[key] = self.make_sorter(*args, **kwargs) def set_filter(self, key, *args, **kwargs): - if len(args) == 1 and args[0] is None: - self.remove_filter(key) + """ """ + + if len(args) == 1: + if args[0] is None: + warnings.warn("specifying None is deprecated for Grid.set_filter(); " + "please use Grid.remove_filter() instead", + DeprecationWarning, stacklevel=2) + self.remove_filter(key) + else: + super().set_filter(key, args[0], **kwargs) + + elif len(args) == 0: + super().set_filter(key, **kwargs) + else: - if 'label' not in kwargs and key in self.labels: - kwargs['label'] = self.labels[key] + warnings.warn("multiple args are deprecated for Grid.set_filter(); " + "please refactor your code accordingly", + DeprecationWarning, stacklevel=2) + kwargs.setdefault('label', self.get_label(key)) self.filters[key] = self.make_filter(key, *args, **kwargs) - def remove_filter(self, key): - self.filters.pop(key, None) - - def set_label(self, key, label, column_only=False): - """ - Set/override the label for a column. - - This overrides - :meth:`~wuttaweb:wuttaweb.grids.base.Grid.set_label()` to add - the following params: - - :param column_only: Boolean indicating whether the label - should be applied *only* to the column header (if - ``True``), vs. applying also to the filter (if ``False``). - """ - super().set_label(key, label) - - if not column_only and key in self.filters: - self.filters[key].label = label - def set_click_handler(self, key, handler): if handler: self.click_handlers[key] = handler @@ -702,6 +694,14 @@ class Grid(WuttaGrid): def actions_column_format(self, column_number, row_number, item): return HTML.td(self.render_actions(item, row_number), class_='actions') + # TODO: upstream should handle this.. + def make_backend_filters(self, filters=None): + """ """ + final = self.get_default_filters() + if filters: + final.update(filters) + return final + def get_default_filters(self): """ Returns the default set of filters provided by the grid. @@ -726,16 +726,6 @@ class Grid(WuttaGrid): filters[prop.key] = self.make_filter(prop.key, column) return filters - def make_filters(self, filters=None): - """ - Returns an initial set of filters which will be available to the grid. - The grid itself may or may not provide some default filters, and the - ``filters`` kwarg may contain additions and/or overrides. - """ - if filters: - return filters - return self.get_default_filters() - def make_filter(self, key, column, **kwargs): """ Make a filter suitable for use with the given column. @@ -888,8 +878,8 @@ class Grid(WuttaGrid): # If request has filter settings, grab those, then grab sort/pager # settings from request or session. - elif self.filterable and self.request_has_settings('filter'): - self.update_filter_settings(settings, 'request') + elif self.request_has_settings('filter'): + self.update_filter_settings(settings, src='request') if self.request_has_settings('sort'): self.update_sort_settings(settings, src='request') else: @@ -901,7 +891,7 @@ class Grid(WuttaGrid): # settings from request or session. elif self.request_has_settings('sort'): self.update_sort_settings(settings, src='request') - self.update_filter_settings(settings, 'session') + self.update_filter_settings(settings, src='session') self.update_page_settings(settings) # NOTE: These next two are functionally equivalent, but are kept @@ -911,12 +901,12 @@ class Grid(WuttaGrid): # grab those, then grab filter/sort settings from session. elif self.request_has_settings('page'): self.update_page_settings(settings) - self.update_filter_settings(settings, 'session') + self.update_filter_settings(settings, src='session') self.update_sort_settings(settings, src='session') # If request has no settings, grab all from session. elif self.session_has_settings(): - self.update_filter_settings(settings, 'session') + self.update_filter_settings(settings, src='session') self.update_sort_settings(settings, src='session') self.update_page_settings(settings) @@ -1056,18 +1046,11 @@ class Grid(WuttaGrid): merge('page', int) def request_has_settings(self, type_): - """ - Determine if the current request (GET query string) contains any - filter/sort settings for the grid. - """ - if type_ == 'filter': - for filtr in self.iter_filters(): - if filtr.key in self.request.GET: - return True - if 'filter' in self.request.GET: # user may be applying empty filters - return True + """ """ + if super().request_has_settings(type_): + return True - elif type_ == 'sort': + if type_ == 'sort': # TODO: remove this eventually, but some links in the wild # may still include these params, so leave it for now @@ -1075,14 +1058,6 @@ class Grid(WuttaGrid): if key in self.request.GET: return True - if 'sort1key' in self.request.GET: - return True - - elif type_ == 'page': - for key in ['pagesize', 'page']: - if key in self.request.GET: - return True - return False def session_has_settings(self): @@ -1098,72 +1073,6 @@ class Grid(WuttaGrid): return any([key.startswith(f'{prefix}.filter') for key in self.request.session]) - def update_filter_settings(self, settings, source): - """ - Updates a settings dictionary according to filter settings data found - in either the GET query string, or session storage. - - :param settings: Dictionary of initial settings, which is to be updated. - - :param source: String identifying the source to consult for settings - data. Must be one of: ``('request', 'session')``. - """ - if not self.filterable: - return - - for filtr in self.iter_filters(): - prefix = 'filter.{}'.format(filtr.key) - - if source == 'request': - # consider filter active if query string contains a value for it - settings['{}.active'.format(prefix)] = filtr.key in self.request.GET - settings['{}.verb'.format(prefix)] = self.get_setting( - settings, f'{filtr.key}.verb', src='request', default='') - settings['{}.value'.format(prefix)] = self.get_setting( - settings, filtr.key, src='request', default='') - - else: # source = session - settings['{}.active'.format(prefix)] = self.get_setting( - settings, f'{prefix}.active', src='session', - normalize=lambda v: str(v).lower() == 'true', default=False) - settings['{}.verb'.format(prefix)] = self.get_setting( - settings, f'{prefix}.verb', src='session', default='') - settings['{}.value'.format(prefix)] = self.get_setting( - settings, f'{prefix}.value', src='session', default='') - - def update_page_settings(self, settings): - """ - Updates a settings dictionary according to pager settings data found in - either the GET query string, or session storage. - - Note that due to how the actual pager functions, the effective settings - will often come from *both* the request and session. This is so that - e.g. the page size will remain constant (coming from the session) while - the user jumps between pages (which only provides the single setting). - - :param settings: Dictionary of initial settings, which is to be updated. - """ - if not self.paginated: - return - - pagesize = self.request.GET.get('pagesize') - if pagesize is not None: - if pagesize.isdigit(): - settings['pagesize'] = int(pagesize) - else: - pagesize = self.request.session.get('grid.{}.pagesize'.format(self.key)) - if pagesize is not None: - settings['pagesize'] = pagesize - - page = self.request.GET.get('page') - if page is not None: - if page.isdigit(): - settings['page'] = int(page) - else: - page = self.request.session.get('grid.{}.page'.format(self.key)) - if page is not None: - settings['page'] = int(page) - def persist_settings(self, settings, dest='session'): """ """ if dest not in ('defaults', 'session'): @@ -1251,89 +1160,12 @@ class Grid(WuttaGrid): return data - def sort_data(self, data, sorters=None): - """ """ - if sorters is None: - sorters = self.active_sorters - if not sorters: - return data - - # nb. when data is a query, we want to apply sorters in the - # requested order, so the final query has order_by() in the - # correct "as-is" sequence. however when data is a list we - # must do the opposite, applying in the reverse order, so the - # final list has the most "important" sort(s) applied last. - if not isinstance(data, orm.Query): - sorters = reversed(sorters) - - for sorter in sorters: - sortkey = sorter['key'] - sortdir = sorter['dir'] - - # cannot sort unless we have a sorter callable - sortfunc = self.sorters.get(sortkey) - if not sortfunc: - return data - - # join appropriate model if needed - if sortkey in self.joiners and sortkey not in self.joined: - data = self.joiners[sortkey](data) - self.joined.add(sortkey) - - # invoke the sorter - data = sortfunc(data, sortdir) - - return data - - def paginate_data(self, data): - """ - Paginate the given data set according to current settings, and return - the result. - """ - # we of course assume our current page is correct, at first - pager = self.make_pager(data) - - # if pager has detected that our current page is outside the valid - # range, we must re-orient ourself around the "new" (valid) page - if pager.page != self.page: - self.page = pager.page - self.request.session['grid.{}.page'.format(self.key)] = self.page - pager = self.make_pager(data) - - return pager - - def make_pager(self, data): - - # TODO: this seems hacky..normally we expect `data` to be a - # query of course, but in some cases it may be a list instead. - # if so then we can't use ORM pager - if isinstance(data, list): - import paginate - return paginate.Page(data, - items_per_page=self.pagesize, - page=self.page) - - return SqlalchemyOrmPage(data, - items_per_page=self.pagesize, - page=self.page, - url_maker=URLMaker(self.request)) - def make_visible_data(self): - """ - Apply various settings to the raw data set, to produce a final data - set. This will page / sort / filter as necessary, according to the - grid's defaults and the current request etc. - """ - self.joined = set() - data = self.data - if self.filterable: - data = self.filter_data(data) - if self.sortable: - data = self.sort_data(data) - if self.paginated: - self.pager = self.paginate_data(data) - data = self.pager - return data + """ """ + warnings.warn("grid.make_visible_data() method is deprecated; " + "please use grid.get_visible_data() instead", + DeprecationWarning, stacklevel=2) + return self.get_visible_data() def render_vue_tag(self, master=None, **kwargs): """ """ @@ -1356,7 +1188,7 @@ class Grid(WuttaGrid): includes the context menu items and grid tools. """ if 'grid_columns' not in kwargs: - kwargs['grid_columns'] = self.get_table_columns() + kwargs['grid_columns'] = self.get_vue_columns() if 'grid_data' not in kwargs: kwargs['grid_data'] = self.get_table_data() @@ -1379,6 +1211,7 @@ class Grid(WuttaGrid): return HTML.literal(html) def render_buefy(self, **kwargs): + """ """ warnings.warn("Grid.render_buefy() is deprecated; " "please use Grid.render_complete() instead", DeprecationWarning, stacklevel=2) @@ -1568,23 +1401,19 @@ class Grid(WuttaGrid): def get_vue_columns(self): """ """ - return self.get_table_columns() + columns = super().get_vue_columns() + + for column in columns: + column['visible'] = column['field'] not in self.invisible + + return columns def get_table_columns(self): - """ - Return a list of dicts representing all grid columns. Meant - for use with the client-side JS table. - """ - columns = [] - for name in self.columns: - columns.append({ - 'field': name, - 'label': self.get_label(name), - 'sortable': self.is_sortable(name), - 'searchable': self.is_searchable(name), - 'visible': name not in self.invisible, - }) - return columns + """ """ + warnings.warn("grid.get_table_columns() method is deprecated; " + "please use grid.get_vue_columns() instead", + DeprecationWarning, stacklevel=2) + return self.get_vue_columns() def get_uuid_for_row(self, rowobj): @@ -1610,7 +1439,7 @@ class Grid(WuttaGrid): return self._table_data # filter / sort / paginate to get "visible" data - raw_data = self.make_visible_data() + raw_data = self.get_visible_data() data = [] status_map = {} checked = [] From b8131c83933f87eef5a05a08e919791233040b58 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 22 Aug 2024 13:49:57 -0500 Subject: [PATCH 19/85] fix: change grid reset-view param name to match wuttaweb --- tailbone/grids/core.py | 2 +- tailbone/templates/grids/complete.mako | 2 +- tailbone/views/master.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 969be50a..e58315d3 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -873,7 +873,7 @@ class Grid(WuttaGrid): # If request contains instruction to reset to default filters, then we # can skip the rest of the request/session checks. - if self.request.GET.get('reset-to-default-filters') == 'true': + if self.request.GET.get('reset-view'): pass # If request has filter settings, grab those, then grab sort/pager diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index 54ad0527..49758275 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -683,7 +683,7 @@ this.loading = true // use current url proper, plus reset param - let url = '?reset-to-default-filters=true' + let url = '?reset-view=true' // add current hash, to preserve that in redirect if (location.hash) { diff --git a/tailbone/views/master.py b/tailbone/views/master.py index e4d6c3f6..c53fd8b4 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -335,7 +335,7 @@ class MasterView(View): # If user just refreshed the page with a reset instruction, issue a # redirect in order to clear out the query string. - if self.request.GET.get('reset-to-default-filters') == 'true': + if self.request.GET.get('reset-view'): kw = {'_query': None} hash_ = self.request.GET.get('hash') if hash_: @@ -1184,7 +1184,7 @@ class MasterView(View): # If user just refreshed the page with a reset instruction, issue a # redirect in order to clear out the query string. - if self.request.GET.get('reset-to-default-filters') == 'true': + if self.request.GET.get('reset-view'): kw = {'_query': None} hash_ = self.request.GET.get('hash') if hash_: From 8d5427e92f9fe272ad1ceb4a6a1b5b0c3cd4ef27 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 22 Aug 2024 14:53:59 -0500 Subject: [PATCH 20/85] =?UTF-8?q?bump:=20version=200.20.1=20=E2=86=92=200.?= =?UTF-8?q?21.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 19 +++++++++++++++++++ pyproject.toml | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2b348a..c54d5642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.0 (2024-08-22) + +### Feat + +- move "most" filtering logic for grid class to wuttaweb +- inherit from wuttaweb templates for home, login pages +- inherit from wuttaweb for AppInfoView, appinfo/configure template +- add "has output file templates" config option for master view + +### Fix + +- change grid reset-view param name to match wuttaweb +- move "searchable columns" grid feature to wuttaweb +- use wuttaweb to get/render csrf token +- inherit from wuttaweb for appinfo/index template +- prefer wuttaweb config for "home redirect to login" feature +- fix master/index template rendering for waterpark theme +- fix spacing for navbar logo/title in waterpark theme + ## v0.20.1 (2024-08-20) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 90ecd953..613d3272 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.20.1" +version = "0.21.0" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -53,13 +53,13 @@ dependencies = [ "pyramid_mako", "pyramid_retry", "pyramid_tm", - "rattail[db,bouncer]>=0.18.1", + "rattail[db,bouncer]>=0.18.4", "sa-filters", "simplejson", "transaction", "waitress", "WebHelpers2", - "WuttaWeb>=0.11.0", + "WuttaWeb>=0.12.0", "zope.sqlalchemy>=1.5", ] From f292850d05c7f83334cd2f4156264112e01a4377 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 22 Aug 2024 14:57:39 -0500 Subject: [PATCH 21/85] test: fix some tests --- tests/grids/test_core.py | 2 +- tests/views/wutta/test_people.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index 5169e599..4d143c85 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -135,7 +135,7 @@ class TestGrid(WebTestCase): def test_set_label(self): model = self.app.model - grid = self.make_grid(model_class=model.Setting) + grid = self.make_grid(model_class=model.Setting, filterable=True) self.assertEqual(grid.labels, {}) # basic diff --git a/tests/views/wutta/test_people.py b/tests/views/wutta/test_people.py index f178a64f..31aeb501 100644 --- a/tests/views/wutta/test_people.py +++ b/tests/views/wutta/test_people.py @@ -38,7 +38,7 @@ class TestPersonView(WebTestCase): def test_configure_form(self): model = self.app.model - barney = model.User(username='barney') + barney = model.Person(display_name="Barney Rubble") self.session.add(barney) self.session.commit() view = self.make_view() From 7b40c527c860e95be4dd74e09b2344b672110d98 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 22 Aug 2024 15:14:11 -0500 Subject: [PATCH 22/85] fix: misc. bugfixes per recent changes --- tailbone/grids/core.py | 23 +++++++++-------------- tailbone/views/email.py | 11 +++++------ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index e58315d3..754868bc 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -260,6 +260,9 @@ class Grid(WuttaGrid): # reference grid.vue_component etc. kwargs.setdefault('vue_tagname', 'tailbone-grid') + # nb. these must be set before super init, as they are + # referenced when constructing filters + self.assume_local_times = assume_local_times self.use_byte_string_filters = use_byte_string_filters kwargs['key'] = key @@ -279,7 +282,6 @@ class Grid(WuttaGrid): self.width = width self.enums = enums or {} - self.assume_local_times = assume_local_times self.renderers = self.make_default_renderers(self.renderers) self.raw_renderers = raw_renderers or {} self.invisible = invisible or [] @@ -476,25 +478,18 @@ class Grid(WuttaGrid): def set_filter(self, key, *args, **kwargs): """ """ - if len(args) == 1: if args[0] is None: warnings.warn("specifying None is deprecated for Grid.set_filter(); " "please use Grid.remove_filter() instead", DeprecationWarning, stacklevel=2) self.remove_filter(key) - else: - super().set_filter(key, args[0], **kwargs) + return - elif len(args) == 0: - super().set_filter(key, **kwargs) - - else: - warnings.warn("multiple args are deprecated for Grid.set_filter(); " - "please refactor your code accordingly", - DeprecationWarning, stacklevel=2) - kwargs.setdefault('label', self.get_label(key)) - self.filters[key] = self.make_filter(key, *args, **kwargs) + # TODO: our make_filter() signature differs from upstream, + # so must call it explicitly instead of delegating to super + kwargs.setdefault('label', self.get_label(key)) + self.filters[key] = self.make_filter(key, *args, **kwargs) def set_click_handler(self, key, handler): if handler: @@ -1230,7 +1225,7 @@ class Grid(WuttaGrid): context['data_prop'] = data_prop context['empty_labels'] = empty_labels if 'grid_columns' not in context: - context['grid_columns'] = self.get_table_columns() + context['grid_columns'] = self.get_vue_columns() context.setdefault('paginated', False) if context['paginated']: context.setdefault('per_page', 20) diff --git a/tailbone/views/email.py b/tailbone/views/email.py index a99e8553..98bd4295 100644 --- a/tailbone/views/email.py +++ b/tailbone/views/email.py @@ -116,11 +116,12 @@ class EmailSettingView(MasterView): return data def configure_grid(self, g): - g.sorters['key'] = g.make_simple_sorter('key', foldcase=True) - g.sorters['prefix'] = g.make_simple_sorter('prefix', foldcase=True) - g.sorters['subject'] = g.make_simple_sorter('subject', foldcase=True) - g.sorters['enabled'] = g.make_simple_sorter('enabled') + super().configure_grid(g) + + g.sort_on_backend = False + g.sort_multiple = False g.set_sort_defaults('key') + g.set_type('enabled', 'boolean') g.set_link('key') g.set_link('subject') @@ -130,11 +131,9 @@ class EmailSettingView(MasterView): # to g.set_renderer('to', self.render_to_short) - g.sorters['to'] = g.make_simple_sorter('to', foldcase=True) # hidden if self.has_perm('configure'): - g.sorters['hidden'] = g.make_simple_sorter('hidden') g.set_type('hidden', 'boolean') else: g.remove('hidden') From 7d6f75bb05bbbe2345e0f220f9c7a536c8f119e3 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 22 Aug 2024 15:33:28 -0500 Subject: [PATCH 23/85] =?UTF-8?q?bump:=20version=200.21.0=20=E2=86=92=200.?= =?UTF-8?q?21.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c54d5642..3bcbc6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.1 (2024-08-22) + +### Fix + +- misc. bugfixes per recent changes + ## v0.21.0 (2024-08-22) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 613d3272..2db880ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.0" +version = "0.21.1" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From c176d978701648904c1cd00725cf9057fafbe26e Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 22 Aug 2024 15:54:15 -0500 Subject: [PATCH 24/85] fix: avoid deprecated `component` form kwarg --- tailbone/views/batch/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index 5dd7b548..8ee3a37d 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -861,7 +861,7 @@ class BatchMasterView(MasterView): if not schema: schema = colander.Schema() - kwargs['component'] = 'execute-form' + kwargs['vue_tagname'] = 'execute-form' form = forms.Form(schema=schema, request=self.request, defaults=defaults, **kwargs) self.configure_execute_form(form) return form From 4c3e3aeb6a70ae45eb16a90cc53c1af336e6d083 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 22 Aug 2024 17:09:58 -0500 Subject: [PATCH 25/85] fix: various fixes for waterpark theme --- tailbone/templates/base.mako | 2 +- tailbone/templates/themes/waterpark/base.mako | 83 +++++++++++++++++++ tailbone/templates/themes/waterpark/form.mako | 8 ++ 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index eb950011..c01b3b37 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -668,7 +668,7 @@ text="Edit This"> </once-button> % endif - % if getattr(master, 'cloneable', False) and master.has_perm('clone'): + % if getattr(master, 'cloneable', False) and not master.cloning and master.has_perm('clone'): <once-button tag="a" href="${master.get_action_url('clone', instance)}" icon-left="object-ungroup" text="Clone This"> diff --git a/tailbone/templates/themes/waterpark/base.mako b/tailbone/templates/themes/waterpark/base.mako index 878090dc..520e18ce 100644 --- a/tailbone/templates/themes/waterpark/base.mako +++ b/tailbone/templates/themes/waterpark/base.mako @@ -7,6 +7,7 @@ <%def name="base_styles()"> ${parent.base_styles()} + ${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css') + '?ver={}'.format(tailbone.__version__))} <style> .filters .filter-fieldname .field, @@ -171,6 +172,88 @@ % endif </%def> +<%def name="render_crud_header_buttons()"> + % if master: + % if master.viewing: + % if instance_editable and master.has_perm('edit'): + <wutta-button once + tag="a" href="${master.get_action_url('edit', instance)}" + icon-left="edit" + label="Edit This" /> + % endif + % if getattr(master, 'cloneable', False) and not master.cloning and master.has_perm('clone'): + <wutta-button once + tag="a" href="${master.get_action_url('clone', instance)}" + icon-left="object-ungroup" + label="Clone This" /> + % endif + % if instance_deletable and master.has_perm('delete'): + <wutta-button once type="is-danger" + tag="a" href="${master.get_action_url('delete', instance)}" + icon-left="trash" + label="Delete This" /> + % endif + % elif master.editing: + % if master.has_perm('view'): + <wutta-button once + tag="a" href="${master.get_action_url('view', instance)}" + icon-left="eye" + label="View This" /> + % endif + % if instance_deletable and master.has_perm('delete'): + <wutta-button once type="is-danger" + tag="a" href="${master.get_action_url('delete', instance)}" + icon-left="trash" + label="Delete This" /> + % endif + % elif master.deleting: + % if master.has_perm('view'): + <wutta-button once + tag="a" href="${master.get_action_url('view', instance)}" + icon-left="eye" + label="View This" /> + % endif + % if instance_editable and master.has_perm('edit'): + <wutta-button once + tag="a" href="${master.get_action_url('edit', instance)}" + icon-left="edit" + label="Edit This" /> + % endif + % endif + % endif +</%def> + +<%def name="render_prevnext_header_buttons()"> + % if show_prev_next is not Undefined and show_prev_next: + % if prev_url: + <wutta-button once + tag="a" href="${prev_url}" + icon-left="arrow-left" + label="Older" /> + % else: + <b-button tag="a" href="#" + disabled + icon-pack="fas" + icon-left="arrow-left"> + Older + </b-button> + % endif + % if next_url: + <wutta-button once + tag="a" href="${next_url}" + icon-left="arrow-right" + label="Newer" /> + % else: + <b-button tag="a" href="#" + disabled + icon-pack="fas" + icon-left="arrow-right"> + Newer + </b-button> + % endif + % endif +</%def> + <%def name="render_this_page_component()"> <this-page @change-content-title="changeContentTitle" % if can_edit_help: diff --git a/tailbone/templates/themes/waterpark/form.mako b/tailbone/templates/themes/waterpark/form.mako index cf1ddb8a..f88d6821 100644 --- a/tailbone/templates/themes/waterpark/form.mako +++ b/tailbone/templates/themes/waterpark/form.mako @@ -1,2 +1,10 @@ ## -*- coding: utf-8; -*- <%inherit file="wuttaweb:templates/form.mako" /> + +<%def name="render_vue_template_form()"> + % if form is not Undefined: + ${form.render_vue_template(buttons=capture(self.render_form_buttons))} + % endif +</%def> + +<%def name="render_form_buttons()"></%def> From 29531c83c4b785e2ef7b5c4006bd4c86c7b5f045 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 22 Aug 2024 19:21:48 -0500 Subject: [PATCH 26/85] fix: some fixes for wutta people view --- tailbone/grids/core.py | 35 +++++++++++++++++++++++++--------- tailbone/views/master.py | 6 ++++-- tailbone/views/wutta/people.py | 12 +++++++++++- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 754868bc..afd6e11b 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -24,9 +24,10 @@ Core Grid Classes """ -from urllib.parse import urlencode -import warnings +import inspect import logging +import warnings +from urllib.parse import urlencode import sqlalchemy as sa from sqlalchemy import orm @@ -858,9 +859,13 @@ class Grid(WuttaGrid): settings['page'] = self.page if self.filterable: for filtr in self.iter_filters(): - settings['filter.{}.active'.format(filtr.key)] = filtr.default_active - settings['filter.{}.verb'.format(filtr.key)] = filtr.default_verb - settings['filter.{}.value'.format(filtr.key)] = filtr.default_value + defaults = self.filter_defaults.get(filtr.key, {}) + settings[f'filter.{filtr.key}.active'] = defaults.get('active', + filtr.default_active) + settings[f'filter.{filtr.key}.verb'] = defaults.get('verb', + filtr.default_verb) + settings[f'filter.{filtr.key}.value'] = defaults.get('value', + filtr.default_value) # If user has default settings on file, apply those first. if self.user_has_defaults(): @@ -1239,7 +1244,7 @@ class Grid(WuttaGrid): view = None for action in self.actions: if action.key == 'view': - return action.click_handler + return getattr(action, 'click_handler', None) def set_filters_sequence(self, filters, only=False): """ @@ -1475,10 +1480,22 @@ class Grid(WuttaGrid): # leverage configured rendering logic where applicable; # otherwise use "raw" data value as string + value = self.obtain_value(rowobj, name) if self.renderers and name in self.renderers: - value = self.renderers[name](rowobj, name) - else: - value = self.obtain_value(rowobj, name) + renderer = self.renderers[name] + + # TODO: legacy renderer callables require 2 args, + # but wuttaweb callables require 3 args + sig = inspect.signature(renderer) + required = [param for param in sig.parameters.values() + if param.default == param.empty] + + if len(required) == 2: + # TODO: legacy renderer + value = renderer(rowobj, name) + else: # the future + value = renderer(rowobj, name, value) + if value is None: value = "" diff --git a/tailbone/views/master.py b/tailbone/views/master.py index c53fd8b4..1028ff27 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -612,7 +612,9 @@ class MasterView(View): # delete action if self.rows_deletable and self.has_perm('delete_row'): - actions.append(self.make_action('delete', icon='trash', url=self.row_delete_action_url)) + actions.append(self.make_action('delete', icon='trash', + url=self.row_delete_action_url, + link_class='has-text-danger')) defaults['delete_speedbump'] = self.rows_deletable_speedbump defaults['actions'] = actions @@ -3322,7 +3324,7 @@ class MasterView(View): url=self.default_clone_url) def make_grid_action_delete(self): - kwargs = {} + kwargs = {'link_class': 'has-text-danger'} if self.delete_confirm == 'simple': kwargs['click_handler'] = 'deleteObject' return self.make_action('delete', icon='trash', url=self.default_delete_url, **kwargs) diff --git a/tailbone/views/wutta/people.py b/tailbone/views/wutta/people.py index 968eaf3d..bd96bd4d 100644 --- a/tailbone/views/wutta/people.py +++ b/tailbone/views/wutta/people.py @@ -32,6 +32,7 @@ from wuttaweb.views import people as wutta from tailbone.views import people as tailbone from tailbone.db import Session from rattail.db.model import Person +from tailbone.grids import Grid class PersonView(wutta.PersonView): @@ -44,7 +45,6 @@ class PersonView(wutta.PersonView): """ model_class = Person Session = Session - sort_defaults = 'display_name' labels = { 'display_name': "Full Name", @@ -59,6 +59,11 @@ class PersonView(wutta.PersonView): 'merge_requested', ] + filter_defaults = { + 'display_name': {'active': True, 'verb': 'contains'}, + } + sort_defaults = 'display_name' + form_fields = [ 'first_name', 'middle_name', @@ -74,6 +79,11 @@ class PersonView(wutta.PersonView): # CRUD methods ############################## + # TODO: must use older grid for now, to render filters correctly + def make_grid(self, **kwargs): + """ """ + return Grid(self.request, **kwargs) + def configure_grid(self, g): """ """ super().configure_grid(g) From cea3e4b927eab7114dd0548d6216df8c33dd37a4 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 22 Aug 2024 19:40:21 -0500 Subject: [PATCH 27/85] fix: add basic wutta view for users just proving concepts still at this point..nothing reliable --- tailbone/templates/base.mako | 6 +++- tailbone/views/users.py | 6 +++- tailbone/views/wutta/users.py | 57 +++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 tailbone/views/wutta/users.py diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index c01b3b37..86b1ba1d 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -642,7 +642,11 @@ % if request.is_root or not request.user.prevent_password_change: ${h.link_to("Change Password", url('change_password'), class_='navbar-item')} % endif - ${h.link_to("Edit Preferences", url('my.preferences'), class_='navbar-item')} + % try: + ## nb. does not exist yet for wuttaweb + ${h.link_to("Edit Preferences", url('my.preferences'), class_='navbar-item')} + % except: + % endtry ${h.link_to("Logout", url('logout'), class_='navbar-item')} </div> </div> diff --git a/tailbone/views/users.py b/tailbone/views/users.py index 9b533efe..dfed0a11 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -801,4 +801,8 @@ def defaults(config, **kwargs): def includeme(config): - defaults(config) + wutta_config = config.registry.settings['wutta_config'] + if wutta_config.get_bool('tailbone.use_wutta_views', default=False, usedb=False): + config.include('tailbone.views.wutta.users') + else: + defaults(config) diff --git a/tailbone/views/wutta/users.py b/tailbone/views/wutta/users.py new file mode 100644 index 00000000..3c3f8d52 --- /dev/null +++ b/tailbone/views/wutta/users.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2024 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail 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 General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see <http://www.gnu.org/licenses/>. +# +################################################################################ +""" +User Views +""" + +from wuttaweb.views import users as wutta +from tailbone.views import users as tailbone +from tailbone.db import Session +from rattail.db.model import User +from tailbone.grids import Grid + + +class UserView(wutta.UserView): + """ + This is the first attempt at blending newer Wutta views with + legacy Tailbone config. + + So, this is a Wutta-based view but it should be included by a + Tailbone app configurator. + """ + model_class = User + Session = Session + + # TODO: must use older grid for now, to render filters correctly + def make_grid(self, **kwargs): + """ """ + return Grid(self.request, **kwargs) + + +def defaults(config, **kwargs): + kwargs.setdefault('UserView', UserView) + tailbone.defaults(config, **kwargs) + + +def includeme(config): + defaults(config) From 37f760959d277c2fe158c500c65684fb5af49102 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 22 Aug 2024 19:58:27 -0500 Subject: [PATCH 28/85] fix: merge filters into main grid template to better match wuttaweb --- tailbone/grids/core.py | 22 --------- tailbone/templates/grids/complete.mako | 66 ++++++++++++++++++++++++- tailbone/templates/grids/filters.mako | 67 -------------------------- 3 files changed, 64 insertions(+), 91 deletions(-) delete mode 100644 tailbone/templates/grids/filters.mako diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index afd6e11b..12e45aec 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -1318,28 +1318,6 @@ class Grid(WuttaGrid): return data - def render_filters(self, template='/grids/filters.mako', **kwargs): - """ - Render the filters to a Unicode string, using the specified template. - Additional kwargs are passed along as context to the template. - """ - # Provide default data to filters form, so renderer can do some of the - # work for us. - data = {} - for filtr in self.iter_active_filters(): - data['{}.active'.format(filtr.key)] = filtr.active - data['{}.verb'.format(filtr.key)] = filtr.verb - data[filtr.key] = filtr.value - - form = gridfilters.GridFiltersForm(self.filters, - request=self.request, - defaults=data) - - kwargs['request'] = self.request - kwargs['grid'] = self - kwargs['form'] = form - return render(template, kwargs) - def render_actions(self, row, i): # pragma: no cover """ """ warnings.warn("grid.render_actions() is deprecated!", diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index 49758275..f5d1da95 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -10,8 +10,70 @@ <div style="display: flex; flex-direction: column; justify-content: end;"> <div class="filters"> % if getattr(grid, 'filterable', False): - ## TODO: stop using |n filter - ${grid.render_filters(allow_save_defaults=allow_save_defaults)|n} + <form method="GET" @submit.prevent="applyFilters()"> + + <div style="display: flex; flex-direction: column; gap: 0.5rem;"> + <grid-filter v-for="key in filtersSequence" + :key="key" + :filter="filters[key]" + ref="gridFilters"> + </grid-filter> + </div> + + <div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;"> + + <b-button type="is-primary" + native-type="submit" + icon-pack="fas" + icon-left="check"> + Apply Filters + </b-button> + + <b-button v-if="!addFilterShow" + icon-pack="fas" + icon-left="plus" + @click="addFilterInit()"> + Add Filter + </b-button> + + <b-autocomplete v-if="addFilterShow" + ref="addFilterAutocomplete" + :data="addFilterChoices" + v-model="addFilterTerm" + placeholder="Add Filter" + field="key" + :custom-formatter="formatAddFilterItem" + open-on-focus + keep-first + icon-pack="fas" + clearable + clear-on-select + @select="addFilterSelect"> + </b-autocomplete> + + <b-button @click="resetView()" + icon-pack="fas" + icon-left="home"> + Default View + </b-button> + + <b-button @click="clearFilters()" + icon-pack="fas" + icon-left="trash"> + No Filters + </b-button> + + % if allow_save_defaults and request.user: + <b-button @click="saveDefaults()" + icon-pack="fas" + icon-left="save" + :disabled="savingDefaults"> + {{ savingDefaults ? "Working, please wait..." : "Save Defaults" }} + </b-button> + % endif + + </div> + </form> % endif </div> </div> diff --git a/tailbone/templates/grids/filters.mako b/tailbone/templates/grids/filters.mako deleted file mode 100644 index 9a80b911..00000000 --- a/tailbone/templates/grids/filters.mako +++ /dev/null @@ -1,67 +0,0 @@ -## -*- coding: utf-8; -*- - -<form action="${form.action_url}" method="GET" @submit.prevent="applyFilters()"> - - <div style="display: flex; flex-direction: column; gap: 0.5rem;"> - <grid-filter v-for="key in filtersSequence" - :key="key" - :filter="filters[key]" - ref="gridFilters"> - </grid-filter> - </div> - - <div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;"> - - <b-button type="is-primary" - native-type="submit" - icon-pack="fas" - icon-left="check"> - Apply Filters - </b-button> - - <b-button v-if="!addFilterShow" - icon-pack="fas" - icon-left="plus" - @click="addFilterInit()"> - Add Filter - </b-button> - - <b-autocomplete v-if="addFilterShow" - ref="addFilterAutocomplete" - :data="addFilterChoices" - v-model="addFilterTerm" - placeholder="Add Filter" - field="key" - :custom-formatter="formatAddFilterItem" - open-on-focus - keep-first - icon-pack="fas" - clearable - clear-on-select - @select="addFilterSelect"> - </b-autocomplete> - - <b-button @click="resetView()" - icon-pack="fas" - icon-left="home"> - Default View - </b-button> - - <b-button @click="clearFilters()" - icon-pack="fas" - icon-left="trash"> - No Filters - </b-button> - - % if allow_save_defaults and request.user: - <b-button @click="saveDefaults()" - icon-pack="fas" - icon-left="save" - :disabled="savingDefaults"> - {{ savingDefaults ? "Working, please wait..." : "Save Defaults" }} - </b-button> - % endif - - </div> - -</form> From c1a2c9cc70b36044fb7a82bedf3d5cd59f5cd487 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Fri, 23 Aug 2024 14:14:03 -0500 Subject: [PATCH 29/85] fix: tweak how grid data translates to Vue template context per wuttaweb changes --- tailbone/grids/core.py | 6 ++++++ tailbone/templates/grids/complete.mako | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 12e45aec..ecf462fd 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -1403,6 +1403,10 @@ class Grid(WuttaGrid): if hasattr(rowobj, 'uuid'): return rowobj.uuid + def get_vue_context(self): + """ """ + return self.get_table_data() + def get_vue_data(self): """ """ table_data = self.get_table_data() @@ -1506,6 +1510,8 @@ class Grid(WuttaGrid): results = { 'data': data, + 'row_classes': status_map, + # TODO: deprecate / remove this 'row_status_map': status_map, } diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index f5d1da95..60f9a3b8 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -311,7 +311,8 @@ <script type="text/javascript"> - let ${grid.vue_component}CurrentData = ${json.dumps(grid.get_vue_data())|n} + const ${grid.vue_component}Context = ${json.dumps(grid.get_vue_context())|n} + let ${grid.vue_component}CurrentData = ${grid.vue_component}Context.data let ${grid.vue_component}Data = { loading: false, From b7991b5dc61ff40e268f69be269adacb931519a0 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Fri, 23 Aug 2024 16:18:17 -0500 Subject: [PATCH 30/85] fix: fix input/output file upload feature for configure pages, per oruga --- tailbone/templates/configure.mako | 170 ++++++++++++++++++------------ 1 file changed, 101 insertions(+), 69 deletions(-) diff --git a/tailbone/templates/configure.mako b/tailbone/templates/configure.mako index 6d9c2261..463d48b1 100644 --- a/tailbone/templates/configure.mako +++ b/tailbone/templates/configure.mako @@ -92,7 +92,7 @@ <b-select name="${tmpl['setting_file']}" v-model="inputFileTemplateSettings['${tmpl['setting_file']}']" @input="settingsNeedSaved = true"> - <option :value="null">-new-</option> + <option value="">-new-</option> <option v-for="option in inputFileTemplateFileOptions['${tmpl['key']}']" :key="option" :value="option"> @@ -104,22 +104,40 @@ <b-field label="Upload" v-show="inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !inputFileTemplateSettings['${tmpl['setting_file']}']"> - <b-field class="file is-primary" - :class="{'has-name': !!inputFileTemplateSettings['${tmpl['setting_file']}']}"> - <b-upload name="${tmpl['setting_file']}.upload" - v-model="inputFileTemplateUploads['${tmpl['key']}']" - class="file-label" - @input="settingsNeedSaved = true"> - <span class="file-cta"> - <b-icon class="file-icon" pack="fas" icon="upload"></b-icon> - <span class="file-label">Click to upload</span> - </span> - </b-upload> - <span v-if="inputFileTemplateUploads['${tmpl['key']}']" - class="file-name"> - {{ inputFileTemplateUploads['${tmpl['key']}'].name }} - </span> - </b-field> + % if request.use_oruga: + <o-field class="file"> + <o-upload name="${tmpl['setting_file']}.upload" + v-model="inputFileTemplateUploads['${tmpl['key']}']" + v-slot="{ onclick }" + @input="settingsNeedSaved = true"> + <o-button variant="primary" + @click="onclick"> + <o-icon icon="upload" /> + <span>Click to upload</span> + </o-button> + <span class="file-name" v-if="inputFileTemplateUploads['${tmpl['key']}']"> + {{ inputFileTemplateUploads['${tmpl['key']}'].name }} + </span> + </o-upload> + </o-field> + % else: + <b-field class="file is-primary" + :class="{'has-name': !!inputFileTemplateSettings['${tmpl['setting_file']}']}"> + <b-upload name="${tmpl['setting_file']}.upload" + v-model="inputFileTemplateUploads['${tmpl['key']}']" + class="file-label" + @input="settingsNeedSaved = true"> + <span class="file-cta"> + <b-icon class="file-icon" pack="fas" icon="upload"></b-icon> + <span class="file-label">Click to upload</span> + </span> + </b-upload> + <span v-if="inputFileTemplateUploads['${tmpl['key']}']" + class="file-name"> + {{ inputFileTemplateUploads['${tmpl['key']}'].name }} + </span> + </b-field> + % endif </b-field> @@ -162,7 +180,7 @@ <b-select name="${tmpl['setting_file']}" v-model="outputFileTemplateSettings['${tmpl['setting_file']}']" @input="settingsNeedSaved = true"> - <option :value="null">-new-</option> + <option value="">-new-</option> <option v-for="option in outputFileTemplateFileOptions['${tmpl['key']}']" :key="option" :value="option"> @@ -174,23 +192,40 @@ <b-field label="Upload" v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !outputFileTemplateSettings['${tmpl['setting_file']}']"> - <b-field class="file is-primary" - :class="{'has-name': !!outputFileTemplateSettings['${tmpl['setting_file']}']}"> - <b-upload name="${tmpl['setting_file']}.upload" - v-model="outputFileTemplateUploads['${tmpl['key']}']" - class="file-label" - @input="settingsNeedSaved = true"> - <span class="file-cta"> - <b-icon class="file-icon" pack="fas" icon="upload"></b-icon> - <span class="file-label">Click to upload</span> - </span> - </b-upload> - <span v-if="outputFileTemplateUploads['${tmpl['key']}']" - class="file-name"> - {{ outputFileTemplateUploads['${tmpl['key']}'].name }} - </span> - </b-field> - + % if request.use_oruga: + <o-field class="file"> + <o-upload name="${tmpl['setting_file']}.upload" + v-model="outputFileTemplateUploads['${tmpl['key']}']" + v-slot="{ onclick }" + @input="settingsNeedSaved = true"> + <o-button variant="primary" + @click="onclick"> + <o-icon icon="upload" /> + <span>Click to upload</span> + </o-button> + <span class="file-name" v-if="outputFileTemplateUploads['${tmpl['key']}']"> + {{ outputFileTemplateUploads['${tmpl['key']}'].name }} + </span> + </o-upload> + </o-field> + % else: + <b-field class="file is-primary" + :class="{'has-name': !!outputFileTemplateSettings['${tmpl['setting_file']}']}"> + <b-upload name="${tmpl['setting_file']}.upload" + v-model="outputFileTemplateUploads['${tmpl['key']}']" + class="file-label" + @input="settingsNeedSaved = true"> + <span class="file-cta"> + <b-icon class="file-icon" pack="fas" icon="upload"></b-icon> + <span class="file-label">Click to upload</span> + </span> + </b-upload> + <span v-if="outputFileTemplateUploads['${tmpl['key']}']" + class="file-name"> + {{ outputFileTemplateUploads['${tmpl['key']}'].name }} + </span> + </b-field> + % endif </b-field> </b-field> @@ -275,16 +310,6 @@ ThisPageData.simpleSettings = ${json.dumps(simple_settings)|n} % endif - % if input_file_template_settings is not Undefined: - ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n} - ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n} - ThisPageData.inputFileTemplateUploads = { - % for key in input_file_templates: - '${key}': null, - % endfor - } - % endif - ThisPageData.purgeSettingsShowDialog = false ThisPageData.purgingSettings = false @@ -297,30 +322,7 @@ this.purgeSettingsShowDialog = true } - % if input_file_template_settings is not Undefined: - ThisPage.methods.validateInputFileTemplateSettings = function() { - % for tmpl in input_file_templates.values(): - if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') { - if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) { - if (!this.inputFileTemplateUploads['${tmpl['key']}']) { - return "You must provide a file to upload for the ${tmpl['label']} template." - } - } - } - % endfor - } - % endif - - ThisPage.methods.validateSettings = function() { - let msg - - % if input_file_template_settings is not Undefined: - msg = this.validateInputFileTemplateSettings() - if (msg) { - return msg - } - % endif - } + ThisPage.methods.validateSettings = function() {} ThisPage.methods.saveSettings = function() { let msg @@ -366,6 +368,36 @@ window.addEventListener('beforeunload', this.beforeWindowUnload) } + ############################## + ## input file templates + ############################## + + % if input_file_template_settings is not Undefined: + + ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n} + ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n} + ThisPageData.inputFileTemplateUploads = { + % for key in input_file_templates: + '${key}': null, + % endfor + } + + ThisPage.methods.validateInputFileTemplateSettings = function() { + % for tmpl in input_file_templates.values(): + if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') { + if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) { + if (!this.inputFileTemplateUploads['${tmpl['key']}']) { + return "You must provide a file to upload for the ${tmpl['label']} template." + } + } + } + % endfor + } + + ThisPageData.validators.push(ThisPage.methods.validateInputFileTemplateSettings) + + % endif + ############################## ## output file templates ############################## From d1f4c0f150f51b1fde0bdbdffa5a11d489f4ec9a Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 26 Aug 2024 14:54:45 -0500 Subject: [PATCH 31/85] fix: refactor waterpark base template to use wutta feedback component although for now we still provide the template and add reply-to --- tailbone/templates/themes/waterpark/base.mako | 277 +++++++----------- 1 file changed, 105 insertions(+), 172 deletions(-) diff --git a/tailbone/templates/themes/waterpark/base.mako b/tailbone/templates/themes/waterpark/base.mako index 520e18ce..774479ba 100644 --- a/tailbone/templates/themes/waterpark/base.mako +++ b/tailbone/templates/themes/waterpark/base.mako @@ -164,12 +164,7 @@ /> </div> - % if request.has_perm('common.feedback'): - <feedback-form - action="${url('feedback')}" - :message="feedbackMessage"> - </feedback-form> - % endif + ${parent.render_feedback_button()} </%def> <%def name="render_crud_header_buttons()"> @@ -262,174 +257,133 @@ /> </%def> -<%def name="render_vue_templates()"> - ${parent.render_vue_templates()} +<%def name="render_vue_template_feedback()"> + <script type="text/x-template" id="feedback-template"> + <div> - ${page_help.render_template()} - ${page_help.declare_vars()} + <div class="level-item"> + <b-button type="is-primary" + @click="showFeedback()" + icon-pack="fas" + icon-left="comment"> + Feedback + </b-button> + </div> - % if request.has_perm('common.feedback'): - <script type="text/x-template" id="feedback-template"> - <div> + <b-modal has-modal-card + :active.sync="showDialog"> + <div class="modal-card"> - <div class="level-item"> - <b-button type="is-primary" - @click="showFeedback()" - icon-pack="fas" - icon-left="comment"> - Feedback - </b-button> - </div> + <header class="modal-card-head"> + <p class="modal-card-title">User Feedback</p> + </header> - <b-modal has-modal-card - :active.sync="showDialog"> - <div class="modal-card"> + <section class="modal-card-body"> + <p class="block"> + Questions, suggestions, comments, complaints, etc. + <span class="red">regarding this website</span> are + welcome and may be submitted below. + </p> - <header class="modal-card-head"> - <p class="modal-card-title">User Feedback</p> - </header> + <b-field label="User Name"> + <b-input v-model="userName" + % if request.user: + disabled + % endif + > + </b-input> + </b-field> - <section class="modal-card-body"> - <p class="block"> - Questions, suggestions, comments, complaints, etc. - <span class="red">regarding this website</span> are - welcome and may be submitted below. - </p> + <b-field label="Referring URL"> + <b-input + v-model="referrer" + disabled="true"> + </b-input> + </b-field> - <b-field label="User Name"> - <b-input v-model="userName" - % if request.user: - disabled - % endif - > - </b-input> - </b-field> + <b-field label="Message"> + <b-input type="textarea" + v-model="message" + ref="textarea"> + </b-input> + </b-field> - <b-field label="Referring URL"> - <b-input - v-model="referrer" - disabled="true"> - </b-input> - </b-field> - - <b-field label="Message"> - <b-input type="textarea" - v-model="message" - ref="textarea"> - </b-input> - </b-field> - - % if config.get_bool('tailbone.feedback_allows_reply'): - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <b-checkbox v-model="pleaseReply" - @input="pleaseReplyChanged"> - Please email me back{{ pleaseReply ? " at: " : "" }} - </b-checkbox> - </div> - <div class="level-item" v-show="pleaseReply"> - <b-input v-model="userEmail" - ref="userEmail"> - </b-input> - </div> - </div> + % if config.get_bool('tailbone.feedback_allows_reply'): + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <b-checkbox v-model="pleaseReply" + @input="pleaseReplyChanged"> + Please email me back{{ pleaseReply ? " at: " : "" }} + </b-checkbox> </div> - % endif + <div class="level-item" v-show="pleaseReply"> + <b-input v-model="userEmail" + ref="userEmail"> + </b-input> + </div> + </div> + </div> + % endif - </section> - - <footer class="modal-card-foot"> - <b-button @click="showDialog = false"> - Cancel - </b-button> - <b-button type="is-primary" - icon-pack="fas" - icon-left="paper-plane" - @click="sendFeedback()" - :disabled="sendingFeedback || !message.trim()"> - {{ sendingFeedback ? "Working, please wait..." : "Send Message" }} - </b-button> - </footer> - </div> - </b-modal> + </section> + <footer class="modal-card-foot"> + <b-button @click="showDialog = false"> + Cancel + </b-button> + <b-button type="is-primary" + icon-pack="fas" + icon-left="paper-plane" + @click="sendFeedback()" + :disabled="sendingFeedback || !message || !message.trim()"> + {{ sendingFeedback ? "Working, please wait..." : "Send Message" }} + </b-button> + </footer> </div> - </script> - <script> + </b-modal> - const FeedbackForm = { - template: '#feedback-template', - mixins: [SimpleRequestMixin], - props: [ - 'action', - 'message', - ], - methods: { + </div> + </script> +</%def> - showFeedback() { - this.referrer = location.href - this.showDialog = true - this.$nextTick(function() { - this.$refs.textarea.focus() - }) - }, +<%def name="render_vue_script_feedback()"> + ${parent.render_vue_script_feedback()} + <script> - % if config.get_bool('tailbone.feedback_allows_reply'): - pleaseReplyChanged(value) { - this.$nextTick(() => { - this.$refs.userEmail.focus() - }) - }, - % endif + WuttaFeedbackForm.template = '#feedback-template' + WuttaFeedbackForm.props.message = String - sendFeedback() { - this.sendingFeedback = true + % if config.get_bool('tailbone.feedback_allows_reply'): - const params = { - referrer: this.referrer, - user: this.userUUID, - user_name: this.userName, - % if config.get_bool('tailbone.feedback_allows_reply'): - please_reply_to: this.pleaseReply ? this.userEmail : null, - % endif - message: this.message.trim(), - } + WuttaFeedbackFormData.pleaseReply = false + WuttaFeedbackFormData.userEmail = null - this.simplePOST(this.action, params, response => { + WuttaFeedbackForm.methods.pleaseReplyChanged = function(value) { + this.$nextTick(() => { + this.$refs.userEmail.focus() + }) + } - this.$buefy.toast.open({ - message: "Message sent! Thank you for your feedback.", - type: 'is-info', - duration: 4000, // 4 seconds - }) - - this.showDialog = false - // clear out message, in case they need to send another - this.message = "" - this.sendingFeedback = false - - }, response => { // failure - this.sendingFeedback = false - }) - }, + WuttaFeedbackForm.methods.getExtraParams = function() { + return { + please_reply_to: this.pleaseReply ? this.userEmail : null, } } - const FeedbackFormData = { - referrer: null, - userUUID: null, - userName: null, - userEmail: null, - % if config.get_bool('tailbone.feedback_allows_reply'): - pleaseReply: false, - % endif - showDialog: false, - sendingFeedback: false, - } + % endif - </script> - % endif + // TODO: deprecate / remove these + const FeedbackForm = WuttaFeedbackForm + const FeedbackFormData = WuttaFeedbackFormData + + </script> +</%def> + +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + ${page_help.render_template()} + ${page_help.declare_vars()} </%def> <%def name="modify_vue_vars()"> @@ -528,21 +482,6 @@ % endif - ############################## - ## feedback - ############################## - - % if request.has_perm('common.feedback'): - - WholePageData.feedbackMessage = "" - - % if request.user: - FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n} - FeedbackFormData.userName = ${json.dumps(str(request.user))|n} - % endif - - % endif - ############################## ## edit fields help ############################## @@ -562,10 +501,4 @@ ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + f'?ver={tailbone.__version__}')} ${make_grid_filter_components()} ${page_help.make_component()} - % if request.has_perm('common.feedback'): - <script> - FeedbackForm.data = function() { return FeedbackFormData } - Vue.component('feedback-form', FeedbackForm) - </script> - % endif </%def> From 3a9bf69aa7f63fc838259eef477324beee7c66a8 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 26 Aug 2024 14:56:15 -0500 Subject: [PATCH 32/85] =?UTF-8?q?bump:=20version=200.21.1=20=E2=86=92=200.?= =?UTF-8?q?21.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 13 +++++++++++++ pyproject.toml | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bcbc6ec..4616cf5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.2 (2024-08-26) + +### Fix + +- refactor waterpark base template to use wutta feedback component +- fix input/output file upload feature for configure pages, per oruga +- tweak how grid data translates to Vue template context +- merge filters into main grid template +- add basic wutta view for users +- some fixes for wutta people view +- various fixes for waterpark theme +- avoid deprecated `component` form kwarg + ## v0.21.1 (2024-08-22) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 2db880ad..831133c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.1" +version = "0.21.2" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -53,13 +53,13 @@ dependencies = [ "pyramid_mako", "pyramid_retry", "pyramid_tm", - "rattail[db,bouncer]>=0.18.4", + "rattail[db,bouncer]>=0.18.5", "sa-filters", "simplejson", "transaction", "waitress", "WebHelpers2", - "WuttaWeb>=0.12.0", + "WuttaWeb>=0.13.1", "zope.sqlalchemy>=1.5", ] From d67eb2f1cc15719478a26b8b76246947b528885e Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 26 Aug 2024 15:24:40 -0500 Subject: [PATCH 33/85] fix: show non-standard config values for app info configure email this page is currently showing some basic email sender/recips etc. but the config keys traditionally used by rattail are different than wuttjamaican..so for now we must "translate" --- tailbone/views/settings.py | 49 ++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py index 099a77e1..0180aa4b 100644 --- a/tailbone/views/settings.py +++ b/tailbone/views/settings.py @@ -81,15 +81,56 @@ class AppInfoView(WuttaAppInfoView): """ """ simple_settings = super().configure_get_simple_settings() - # TODO: the update home page redirect setting is off by - # default for wuttaweb, but on for tailbone for setting in simple_settings: + + # TODO: the update home page redirect setting is off by + # default for wuttaweb, but on for tailbone if setting['name'] == 'wuttaweb.home_redirect_to_login': value = self.config.get_bool('wuttaweb.home_redirect_to_login') if value is None: value = self.config.get_bool('tailbone.login_is_home', default=True) - setting['default'] = value - break + setting['value'] = value + + # TODO: sending email is off by default for wuttjamaican, + # but on for rattail + elif setting['name'] == 'rattail.mail.send_emails': + value = self.config.get_bool('rattail.mail.send_emails', default=True) + setting['value'] = value + + # TODO: email defaults have different config keys in rattail + elif setting['name'] == 'rattail.email.default.sender': + value = self.config.get('rattail.email.default.sender') + if value is None: + value = self.config.get('rattail.mail.default.from') + setting['value'] = value + + # TODO: email defaults have different config keys in rattail + elif setting['name'] == 'rattail.email.default.subject': + value = self.config.get('rattail.email.default.subject') + if value is None: + value = self.config.get('rattail.mail.default.subject') + setting['value'] = value + + # TODO: email defaults have different config keys in rattail + elif setting['name'] == 'rattail.email.default.to': + value = self.config.get('rattail.email.default.to') + if value is None: + value = self.config.get('rattail.mail.default.to') + setting['value'] = value + + # TODO: email defaults have different config keys in rattail + elif setting['name'] == 'rattail.email.default.cc': + value = self.config.get('rattail.email.default.cc') + if value is None: + value = self.config.get('rattail.mail.default.cc') + setting['value'] = value + + # TODO: email defaults have different config keys in rattail + elif setting['name'] == 'rattail.email.default.bcc': + value = self.config.get('rattail.email.default.bcc') + if value is None: + value = self.config.get('rattail.mail.default.bcc') + setting['value'] = value # nb. these are no longer used (deprecated), but we keep # them defined here so the tool auto-deletes them From dffd951369de5ca36a877f9b8b36e344245266b0 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 26 Aug 2024 15:25:56 -0500 Subject: [PATCH 34/85] =?UTF-8?q?bump:=20version=200.21.2=20=E2=86=92=200.?= =?UTF-8?q?21.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4616cf5f..52a17a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.3 (2024-08-26) + +### Fix + +- show non-standard config values for app info configure email + ## v0.21.2 (2024-08-26) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 831133c1..2c18bd02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.2" +version = "0.21.3" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 7a9d5772db794d69632ce3a8621396d08e6ec679 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 26 Aug 2024 16:11:32 -0500 Subject: [PATCH 35/85] fix: handle differing email profile keys for appinfo/configure hopefully this all can improve some day soon.. --- tailbone/templates/configure.mako | 5 +- tailbone/views/settings.py | 96 +++++++++++++++++++++---------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/tailbone/templates/configure.mako b/tailbone/templates/configure.mako index 463d48b1..e6b128fc 100644 --- a/tailbone/templates/configure.mako +++ b/tailbone/templates/configure.mako @@ -280,15 +280,14 @@ <b-button @click="purgeSettingsShowDialog = false"> Cancel </b-button> - ${h.form(request.current_route_url())} + ${h.form(request.current_route_url(), **{'@submit': 'purgingSettings = true'})} ${h.csrf_token(request)} ${h.hidden('remove_settings', 'true')} <b-button type="is-danger" native-type="submit" :disabled="purgingSettings" icon-pack="fas" - icon-left="trash" - @click="purgingSettings = true"> + icon-left="trash"> {{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }} </b-button> ${h.end_form()} diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py index 0180aa4b..10a0c2eb 100644 --- a/tailbone/views/settings.py +++ b/tailbone/views/settings.py @@ -77,13 +77,41 @@ class AppInfoView(WuttaAppInfoView): return context + # nb. these email settings require special handling below + configure_profile_key_mismatches = [ + 'default.subject', + 'default.to', + 'default.cc', + 'default.bcc', + 'feedback.subject', + 'feedback.to', + ] + def configure_get_simple_settings(self): """ """ simple_settings = super().configure_get_simple_settings() + # TODO: + # there are several email config keys which differ between + # wuttjamaican and rattail. basically all of the "profile" keys + # have a different prefix. + + # after wuttaweb has declared its settings, we examine each and + # overwrite the value if one is defined with rattail config key. + # (nb. this happens even if wuttjamaican key has a value!) + + # note that we *do* declare the profile mismatch keys for + # rattail, as part of simple settings. this ensures the + # parent logic will always remove them when saving. however + # we must also include them in gather_settings() to ensure + # they are saved to match wuttjamaican values. + + # there are also a couple of flags where rattail's default is the + # opposite of wuttjamaican. so we overwrite those too as needed. + for setting in simple_settings: - # TODO: the update home page redirect setting is off by + # nb. the update home page redirect setting is off by # default for wuttaweb, but on for tailbone if setting['name'] == 'wuttaweb.home_redirect_to_login': value = self.config.get_bool('wuttaweb.home_redirect_to_login') @@ -91,55 +119,43 @@ class AppInfoView(WuttaAppInfoView): value = self.config.get_bool('tailbone.login_is_home', default=True) setting['value'] = value - # TODO: sending email is off by default for wuttjamaican, + # nb. sending email is off by default for wuttjamaican, # but on for rattail elif setting['name'] == 'rattail.mail.send_emails': value = self.config.get_bool('rattail.mail.send_emails', default=True) setting['value'] = value - # TODO: email defaults have different config keys in rattail + # nb. this one is even more special, key is entirely different elif setting['name'] == 'rattail.email.default.sender': value = self.config.get('rattail.email.default.sender') if value is None: value = self.config.get('rattail.mail.default.from') setting['value'] = value - # TODO: email defaults have different config keys in rattail - elif setting['name'] == 'rattail.email.default.subject': - value = self.config.get('rattail.email.default.subject') - if value is None: - value = self.config.get('rattail.mail.default.subject') - setting['value'] = value + else: - # TODO: email defaults have different config keys in rattail - elif setting['name'] == 'rattail.email.default.to': - value = self.config.get('rattail.email.default.to') - if value is None: - value = self.config.get('rattail.mail.default.to') - setting['value'] = value - - # TODO: email defaults have different config keys in rattail - elif setting['name'] == 'rattail.email.default.cc': - value = self.config.get('rattail.email.default.cc') - if value is None: - value = self.config.get('rattail.mail.default.cc') - setting['value'] = value - - # TODO: email defaults have different config keys in rattail - elif setting['name'] == 'rattail.email.default.bcc': - value = self.config.get('rattail.email.default.bcc') - if value is None: - value = self.config.get('rattail.mail.default.bcc') - setting['value'] = value + # nb. fetch alternate value for profile key mismatch + for key in self.configure_profile_key_mismatches: + if setting['name'] == f'rattail.email.{key}': + value = self.config.get(f'rattail.email.{key}') + if value is None: + value = self.config.get(f'rattail.mail.{key}') + setting['value'] = value + break # nb. these are no longer used (deprecated), but we keep # them defined here so the tool auto-deletes them simple_settings.extend([ + {'name': 'tailbone.login_is_home'}, {'name': 'tailbone.buefy_version'}, {'name': 'tailbone.vue_version'}, ]) + simple_settings.append({'name': 'rattail.mail.default.from'}) + for key in self.configure_profile_key_mismatches: + simple_settings.append({'name': f'rattail.mail.{key}'}) + for key in self.get_weblibs(): simple_settings.extend([ {'name': f'tailbone.libver.{key}'}, @@ -148,6 +164,28 @@ class AppInfoView(WuttaAppInfoView): return simple_settings + def configure_gather_settings(self, data, simple_settings=None): + """ """ + settings = super().configure_gather_settings(data, simple_settings=simple_settings) + + # nb. must add legacy rattail profile settings to match new ones + for setting in list(settings): + + if setting['name'] == 'rattail.email.default.sender': + value = setting['value'] + settings.append({'name': 'rattail.mail.default.from', + 'value': value}) + + else: + for key in self.configure_profile_key_mismatches: + if setting['name'] == f'rattail.email.{key}': + value = setting['value'] + settings.append({'name': f'rattail.mail.{key}', + 'value': value}) + break + + return settings + class SettingView(MasterView): """ From ca05e688905398758470d5dd2db0ba288b8216a5 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 26 Aug 2024 16:12:14 -0500 Subject: [PATCH 36/85] =?UTF-8?q?bump:=20version=200.21.3=20=E2=86=92=200.?= =?UTF-8?q?21.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a17a2f..e18c786c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.4 (2024-08-26) + +### Fix + +- handle differing email profile keys for appinfo/configure + ## v0.21.3 (2024-08-26) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 2c18bd02..4845708b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.3" +version = "0.21.4" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 2e20fc5b7527275eaf7408dad56e3516ef6433e3 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 27 Aug 2024 13:50:30 -0500 Subject: [PATCH 37/85] fix: set empty string for "-new-" file configure option otherwise the "-new-" option is not properly auto-selected --- tailbone/views/master.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 1028ff27..6e05c35d 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -5441,7 +5441,7 @@ class MasterView(View): for template in self.normalize_input_file_templates( include_file_options=True): settings[template['setting_mode']] = template['mode'] - settings[template['setting_file']] = template['file'] + settings[template['setting_file']] = template['file'] or '' settings[template['setting_url']] = template['url'] file_options[template['key']] = template['file_options'] file_option_dirs[template['key']] = template['file_options_dir'] @@ -5457,7 +5457,7 @@ class MasterView(View): for template in self.normalize_output_file_templates( include_file_options=True): settings[template['setting_mode']] = template['mode'] - settings[template['setting_file']] = template['file'] + settings[template['setting_file']] = template['file'] or '' settings[template['setting_url']] = template['url'] file_options[template['key']] = template['file_options'] file_option_dirs[template['key']] = template['file_options_dir'] From b30f066c41f3b758882e0d8fc68e4a61b501e186 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 28 Aug 2024 00:30:15 -0500 Subject: [PATCH 38/85] =?UTF-8?q?bump:=20version=200.21.4=20=E2=86=92=200.?= =?UTF-8?q?21.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e18c786c..d3c8a92f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.5 (2024-08-28) + +### Fix + +- set empty string for "-new-" file configure option + ## v0.21.4 (2024-08-26) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 4845708b..4743fd3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.4" +version = "0.21.5" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -59,7 +59,7 @@ dependencies = [ "transaction", "waitress", "WebHelpers2", - "WuttaWeb>=0.13.1", + "WuttaWeb>=0.14.0", "zope.sqlalchemy>=1.5", ] From b81914fbf52357e3097a8f88d913c19ef30c0388 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 28 Aug 2024 00:35:15 -0500 Subject: [PATCH 39/85] test: fix broken test --- tests/test_app.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index e16461ba..f49f6b13 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -5,12 +5,9 @@ from unittest import TestCase from pyramid.config import Configurator -from wuttjamaican.testing import FileConfigTestCase - from rattail.exceptions import ConfigurationError -from rattail.config import RattailConfig +from rattail.testing import DataTestCase from tailbone import app as mod -from tests.util import DataTestCase class TestRattailConfig(TestCase): @@ -30,7 +27,7 @@ class TestRattailConfig(TestCase): class TestMakePyramidConfig(DataTestCase): - def make_config(self): + def make_config(self, **kwargs): myconf = self.write_file('web.conf', """ [rattail.db] default.url = sqlite:// From 0b6cfaa9c57bbbf0ef3ad51cab4e5d5bc56d6843 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 28 Aug 2024 09:53:14 -0500 Subject: [PATCH 40/85] fix: avoid error when grid value cannot be obtained --- tailbone/grids/core.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index ecf462fd..c6257d4b 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -575,7 +575,11 @@ class Grid(WuttaGrid): return getattr(obj, column_name) except AttributeError: pass - return obj[column_name] + + try: + return obj[column_name] + except TypeError: + pass def render_currency(self, obj, column_name): value = self.obtain_value(obj, column_name) From 71d63f6b93fee7ff8ff2ff19eebe844dce9476df Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 28 Aug 2024 09:53:37 -0500 Subject: [PATCH 41/85] =?UTF-8?q?bump:=20version=200.21.5=20=E2=86=92=200.?= =?UTF-8?q?21.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3c8a92f..59fcfcc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.6 (2024-08-28) + +### Fix + +- avoid error when grid value cannot be obtained + ## v0.21.5 (2024-08-28) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 4743fd3b..16018dbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.5" +version = "0.21.6" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From bc399182ba5eb957ae7c521f3b71701ff4bf39d1 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 28 Aug 2024 14:20:17 -0500 Subject: [PATCH 42/85] fix: avoid error when form value cannot be obtained --- tailbone/forms/core.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 059b212a..b5020975 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -1380,7 +1380,11 @@ class Form(object): return getattr(record, field_name) except AttributeError: pass - return record[field_name] + + try: + return record[field_name] + except TypeError: + pass # TODO: is this always safe to do? elif self.defaults and field_name in self.defaults: From 20dcdd8b86dfdbab1224676e3135ee8171b57f00 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 28 Aug 2024 14:20:51 -0500 Subject: [PATCH 43/85] =?UTF-8?q?bump:=20version=200.21.6=20=E2=86=92=200.?= =?UTF-8?q?21.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59fcfcc9..aee19700 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.7 (2024-08-28) + +### Fix + +- avoid error when form value cannot be obtained + ## v0.21.6 (2024-08-28) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 16018dbb..45a2adc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.6" +version = "0.21.7" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 812d8d2349e7517e2ef5702dcf904cd0b5c5c8af Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 28 Aug 2024 14:37:18 -0500 Subject: [PATCH 44/85] fix: ignore session kwarg for `MasterView.make_row_grid()` --- tailbone/views/master.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 6e05c35d..baf63caa 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -551,7 +551,8 @@ class MasterView(View): def get_quickie_result_url(self, obj): return self.get_action_url('view', obj) - def make_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): + def make_row_grid(self, factory=None, key=None, data=None, columns=None, + session=None, **kwargs): """ Make and return a new (configured) rows grid instance. """ From 9be2f6347571d5989fabad88a9fc90ebf63812f9 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 28 Aug 2024 14:37:40 -0500 Subject: [PATCH 45/85] =?UTF-8?q?bump:=20version=200.21.7=20=E2=86=92=200.?= =?UTF-8?q?21.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aee19700..a31b80ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.8 (2024-08-28) + +### Fix + +- ignore session kwarg for `MasterView.make_row_grid()` + ## v0.21.7 (2024-08-28) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 45a2adc9..350803dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.7" +version = "0.21.8" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 2219cf81988c583320014492a6e114c40e025e2b Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 28 Aug 2024 17:38:05 -0500 Subject: [PATCH 46/85] fix: render custom attrs in form component tag --- tailbone/forms/core.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index b5020975..601dcfb1 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -1037,9 +1037,9 @@ class Form(object): def render_vue_tag(self, **kwargs): """ """ - return self.render_vuejs_component() + return self.render_vuejs_component(**kwargs) - def render_vuejs_component(self): + def render_vuejs_component(self, **kwargs): """ Render the Vue.js component HTML for the form. @@ -1050,10 +1050,11 @@ class Form(object): <tailbone-form :configure-fields-help="configureFieldsHelp"> </tailbone-form> """ - kwargs = dict(self.vuejs_component_kwargs) + kw = dict(self.vuejs_component_kwargs) + kw.update(kwargs) if self.can_edit_help: - kwargs.setdefault(':configure-fields-help', 'configureFieldsHelp') - return HTML.tag(self.vue_tagname, **kwargs) + kw.setdefault(':configure-fields-help', 'configureFieldsHelp') + return HTML.tag(self.vue_tagname, **kw) def set_json_data(self, key, value): """ From 55f45ae8a081123af3c8fc931a7745f0d7ea0b2b Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 28 Aug 2024 17:38:33 -0500 Subject: [PATCH 47/85] =?UTF-8?q?bump:=20version=200.21.8=20=E2=86=92=200.?= =?UTF-8?q?21.9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a31b80ac..da628cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.9 (2024-08-28) + +### Fix + +- render custom attrs in form component tag + ## v0.21.8 (2024-08-28) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 350803dc..2720d003 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.8" +version = "0.21.9" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 8df52bf2a2d8902cc1565a5e46370273db580be2 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 29 Aug 2024 17:01:28 -0500 Subject: [PATCH 48/85] fix: expose datasync consumer batch size via configure page --- tailbone/templates/datasync/configure.mako | 29 ++++++---- tailbone/views/datasync.py | 65 +++++++++++++--------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/tailbone/templates/datasync/configure.mako b/tailbone/templates/datasync/configure.mako index 3651d0c4..2e444fb5 100644 --- a/tailbone/templates/datasync/configure.mako +++ b/tailbone/templates/datasync/configure.mako @@ -83,8 +83,8 @@ </b-notification> <b-field> - <b-checkbox name="use_profile_settings" - v-model="useProfileSettings" + <b-checkbox name="rattail.datasync.use_profile_settings" + v-model="simpleSettings['rattail.datasync.use_profile_settings']" native-value="true" @input="settingsNeedSaved = true"> Use these Settings to configure watchers and consumers @@ -99,7 +99,7 @@ </div> <div class="level-right"> <div class="level-item" - v-show="useProfileSettings"> + v-show="simpleSettings['rattail.datasync.use_profile_settings']"> <b-button type="is-primary" @click="newProfile()" icon-pack="fas" @@ -162,7 +162,7 @@ </${b}-table-column> <${b}-table-column label="Actions" v-slot="props" - v-if="useProfileSettings"> + v-if="simpleSettings['rattail.datasync.use_profile_settings']"> <a href="#" class="grid-action" @click.prevent="editProfile(props.row)"> @@ -580,18 +580,27 @@ <b-field label="Supervisor Process Name" message="This should be the complete name, including group - e.g. poser:poser_datasync" expanded> - <b-input name="supervisor_process_name" - v-model="supervisorProcessName" + <b-input name="rattail.datasync.supervisor_process_name" + v-model="simpleSettings['rattail.datasync.supervisor_process_name']" @input="settingsNeedSaved = true" expanded> </b-input> </b-field> + <b-field label="Consumer Batch Size" + message="Max number of changes to be consumed at once." + expanded> + <numeric-input name="rattail.datasync.batch_size_limit" + v-model="simpleSettings['rattail.datasync.batch_size_limit']" + @input="settingsNeedSaved = true" /> + </b-field> + + <h3 class="is-size-3">Legacy</h3> <b-field label="Restart Command" message="This will run as '${system_user}' system user - please configure sudoers as needed. Typical command is like: sudo supervisorctl restart poser:poser_datasync" expanded> - <b-input name="restart_command" - v-model="restartCommand" + <b-input name="tailbone.datasync.restart" + v-model="simpleSettings['tailbone.datasync.restart']" @input="settingsNeedSaved = true" expanded> </b-input> @@ -606,7 +615,6 @@ ThisPageData.showConfigFilesNote = false ThisPageData.profilesData = ${json.dumps(profiles_data)|n} ThisPageData.showDisabledProfiles = false - ThisPageData.useProfileSettings = ${json.dumps(use_profile_settings)|n} ThisPageData.editProfileShowDialog = false ThisPageData.editingProfile = null @@ -631,9 +639,6 @@ ThisPageData.editingConsumerRunas = null ThisPageData.editingConsumerEnabled = true - ThisPageData.supervisorProcessName = ${json.dumps(supervisor_process_name)|n} - ThisPageData.restartCommand = ${json.dumps(restart_command)|n} - ThisPage.computed.updateConsumerDisabled = function() { if (!this.editingConsumerKey) { return true diff --git a/tailbone/views/datasync.py b/tailbone/views/datasync.py index 134d6018..2b955b5f 100644 --- a/tailbone/views/datasync.py +++ b/tailbone/views/datasync.py @@ -202,10 +202,36 @@ class DataSyncThreadView(MasterView): return self.redirect(self.request.get_referrer( default=self.request.route_url('datasyncchanges'))) - def configure_get_context(self): + def configure_get_simple_settings(self): + """ """ + return [ + + # basic + {'section': 'rattail.datasync', + 'option': 'use_profile_settings', + 'type': bool}, + + # misc. + {'section': 'rattail.datasync', + 'option': 'supervisor_process_name'}, + {'section': 'rattail.datasync', + 'option': 'batch_size_limit', + 'type': int}, + + # legacy + {'section': 'tailbone', + 'option': 'datasync.restart'}, + + ] + + def configure_get_context(self, **kwargs): + """ """ + context = super().configure_get_context(**kwargs) + profiles = self.datasync_handler.get_configured_profiles( include_disabled=True, ignore_problems=True) + context['profiles'] = profiles profiles_data = [] for profile in sorted(profiles.values(), key=lambda p: p.key): @@ -243,25 +269,15 @@ class DataSyncThreadView(MasterView): data['consumers_data'] = consumers profiles_data.append(data) - return { - 'profiles': profiles, - 'profiles_data': profiles_data, - 'use_profile_settings': self.datasync_handler.should_use_profile_settings(), - 'supervisor_process_name': self.rattail_config.get( - 'rattail.datasync', 'supervisor_process_name'), - 'restart_command': self.rattail_config.get( - 'tailbone', 'datasync.restart'), - } + context['profiles_data'] = profiles_data + return context - def configure_gather_settings(self, data): - settings = [] - watch = [] + def configure_gather_settings(self, data, **kwargs): + """ """ + settings = super().configure_gather_settings(data, **kwargs) - use_profile_settings = data.get('use_profile_settings') == 'true' - settings.append({'name': 'rattail.datasync.use_profile_settings', - 'value': 'true' if use_profile_settings else 'false'}) - - if use_profile_settings: + if data.get('rattail.datasync.use_profile_settings') == 'true': + watch = [] for profile in json.loads(data['profiles']): pkey = profile['key'] @@ -323,17 +339,12 @@ class DataSyncThreadView(MasterView): settings.append({'name': 'rattail.datasync.watch', 'value': ', '.join(watch)}) - if data['supervisor_process_name']: - settings.append({'name': 'rattail.datasync.supervisor_process_name', - 'value': data['supervisor_process_name']}) - - if data['restart_command']: - settings.append({'name': 'tailbone.datasync.restart', - 'value': data['restart_command']}) - return settings - def configure_remove_settings(self): + def configure_remove_settings(self, **kwargs): + """ """ + super().configure_remove_settings(**kwargs) + purge_datasync_settings(self.rattail_config, self.Session()) @classmethod From b9b8bbd2eae1543cb74898f95e72cee5e7de6f46 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 29 Aug 2024 17:18:32 -0500 Subject: [PATCH 49/85] fix: wrap notes text for batch view --- tailbone/views/batch/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index 8ee3a37d..a75fda1c 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -383,7 +383,7 @@ class BatchMasterView(MasterView): f.set_label('executed_by', "Executed by") # notes - f.set_type('notes', 'text') + f.set_type('notes', 'text_wrapped') # if self.creating and self.request.user: # batch = fs.model From 5e742eab1795fe4c53573070af264c8d8a4cf3c0 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 9 Sep 2024 08:32:28 -0500 Subject: [PATCH 50/85] fix: use better icon for submit button on login page --- tailbone/forms/core.py | 2 ++ tailbone/templates/forms/deform.mako | 2 +- tailbone/views/auth.py | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 601dcfb1..4024557b 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -401,6 +401,8 @@ class Form(object): self.edit_help_url = edit_help_url self.route_prefix = route_prefix + self.button_icon_submit = kwargs.get('button_icon_submit', 'save') + def __iter__(self): return iter(self.fields) diff --git a/tailbone/templates/forms/deform.mako b/tailbone/templates/forms/deform.mako index ea35ab17..2100b460 100644 --- a/tailbone/templates/forms/deform.mako +++ b/tailbone/templates/forms/deform.mako @@ -59,7 +59,7 @@ native-type="submit" :disabled="${form.vue_component}Submitting" icon-pack="fas" - icon-left="save"> + icon-left="${form.button_icon_submit}"> {{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }} </b-button> % else: diff --git a/tailbone/views/auth.py b/tailbone/views/auth.py index 730d7b6a..a54a19a9 100644 --- a/tailbone/views/auth.py +++ b/tailbone/views/auth.py @@ -24,8 +24,6 @@ Auth Views """ -from rattail.db.auth import set_user_password - import colander from deform import widget as dfwidget from pyramid.httpexceptions import HTTPForbidden @@ -104,6 +102,7 @@ class AuthenticationView(View): form.save_label = "Login" form.show_reset = True form.show_cancel = False + form.button_icon_submit = 'user' if form.validate(): user = self.authenticate_user(form.validated['username'], form.validated['password']) @@ -185,7 +184,8 @@ class AuthenticationView(View): schema = ChangePassword().bind(user=self.request.user, request=self.request) form = forms.Form(schema=schema, request=self.request) if form.validate(): - set_user_password(self.request.user, form.validated['new_password']) + auth = self.app.get_auth_handler() + auth.set_user_password(self.request.user, form.validated['new_password']) self.request.session.flash("Your password has been changed.") return self.redirect(self.request.get_referrer()) From a4d81a6e3cf431bae5fb91337ccf1c345e75c137 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Fri, 13 Sep 2024 18:16:07 -0500 Subject: [PATCH 51/85] docs: use markdown for readme file --- README.rst => README.md | 8 +++----- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) rename README.rst => README.md (56%) diff --git a/README.rst b/README.md similarity index 56% rename from README.rst rename to README.md index 0cffc62d..74c007f6 100644 --- a/README.rst +++ b/README.md @@ -1,10 +1,8 @@ -Tailbone -======== +# Tailbone Tailbone is an extensible web application based on Rattail. It provides a "back-office network environment" (BONE) for use in managing retail data. -Please see Rattail's `home page`_ for more information. - -.. _home page: http://rattailproject.org/ +Please see Rattail's [home page](http://rattailproject.org/) for more +information. diff --git a/pyproject.toml b/pyproject.toml index 2720d003..8c6525c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "hatchling.build" name = "Tailbone" version = "0.21.9" description = "Backoffice Web Application for Rattail" -readme = "README.rst" +readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] license = {text = "GNU GPL v3+"} classifiers = [ From 0b646d2d187fafe743cb7816ab0a86d171b76646 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Sat, 14 Sep 2024 12:49:37 -0500 Subject: [PATCH 52/85] fix: update project repo links, kallithea -> forgejo --- pyproject.toml | 6 ++-- tailbone/views/upgrades.py | 69 +++++++++++--------------------------- 2 files changed, 23 insertions(+), 52 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c6525c6..a1c96dd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,9 +84,9 @@ tailbone = "tailbone.config:ConfigExtension" [project.urls] Homepage = "https://rattailproject.org" -Repository = "https://kallithea.rattailproject.org/rattail-project/tailbone" -Issues = "https://redmine.rattailproject.org/projects/tailbone/issues" -Changelog = "https://kallithea.rattailproject.org/rattail-project/tailbone/files/master/CHANGELOG.md" +Repository = "https://forgejo.wuttaproject.org/rattail/tailbone" +Issues = "https://forgejo.wuttaproject.org/rattail/tailbone/issues" +Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone/src/branch/master/CHANGELOG.md" [tool.commitizen] diff --git a/tailbone/views/upgrades.py b/tailbone/views/upgrades.py index 3276b64d..ffa88032 100644 --- a/tailbone/views/upgrades.py +++ b/tailbone/views/upgrades.py @@ -348,56 +348,27 @@ class UpgradeView(MasterView): commit_hash_pattern = re.compile(r'^.{40}$') def get_changelog_projects(self): - projects = { - 'rattail': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail/files/v{new_version}/CHANGES.rst', - }, - 'Tailbone': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone/files/v{new_version}/CHANGES.rst', - }, - 'pyCOREPOS': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-project/pycorepos/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-project/pycorepos/files/v{new_version}/CHANGES.rst', - }, - 'rattail_corepos': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-corepos/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-corepos/files/v{new_version}/CHANGES.rst', - }, - 'tailbone_corepos': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-corepos/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-corepos/files/v{new_version}/CHANGES.rst', - }, - 'onager': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/onager/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/onager/files/v{new_version}/CHANGES.rst', - }, - 'rattail-onager': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/rattail-onager/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/rattail-onager/files/v{new_version}/CHANGELOG.md', - }, - 'rattail_tempmon': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-tempmon/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-tempmon/files/v{new_version}/CHANGES.rst', - }, - 'tailbone-onager': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/tailbone-onager/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/tailbone-onager/files/v{new_version}/CHANGELOG.md', - }, - 'rattail_woocommerce': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-woocommerce/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-woocommerce/files/v{new_version}/CHANGES.rst', - }, - 'tailbone_woocommerce': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-woocommerce/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-woocommerce/files/v{new_version}/CHANGES.rst', - }, - 'tailbone_theo': { - 'commit_url': 'https://kallithea.rattailproject.org/rattail-project/theo/changelog/{new_version}/?size=10', - 'release_url': 'https://kallithea.rattailproject.org/rattail-project/theo/files/v{new_version}/CHANGES.rst', - }, + project_map = { + 'onager': 'onager', + 'pyCOREPOS': 'pycorepos', + 'rattail': 'rattail', + 'rattail_corepos': 'rattail-corepos', + 'rattail-onager': 'rattail-onager', + 'rattail_tempmon': 'rattail-tempmon', + 'rattail_woocommerce': 'rattail-woocommerce', + 'Tailbone': 'tailbone', + 'tailbone_corepos': 'tailbone-corepos', + 'tailbone-onager': 'tailbone-onager', + 'tailbone_theo': 'theo', + 'tailbone_woocommerce': 'tailbone-woocommerce', } + + projects = {} + for name, repo in project_map.items(): + projects[name] = { + 'commit_url': f'https://forgejo.wuttaproject.org/rattail/{repo}/compare/{{old_version}}...{{new_version}}', + 'release_url': f'https://forgejo.wuttaproject.org/rattail/{repo}/src/tag/v{{new_version}}/CHANGELOG.md', + } return projects def get_changelog_url(self, project, old_version, new_version): From 0b4efae392ff35ca4a0d0ac1ea59859b25e084f2 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Sun, 15 Sep 2024 10:56:01 -0500 Subject: [PATCH 53/85] =?UTF-8?q?bump:=20version=200.21.9=20=E2=86=92=200.?= =?UTF-8?q?21.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da628cf3..73c8b72b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.10 (2024-09-15) + +### Fix + +- update project repo links, kallithea -> forgejo +- use better icon for submit button on login page +- wrap notes text for batch view +- expose datasync consumer batch size via configure page + ## v0.21.9 (2024-08-28) ### Fix diff --git a/pyproject.toml b/pyproject.toml index a1c96dd4..3368842b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.9" +version = "0.21.10" description = "Backoffice Web Application for Rattail" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 2308d2e2408ea5429ce196ed6c193241a21742a8 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 16 Sep 2024 12:55:58 -0500 Subject: [PATCH 54/85] fix: become/stop root should redirect to previous url for default theme; butterball already did that --- tailbone/templates/base.mako | 18 ++++++++++++++++-- tailbone/templates/themes/butterball/base.mako | 16 ++-------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index 86b1ba1d..8228f823 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -632,9 +632,23 @@ % endif <div class="navbar-dropdown"> % if request.is_root: - ${h.link_to("Stop being root", url('stop_root'), class_='navbar-item root-user')} + ${h.form(url('stop_root'), ref='stopBeingRootForm')} + ${h.csrf_token(request)} + <input type="hidden" name="referrer" value="${request.current_route_url()}" /> + <a @click="$refs.stopBeingRootForm.submit()" + class="navbar-item root-user"> + Stop being root + </a> + ${h.end_form()} % elif request.is_admin: - ${h.link_to("Become root", url('become_root'), class_='navbar-item root-user')} + ${h.form(url('become_root'), ref='startBeingRootForm')} + ${h.csrf_token(request)} + <input type="hidden" name="referrer" value="${request.current_route_url()}" /> + <a @click="$refs.startBeingRootForm.submit()" + class="navbar-item root-user"> + Become root + </a> + ${h.end_form()} % endif % if messaging_enabled: ${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')} diff --git a/tailbone/templates/themes/butterball/base.mako b/tailbone/templates/themes/butterball/base.mako index 14616474..b69eacfb 100644 --- a/tailbone/templates/themes/butterball/base.mako +++ b/tailbone/templates/themes/butterball/base.mako @@ -909,7 +909,7 @@ ${h.form(url('stop_root'), ref='stopBeingRootForm')} ${h.csrf_token(request)} <input type="hidden" name="referrer" value="${request.current_route_url()}" /> - <a @click="stopBeingRoot()" + <a @click="$refs.stopBeingRootForm.submit()" class="navbar-item has-background-danger has-text-white"> Stop being root </a> @@ -918,7 +918,7 @@ ${h.form(url('become_root'), ref='startBeingRootForm')} ${h.csrf_token(request)} <input type="hidden" name="referrer" value="${request.current_route_url()}" /> - <a @click="startBeingRoot()" + <a @click="$refs.startBeingRootForm.submit()" class="navbar-item has-background-danger has-text-white"> Become root </a> @@ -1103,18 +1103,6 @@ const key = 'menu_' + hash + '_shown' this[key] = !this[key] }, - - % if request.is_admin: - - startBeingRoot() { - this.$refs.startBeingRootForm.submit() - }, - - stopBeingRoot() { - this.$refs.stopBeingRootForm.submit() - }, - - % endif }, } From d520f64fee9c2c083e867816e2c90e56028c41f8 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 3 Oct 2024 08:56:52 -0500 Subject: [PATCH 55/85] fix: custom method for adding grid action since for now, we are using custom grid action class --- tailbone/grids/core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index c6257d4b..73de42c6 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -1544,6 +1544,11 @@ class Grid(WuttaGrid): self._table_data = results return self._table_data + # TODO: remove this when we use upstream GridAction + def add_action(self, key, **kwargs): + """ """ + self.actions.append(GridAction(self.request, key, **kwargs)) + def set_action_urls(self, row, rowobj, i): """ Pre-generate all action URLs for the given data row. Meant for use From c6365f263166c53934fd81083c01d2bceccb01ab Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Thu, 3 Oct 2024 09:05:46 -0500 Subject: [PATCH 56/85] =?UTF-8?q?bump:=20version=200.21.10=20=E2=86=92=200?= =?UTF-8?q?.21.11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c8b72b..3c31ae92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.21.11 (2024-10-03) + +### Fix + +- custom method for adding grid action +- become/stop root should redirect to previous url + ## v0.21.10 (2024-09-15) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 3368842b..5b63a71f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.10" +version = "0.21.11" description = "Backoffice Web Application for Rattail" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 072db39233dd8c0c22e429202f446cd67f578863 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 22 Oct 2024 14:26:10 -0500 Subject: [PATCH 57/85] feat: add support for new ordering batch from parsed file --- tailbone/api/batch/receiving.py | 30 +- tailbone/templates/ordering/configure.mako | 74 +++++ tailbone/templates/receiving/configure.mako | 8 +- tailbone/views/batch/core.py | 5 +- tailbone/views/purchasing/batch.py | 290 +++++++++++++++++++- tailbone/views/purchasing/ordering.py | 101 ++++++- tailbone/views/purchasing/receiving.py | 219 +++------------ 7 files changed, 498 insertions(+), 229 deletions(-) create mode 100644 tailbone/templates/ordering/configure.mako diff --git a/tailbone/api/batch/receiving.py b/tailbone/api/batch/receiving.py index daa4290f..b23bff55 100644 --- a/tailbone/api/batch/receiving.py +++ b/tailbone/api/batch/receiving.py @@ -29,8 +29,7 @@ import logging import humanize import sqlalchemy as sa -from rattail.db import model -from rattail.util import pretty_quantity +from rattail.db.model import PurchaseBatch, PurchaseBatchRow from cornice import Service from deform import widget as dfwidget @@ -45,7 +44,7 @@ log = logging.getLogger(__name__) class ReceivingBatchViews(APIBatchView): - model_class = model.PurchaseBatch + model_class = PurchaseBatch default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' route_prefix = 'receivingbatchviews' permission_prefix = 'receiving' @@ -55,7 +54,8 @@ class ReceivingBatchViews(APIBatchView): supports_execute = True def base_query(self): - query = super(ReceivingBatchViews, self).base_query() + model = self.app.model + query = super().base_query() query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING) return query @@ -85,7 +85,7 @@ class ReceivingBatchViews(APIBatchView): # assume "receive from PO" if given a PO key if data.get('purchase_key'): - data['receiving_workflow'] = 'from_po' + data['workflow'] = 'from_po' return super().create_object(data) @@ -120,6 +120,7 @@ class ReceivingBatchViews(APIBatchView): return self._get(obj=batch) def eligible_purchases(self): + model = self.app.model uuid = self.request.params.get('vendor_uuid') vendor = self.Session.get(model.Vendor, uuid) if uuid else None if not vendor: @@ -176,7 +177,7 @@ class ReceivingBatchViews(APIBatchView): class ReceivingBatchRowViews(APIBatchRowView): - model_class = model.PurchaseBatchRow + model_class = PurchaseBatchRow default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' route_prefix = 'receiving.rows' permission_prefix = 'receiving' @@ -185,7 +186,8 @@ class ReceivingBatchRowViews(APIBatchRowView): supports_quick_entry = True def make_filter_spec(self): - filters = super(ReceivingBatchRowViews, self).make_filter_spec() + model = self.app.model + filters = super().make_filter_spec() if filters: # must translate certain convenience filters @@ -296,11 +298,11 @@ class ReceivingBatchRowViews(APIBatchRowView): return filters def normalize(self, row): - data = super(ReceivingBatchRowViews, self).normalize(row) + data = super().normalize(row) + model = self.app.model batch = row.batch - app = self.get_rattail_app() - prodder = app.get_products_handler() + prodder = self.app.get_products_handler() data['product_uuid'] = row.product_uuid data['item_id'] = row.item_id @@ -375,7 +377,7 @@ class ReceivingBatchRowViews(APIBatchRowView): if accounted_for: # some product accounted for; button should receive "remainder" only if remainder: - remainder = pretty_quantity(remainder) + remainder = self.app.render_quantity(remainder) data['quick_receive_quantity'] = remainder data['quick_receive_text'] = "Receive Remainder ({} {})".format( remainder, data['unit_uom']) @@ -386,7 +388,7 @@ class ReceivingBatchRowViews(APIBatchRowView): else: # nothing yet accounted for, button should receive "all" if not remainder: log.warning("quick receive remainder is empty for row %s", row.uuid) - remainder = pretty_quantity(remainder) + remainder = self.app.render_quantity(remainder) data['quick_receive_quantity'] = remainder data['quick_receive_text'] = "Receive ALL ({} {})".format( remainder, data['unit_uom']) @@ -414,7 +416,7 @@ class ReceivingBatchRowViews(APIBatchRowView): data['received_alert'] = None if self.batch_handler.get_units_confirmed(row): msg = "You have already received some of this product; last update was {}.".format( - humanize.naturaltime(app.make_utc() - row.modified)) + humanize.naturaltime(self.app.make_utc() - row.modified)) data['received_alert'] = msg return data @@ -423,6 +425,8 @@ class ReceivingBatchRowViews(APIBatchRowView): """ View which handles "receiving" against a particular batch row. """ + model = self.app.model + # first do basic input validation schema = ReceiveRow().bind(session=self.Session()) form = forms.Form(schema=schema, request=self.request) diff --git a/tailbone/templates/ordering/configure.mako b/tailbone/templates/ordering/configure.mako new file mode 100644 index 00000000..dc505c42 --- /dev/null +++ b/tailbone/templates/ordering/configure.mako @@ -0,0 +1,74 @@ +## -*- coding: utf-8; -*- +<%inherit file="/configure.mako" /> + +<%def name="form_content()"> + + <h3 class="block is-size-3">Workflows</h3> + <div class="block" style="padding-left: 2rem;"> + + <p class="block"> + Users can only choose from the workflows enabled below. + </p> + + <b-field> + <b-checkbox name="rattail.batch.purchase.allow_ordering_from_scratch" + v-model="simpleSettings['rattail.batch.purchase.allow_ordering_from_scratch']" + native-value="true" + @input="settingsNeedSaved = true"> + From Scratch + </b-checkbox> + </b-field> + + <b-field> + <b-checkbox name="rattail.batch.purchase.allow_ordering_from_file" + v-model="simpleSettings['rattail.batch.purchase.allow_ordering_from_file']" + native-value="true" + @input="settingsNeedSaved = true"> + From Order File + </b-checkbox> + </b-field> + + </div> + + <h3 class="block is-size-3">Vendors</h3> + <div class="block" style="padding-left: 2rem;"> + + <b-field message="If not set, user must choose a "supported" vendor."> + <b-checkbox name="rattail.batch.purchase.allow_ordering_any_vendor" + v-model="simpleSettings['rattail.batch.purchase.allow_ordering_any_vendor']" + native-value="true" + @input="settingsNeedSaved = true"> + Allow ordering for <span class="has-text-weight-bold">any</span> vendor + </b-checkbox> + </b-field> + + </div> + + <h3 class="block is-size-3">Order Parsers</h3> + <div class="block" style="padding-left: 2rem;"> + + <p class="block"> + Only the selected file parsers will be exposed to users. + </p> + + % for Parser in order_parsers: + <b-field message="${Parser.key}"> + <b-checkbox name="order_parser_${Parser.key}" + v-model="orderParsers['${Parser.key}']" + native-value="true" + @input="settingsNeedSaved = true"> + ${Parser.title} + </b-checkbox> + </b-field> + % endfor + + </div> + +</%def> + +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + <script> + ThisPageData.orderParsers = ${json.dumps(order_parsers_data)|n} + </script> +</%def> diff --git a/tailbone/templates/receiving/configure.mako b/tailbone/templates/receiving/configure.mako index f613e13e..a36dde43 100644 --- a/tailbone/templates/receiving/configure.mako +++ b/tailbone/templates/receiving/configure.mako @@ -69,12 +69,12 @@ <h3 class="block is-size-3">Vendors</h3> <div class="block" style="padding-left: 2rem;"> - <b-field message="If set, user must choose a "supported" vendor; otherwise they may choose "any" vendor."> - <b-checkbox name="rattail.batch.purchase.supported_vendors_only" - v-model="simpleSettings['rattail.batch.purchase.supported_vendors_only']" + <b-field message="If not set, user must choose a "supported" vendor."> + <b-checkbox name="rattail.batch.purchase.allow_receiving_any_vendor" + v-model="simpleSettings['rattail.batch.purchase.allow_receiving_any_vendor']" native-value="true" @input="settingsNeedSaved = true"> - Only allow batch for "supported" vendors + Allow receiving for <span class="has-text-weight-bold">any</span> vendor </b-checkbox> </b-field> diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index a75fda1c..c162b579 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -46,10 +46,11 @@ import colander from deform import widget as dfwidget from webhelpers2.html import HTML, tags +from wuttaweb.util import render_csrf_token + from tailbone import forms, grids from tailbone.db import Session from tailbone.views import MasterView -from tailbone.util import csrf_token log = logging.getLogger(__name__) @@ -441,7 +442,7 @@ class BatchMasterView(MasterView): form = [ begin_form, - csrf_token(self.request), + render_csrf_token(self.request), tags.hidden('complete', value=value), submit, tags.end_form(), diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py index 590b9af5..5e00704e 100644 --- a/tailbone/views/purchasing/batch.py +++ b/tailbone/views/purchasing/batch.py @@ -24,6 +24,8 @@ Base class for purchasing batch views """ +import warnings + from rattail.db.model import PurchaseBatch, PurchaseBatchRow import colander @@ -67,6 +69,8 @@ class PurchasingBatchView(BatchMasterView): 'store', 'buyer', 'vendor', + 'description', + 'workflow', 'department', 'purchase', 'vendor_email', @@ -158,6 +162,174 @@ class PurchasingBatchView(BatchMasterView): def batch_mode(self): raise NotImplementedError("Please define `batch_mode` for your purchasing batch view") + def get_supported_workflows(self): + """ + Return the supported "create batch" workflows. + """ + enum = self.app.enum + if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING: + return self.batch_handler.supported_ordering_workflows() + elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING: + return self.batch_handler.supported_receiving_workflows() + elif self.batch_mode == enum.PURCHASE_BATCH_MODE_COSTING: + return self.batch_handler.supported_costing_workflows() + raise ValueError("unknown batch mode") + + def allow_any_vendor(self): + """ + Return boolean indicating whether creating a batch for "any" + vendor is allowed, vs. only supported vendors. + """ + enum = self.app.enum + + if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING: + return self.batch_handler.allow_ordering_any_vendor() + + elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING: + value = self.config.get_bool('rattail.batch.purchase.allow_receiving_any_vendor') + if value is not None: + return value + value = self.config.get_bool('rattail.batch.purchase.supported_vendors_only') + if value is not None: + warnings.warn("setting rattail.batch.purchase.supported_vendors_only is deprecated; " + "please use rattail.batch.purchase.allow_receiving_any_vendor instead", + DeprecationWarning) + # nb. must negate this setting + return not value + return False + + raise ValueError("unknown batch mode") + + def get_supported_vendors(self): + """ + Return the supported vendors for creating a batch. + """ + return [] + + def create(self, form=None, **kwargs): + """ + Custom view for creating a new batch. We split the process + into two steps, 1) choose workflow and 2) create batch. This + is because the specific form details for creating a batch will + depend on which "type" of batch creation is to be done, and + it's much easier to keep conditional logic for that in the + server instead of client-side etc. + """ + model = self.app.model + enum = self.app.enum + route_prefix = self.get_route_prefix() + + workflows = self.get_supported_workflows() + valid_workflows = [workflow['workflow_key'] + for workflow in workflows] + + # if user has already identified their desired workflow, then + # we can just farm out to the default logic. we will of + # course configure our form differently, based on workflow, + # but this create() method at least will not need + # customization for that. + if self.request.matched_route.name.endswith('create_workflow'): + + redirect = self.redirect(self.request.route_url(f'{route_prefix}.create')) + + # however we do have one more thing to check - the workflow + # requested must of course be valid! + workflow_key = self.request.matchdict['workflow_key'] + if workflow_key not in valid_workflows: + self.request.session.flash(f"Not a supported workflow: {workflow_key}", 'error') + raise redirect + + # also, we require vendor to be correctly identified. if + # someone e.g. navigates to a URL by accident etc. we want + # to gracefully handle and redirect + uuid = self.request.matchdict['vendor_uuid'] + vendor = self.Session.get(model.Vendor, uuid) + if not vendor: + self.request.session.flash("Invalid vendor selection. " + "Please choose an existing vendor.", + 'warning') + raise redirect + + # okay now do the normal thing, per workflow + return super().create(**kwargs) + + # on the other hand, if caller provided a form, that means we are in + # the middle of some other custom workflow, e.g. "add child to truck + # dump parent" or some such. in which case we also defer to the normal + # logic, so as to not interfere with that. + if form: + return super().create(form=form, **kwargs) + + # okay, at this point we need the user to select a vendor and workflow + self.creating = True + context = {} + + # form to accept user choice of vendor/workflow + schema = colander.Schema() + schema.add(colander.SchemaNode(colander.String(), name='vendor')) + schema.add(colander.SchemaNode(colander.String(), name='workflow', + validator=colander.OneOf(valid_workflows))) + factory = self.get_form_factory() + form = factory(schema=schema, request=self.request) + + # configure vendor field + vendor_handler = self.app.get_vendor_handler() + if self.allow_any_vendor(): + # user may choose *any* available vendor + use_dropdown = vendor_handler.choice_uses_dropdown() + if use_dropdown: + vendors = self.Session.query(model.Vendor)\ + .order_by(model.Vendor.id)\ + .all() + vendor_values = [(vendor.uuid, f"({vendor.id}) {vendor.name}") + for vendor in vendors] + form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values)) + if len(vendors) == 1: + form.set_default('vendor', vendors[0].uuid) + else: + vendor_display = "" + if self.request.method == 'POST': + if self.request.POST.get('vendor'): + vendor = self.Session.get(model.Vendor, self.request.POST['vendor']) + if vendor: + vendor_display = str(vendor) + vendors_url = self.request.route_url('vendors.autocomplete') + form.set_widget('vendor', forms.widgets.JQueryAutocompleteWidget( + field_display=vendor_display, service_url=vendors_url)) + else: # only "supported" vendors allowed + vendors = self.get_supported_vendors() + vendor_values = [(vendor.uuid, vendor_handler.render_vendor(vendor)) + for vendor in vendors] + form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values)) + form.set_validator('vendor', self.valid_vendor_uuid) + + # configure workflow field + values = [(workflow['workflow_key'], workflow['display']) + for workflow in workflows] + form.set_widget('workflow', + dfwidget.SelectWidget(values=values)) + if len(workflows) == 1: + form.set_default('workflow', workflows[0]['workflow_key']) + + form.submit_label = "Continue" + form.cancel_url = self.get_index_url() + + # if form validates, that means user has chosen a creation + # type, so we just redirect to the appropriate "new batch of + # type X" page + if form.validate(): + workflow_key = form.validated['workflow'] + vendor_uuid = form.validated['vendor'] + url = self.request.route_url(f'{route_prefix}.create_workflow', + workflow_key=workflow_key, + vendor_uuid=vendor_uuid) + raise self.redirect(url) + + context['form'] = form + if hasattr(form, 'make_deform_form'): + context['dform'] = form.make_deform_form() + return self.render_to_response('create', context) + def query(self, session): model = self.model return session.query(model.PurchaseBatch)\ @@ -226,20 +398,40 @@ class PurchasingBatchView(BatchMasterView): def configure_form(self, f): super().configure_form(f) - model = self.model + model = self.app.model + enum = self.app.enum + route_prefix = self.get_route_prefix() + + today = self.app.today() batch = f.model_instance - app = self.get_rattail_app() - today = app.localtime().date() + workflow = self.request.matchdict.get('workflow_key') + vendor_handler = self.app.get_vendor_handler() # mode - f.set_enum('mode', self.enum.PURCHASE_BATCH_MODE) + f.set_enum('mode', enum.PURCHASE_BATCH_MODE) + + # workflow + if self.creating: + if workflow: + f.set_widget('workflow', dfwidget.HiddenWidget()) + f.set_default('workflow', workflow) + f.set_hidden('workflow') + # nb. show readonly '_workflow' + f.insert_after('workflow', '_workflow') + f.set_readonly('_workflow') + f.set_renderer('_workflow', self.render_workflow) + else: + f.set_readonly('workflow') + f.set_renderer('workflow', self.render_workflow) + else: + f.remove('workflow') # store - single_store = self.rattail_config.single_store() + single_store = self.config.single_store() if self.creating: f.replace('store', 'store_uuid') if single_store: - store = self.rattail_config.get_store(self.Session()) + store = self.config.get_store(self.Session()) f.set_widget('store_uuid', dfwidget.HiddenWidget()) f.set_default('store_uuid', store.uuid) f.set_hidden('store_uuid') @@ -263,7 +455,6 @@ class PurchasingBatchView(BatchMasterView): if self.creating: f.replace('vendor', 'vendor_uuid') f.set_label('vendor_uuid', "Vendor") - vendor_handler = app.get_vendor_handler() use_dropdown = vendor_handler.choice_uses_dropdown() if use_dropdown: vendors = self.Session.query(model.Vendor)\ @@ -313,7 +504,7 @@ class PurchasingBatchView(BatchMasterView): if buyer: buyer_display = str(buyer) elif self.creating: - buyer = app.get_employee(self.request.user) + buyer = self.app.get_employee(self.request.user) if buyer: buyer_display = str(buyer) f.set_default('buyer_uuid', buyer.uuid) @@ -324,6 +515,30 @@ class PurchasingBatchView(BatchMasterView): field_display=buyer_display, service_url=buyers_url)) f.set_label('buyer_uuid', "Buyer") + # order_file + if self.creating: + f.set_type('order_file', 'file', required=False) + else: + f.set_readonly('order_file') + f.set_renderer('order_file', self.render_downloadable_file) + + # order_parser_key + if self.creating: + kwargs = {} + if 'vendor_uuid' in self.request.matchdict: + vendor = self.Session.get(model.Vendor, + self.request.matchdict['vendor_uuid']) + if vendor: + kwargs['vendor'] = vendor + parsers = vendor_handler.get_supported_order_parsers(**kwargs) + parser_values = [(p.key, p.title) for p in parsers] + if len(parsers) == 1: + f.set_default('order_parser_key', parsers[0].key) + f.set_widget('order_parser_key', dfwidget.SelectWidget(values=parser_values)) + f.set_label('order_parser_key', "Order Parser") + else: + f.remove_field('order_parser_key') + # invoice_file if self.creating: f.set_type('invoice_file', 'file', required=False) @@ -341,7 +556,7 @@ class PurchasingBatchView(BatchMasterView): if vendor: kwargs['vendor'] = vendor - parsers = self.handler.get_supported_invoice_parsers(**kwargs) + parsers = self.batch_handler.get_supported_invoice_parsers(**kwargs) parser_values = [(p.key, p.display) for p in parsers] if len(parsers) == 1: f.set_default('invoice_parser_key', parsers[0].key) @@ -400,6 +615,35 @@ class PurchasingBatchView(BatchMasterView): 'vendor_contact', 'status_code') + # tweak some things if we are in "step 2" of creating new batch + if self.creating and workflow: + + # display vendor but do not allow changing + vendor = self.Session.get(model.Vendor, self.request.matchdict['vendor_uuid']) + if not vendor: + raise ValueError(f"vendor not found: {self.request.matchdict['vendor_uuid']}") + f.set_readonly('vendor_uuid') + f.set_default('vendor_uuid', str(vendor)) + + # cancel should take us back to choosing a workflow + f.cancel_url = self.request.route_url(f'{route_prefix}.create') + + def render_workflow(self, batch, field): + key = self.request.matchdict['workflow_key'] + info = self.get_workflow_info(key) + if info: + return info['display'] + + def get_workflow_info(self, key): + enum = self.app.enum + if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING: + return self.batch_handler.ordering_workflow_info(key) + elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING: + return self.batch_handler.receiving_workflow_info(key) + elif self.batch_mode == enum.PURCHASE_BATCH_MODE_COSTING: + return self.batch_handler.costing_workflow_info(key) + raise ValueError("unknown batch mode") + def render_store(self, batch, field): store = batch.store if not store: @@ -515,10 +759,12 @@ class PurchasingBatchView(BatchMasterView): def get_batch_kwargs(self, batch, **kwargs): kwargs = super().get_batch_kwargs(batch, **kwargs) - model = self.model + model = self.app.model kwargs['mode'] = self.batch_mode + kwargs['workflow'] = self.request.POST['workflow'] kwargs['truck_dump'] = batch.truck_dump + kwargs['order_parser_key'] = batch.order_parser_key kwargs['invoice_parser_key'] = batch.invoice_parser_key if batch.store: @@ -536,6 +782,11 @@ class PurchasingBatchView(BatchMasterView): elif batch.vendor_uuid: kwargs['vendor_uuid'] = batch.vendor_uuid + # must pull vendor from URL if it was not in form data + if 'vendor_uuid' not in kwargs and 'vendor' not in kwargs: + if 'vendor_uuid' in self.request.matchdict: + kwargs['vendor_uuid'] = self.request.matchdict['vendor_uuid'] + if batch.department: kwargs['department'] = batch.department elif batch.department_uuid: @@ -919,6 +1170,25 @@ class PurchasingBatchView(BatchMasterView): # # otherwise just view batch again # return self.get_action_url('view', batch) + @classmethod + def defaults(cls, config): + cls._purchase_batch_defaults(config) + cls._batch_defaults(config) + cls._defaults(config) + + @classmethod + def _purchase_batch_defaults(cls, config): + route_prefix = cls.get_route_prefix() + url_prefix = cls.get_url_prefix() + permission_prefix = cls.get_permission_prefix() + + # new batch using workflow X + config.add_route(f'{route_prefix}.create_workflow', + f'{url_prefix}/new/{{workflow_key}}/{{vendor_uuid}}') + config.add_view(cls, attr='create', + route_name=f'{route_prefix}.create_workflow', + permission=f'{permission_prefix}.create') + class NewProduct(colander.Schema): diff --git a/tailbone/views/purchasing/ordering.py b/tailbone/views/purchasing/ordering.py index 2e24eebb..c7cc7bfc 100644 --- a/tailbone/views/purchasing/ordering.py +++ b/tailbone/views/purchasing/ordering.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -28,14 +28,10 @@ import os import json import openpyxl -from sqlalchemy import orm -from rattail.db import model, api from rattail.core import Object -from rattail.time import localtime - -from webhelpers2.html import tags +from tailbone.db import Session from tailbone.views.purchasing import PurchasingBatchView @@ -51,6 +47,8 @@ class OrderingBatchView(PurchasingBatchView): rows_editable = True has_worksheet = True default_help_url = 'https://rattailproject.org/docs/rattail-manual/features/purchasing/ordering/index.html' + downloadable = True + configurable = True labels = { 'po_total_calculated': "PO Total", @@ -59,9 +57,14 @@ class OrderingBatchView(PurchasingBatchView): form_fields = [ 'id', 'store', - 'buyer', 'vendor', + 'description', + 'workflow', + 'order_file', + 'order_parser_key', + 'buyer', 'department', + 'params', 'purchase', 'vendor_email', 'vendor_fax', @@ -132,15 +135,26 @@ class OrderingBatchView(PurchasingBatchView): return self.enum.PURCHASE_BATCH_MODE_ORDERING def configure_form(self, f): - super(OrderingBatchView, self).configure_form(f) + super().configure_form(f) batch = f.model_instance + workflow = self.request.matchdict.get('workflow_key') # purchase if self.creating or not batch.executed or not batch.purchase: f.remove_field('purchase') + # now that all fields are setup, some final tweaks based on workflow + if self.creating and workflow: + + if workflow == 'from_scratch': + f.remove('order_file', + 'order_parser_key') + + elif workflow == 'from_file': + f.set_required('order_file') + def get_batch_kwargs(self, batch, **kwargs): - kwargs = super(OrderingBatchView, self).get_batch_kwargs(batch, **kwargs) + kwargs = super().get_batch_kwargs(batch, **kwargs) kwargs['ship_method'] = batch.ship_method kwargs['notes_to_vendor'] = batch.notes_to_vendor return kwargs @@ -155,7 +169,7 @@ class OrderingBatchView(PurchasingBatchView): * ``cases_ordered`` * ``units_ordered`` """ - super(OrderingBatchView, self).configure_row_form(f) + super().configure_row_form(f) # when editing, only certain fields should allow changes if self.editing: @@ -308,7 +322,7 @@ class OrderingBatchView(PurchasingBatchView): title = self.get_instance_title(batch) order_date = batch.date_ordered if not order_date: - order_date = localtime(self.rattail_config).date() + order_date = self.app.today() return self.render_to_response('worksheet', { 'batch': batch, @@ -369,6 +383,7 @@ class OrderingBatchView(PurchasingBatchView): of being updated. If a matching row is not found, it will not be created. """ + model = self.app.model batch = self.get_instance() try: @@ -478,13 +493,75 @@ class OrderingBatchView(PurchasingBatchView): return self.file_response(path) def get_execute_success_url(self, batch, result, **kwargs): + model = self.app.model if isinstance(result, model.Purchase): return self.request.route_url('purchases.view', uuid=result.uuid) - return super(OrderingBatchView, self).get_execute_success_url(batch, result, **kwargs) + return super().get_execute_success_url(batch, result, **kwargs) + + def configure_get_simple_settings(self): + return [ + + # workflows + {'section': 'rattail.batch', + 'option': 'purchase.allow_ordering_from_scratch', + 'type': bool, + 'default': True}, + {'section': 'rattail.batch', + 'option': 'purchase.allow_ordering_from_file', + 'type': bool, + 'default': True}, + + # vendors + {'section': 'rattail.batch', + 'option': 'purchase.allow_ordering_any_vendor', + 'type': bool, + 'default': True, + }, + ] + + def configure_get_context(self): + context = super().configure_get_context() + vendor_handler = self.app.get_vendor_handler() + + Parsers = vendor_handler.get_all_order_parsers() + Supported = vendor_handler.get_supported_order_parsers() + context['order_parsers'] = Parsers + context['order_parsers_data'] = dict([(Parser.key, Parser in Supported) + for Parser in Parsers]) + + return context + + def configure_gather_settings(self, data): + settings = super().configure_gather_settings(data) + vendor_handler = self.app.get_vendor_handler() + + supported = [] + for Parser in vendor_handler.get_all_order_parsers(): + name = f'order_parser_{Parser.key}' + if data.get(name) == 'true': + supported.append(Parser.key) + settings.append({'name': 'rattail.vendors.supported_order_parsers', + 'value': ', '.join(supported)}) + + return settings + + def configure_remove_settings(self): + super().configure_remove_settings() + + names = [ + 'rattail.vendors.supported_order_parsers', + ] + + # nb. using thread-local session here; we do not use + # self.Session b/c it may not point to Rattail + session = Session() + for name in names: + self.app.delete_setting(session, name) @classmethod def defaults(cls, config): cls._ordering_defaults(config) + cls._purchase_batch_defaults(config) cls._batch_defaults(config) cls._defaults(config) diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index de19a2b9..01858c98 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -108,7 +108,7 @@ class ReceivingBatchView(PurchasingBatchView): 'store', 'vendor', 'description', - 'receiving_workflow', + 'workflow', 'truck_dump', 'truck_dump_children_first', 'truck_dump_children', @@ -235,135 +235,18 @@ class ReceivingBatchView(PurchasingBatchView): if not self.handler.allow_truck_dump_receiving(): g.remove('truck_dump') - def create(self, form=None, **kwargs): - """ - Custom view for creating a new receiving batch. We split the process - into two steps, 1) choose and 2) create. This is because the specific - form details for creating a batch will depend on which "type" of batch - creation is to be done, and it's much easier to keep conditional logic - for that in the server instead of client-side etc. - - See also - :meth:`tailbone.views.purchasing.costing:CostingBatchView.create()` - which uses similar logic. - """ - model = self.model - route_prefix = self.get_route_prefix() - workflows = self.handler.supported_receiving_workflows() - valid_workflows = [workflow['workflow_key'] - for workflow in workflows] - - # if user has already identified their desired workflow, then we can - # just farm out to the default logic. we will of course configure our - # form differently, based on workflow, but this create() method at - # least will not need customization for that. - if self.request.matched_route.name.endswith('create_workflow'): - - redirect = self.redirect(self.request.route_url('{}.create'.format(route_prefix))) - - # however we do have one more thing to check - the workflow - # requested must of course be valid! - workflow_key = self.request.matchdict['workflow_key'] - if workflow_key not in valid_workflows: - self.request.session.flash( - "Not a supported workflow: {}".format(workflow_key), - 'error') - raise redirect - - # also, we require vendor to be correctly identified. if - # someone e.g. navigates to a URL by accident etc. we want - # to gracefully handle and redirect - uuid = self.request.matchdict['vendor_uuid'] - vendor = self.Session.get(model.Vendor, uuid) - if not vendor: - self.request.session.flash("Invalid vendor selection. " - "Please choose an existing vendor.", - 'warning') - raise redirect - - # okay now do the normal thing, per workflow - return super().create(**kwargs) - - # on the other hand, if caller provided a form, that means we are in - # the middle of some other custom workflow, e.g. "add child to truck - # dump parent" or some such. in which case we also defer to the normal - # logic, so as to not interfere with that. - if form: - return super().create(form=form, **kwargs) - - # okay, at this point we need the user to select a vendor and workflow - self.creating = True - context = {} - - # form to accept user choice of vendor/workflow - schema = NewReceivingBatch().bind(valid_workflows=valid_workflows) - form = forms.Form(schema=schema, request=self.request) - - # configure vendor field - app = self.get_rattail_app() - vendor_handler = app.get_vendor_handler() - if self.rattail_config.getbool('rattail.batch', 'purchase.supported_vendors_only'): - # only show vendors for which we have dedicated invoice parsers - vendors = {} - for parser in self.batch_handler.get_supported_invoice_parsers(): - if parser.vendor_key: - vendor = vendor_handler.get_vendor(self.Session(), - parser.vendor_key) - if vendor: - vendors[vendor.uuid] = vendor - vendors = sorted(vendors.values(), key=lambda v: v.name) - vendor_values = [(vendor.uuid, vendor_handler.render_vendor(vendor)) - for vendor in vendors] - form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values)) - else: - # user may choose *any* available vendor - use_dropdown = vendor_handler.choice_uses_dropdown() - if use_dropdown: - vendors = self.Session.query(model.Vendor)\ - .order_by(model.Vendor.id)\ - .all() - vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, vendor.name)) - for vendor in vendors] - form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values)) - if len(vendors) == 1: - form.set_default('vendor', vendors[0].uuid) - else: - vendor_display = "" - if self.request.method == 'POST': - if self.request.POST.get('vendor'): - vendor = self.Session.get(model.Vendor, self.request.POST['vendor']) - if vendor: - vendor_display = str(vendor) - vendors_url = self.request.route_url('vendors.autocomplete') - form.set_widget('vendor', forms.widgets.JQueryAutocompleteWidget( - field_display=vendor_display, service_url=vendors_url)) - form.set_validator('vendor', self.valid_vendor_uuid) - - # configure workflow field - values = [(workflow['workflow_key'], workflow['display']) - for workflow in workflows] - form.set_widget('workflow', - dfwidget.SelectWidget(values=values)) - if len(workflows) == 1: - form.set_default('workflow', workflows[0]['workflow_key']) - - form.submit_label = "Continue" - form.cancel_url = self.get_index_url() - - # if form validates, that means user has chosen a creation type, so we - # just redirect to the appropriate "new batch of type X" page - if form.validate(): - workflow_key = form.validated['workflow'] - vendor_uuid = form.validated['vendor'] - url = self.request.route_url('{}.create_workflow'.format(route_prefix), - workflow_key=workflow_key, - vendor_uuid=vendor_uuid) - raise self.redirect(url) - - context['form'] = form - if hasattr(form, 'make_deform_form'): - context['dform'] = form.make_deform_form() - return self.render_to_response('create', context) + def get_supported_vendors(self): + """ """ + vendor_handler = self.app.get_vendor_handler() + vendors = {} + for parser in self.batch_handler.get_supported_invoice_parsers(): + if parser.vendor_key: + vendor = vendor_handler.get_vendor(self.Session(), + parser.vendor_key) + if vendor: + vendors[vendor.uuid] = vendor + vendors = sorted(vendors.values(), key=lambda v: v.name) + return vendors def row_deletable(self, row): @@ -404,13 +287,7 @@ class ReceivingBatchView(PurchasingBatchView): # cancel should take us back to choosing a workflow f.cancel_url = self.request.route_url('{}.create'.format(route_prefix)) - # receiving_workflow - if self.creating and workflow: - f.set_readonly('receiving_workflow') - f.set_renderer('receiving_workflow', self.render_receiving_workflow) - else: - f.remove('receiving_workflow') - + # TODO: remove this # batch_type if self.creating: f.set_widget('batch_type', dfwidget.HiddenWidget()) @@ -525,7 +402,7 @@ class ReceivingBatchView(PurchasingBatchView): # multiple invoice files (if applicable) if (not self.creating - and batch.get_param('receiving_workflow') == 'from_multi_invoice'): + and batch.get_param('workflow') == 'from_multi_invoice'): if 'invoice_files' not in f: f.insert_before('invoice_file', 'invoice_files') @@ -624,12 +501,6 @@ class ReceivingBatchView(PurchasingBatchView): items.append(HTML.tag('li', c=[link])) return HTML.tag('ul', c=items) - def render_receiving_workflow(self, batch, field): - key = self.request.matchdict['workflow_key'] - info = self.handler.receiving_workflow_info(key) - if info: - return info['display'] - def get_visible_params(self, batch): params = super().get_visible_params(batch) @@ -654,42 +525,40 @@ class ReceivingBatchView(PurchasingBatchView): def get_batch_kwargs(self, batch, **kwargs): kwargs = super().get_batch_kwargs(batch, **kwargs) - batch_type = self.request.POST['batch_type'] # must pull vendor from URL if it was not in form data if 'vendor_uuid' not in kwargs and 'vendor' not in kwargs: if 'vendor_uuid' in self.request.matchdict: kwargs['vendor_uuid'] = self.request.matchdict['vendor_uuid'] - # TODO: ugh should just have workflow and no batch_type - kwargs['receiving_workflow'] = batch_type - if batch_type == 'from_scratch': + workflow = kwargs['workflow'] + if workflow == 'from_scratch': kwargs.pop('truck_dump_batch', None) kwargs.pop('truck_dump_batch_uuid', None) - elif batch_type == 'from_invoice': + elif workflow == 'from_invoice': pass - elif batch_type == 'from_multi_invoice': + elif workflow == 'from_multi_invoice': pass - elif batch_type == 'from_po': + elif workflow == 'from_po': # TODO: how to best handle this field? this doesn't seem flexible kwargs['purchase_key'] = batch.purchase_uuid - elif batch_type == 'from_po_with_invoice': + elif workflow == 'from_po_with_invoice': # TODO: how to best handle this field? this doesn't seem flexible kwargs['purchase_key'] = batch.purchase_uuid - elif batch_type == 'truck_dump_children_first': + elif workflow == 'truck_dump_children_first': kwargs['truck_dump'] = True kwargs['truck_dump_children_first'] = True kwargs['order_quantities_known'] = True # TODO: this makes sense in some cases, but all? # (should just omit that field when not relevant) kwargs['date_ordered'] = None - elif batch_type == 'truck_dump_children_last': + elif workflow == 'truck_dump_children_last': kwargs['truck_dump'] = True kwargs['truck_dump_ready'] = True # TODO: this makes sense in some cases, but all? # (should just omit that field when not relevant) kwargs['date_ordered'] = None - elif batch_type.startswith('truck_dump_child'): + elif workflow.startswith('truck_dump_child'): truck_dump = self.get_instance() kwargs['store'] = truck_dump.store kwargs['vendor'] = truck_dump.vendor @@ -1986,6 +1855,12 @@ class ReceivingBatchView(PurchasingBatchView): 'type': bool}, # vendors + {'section': 'rattail.batch', + 'option': 'purchase.allow_receiving_any_vendor', + 'type': bool}, + # TODO: deprecated; can remove this once all live config + # is updated. but for now it remains so this setting is + # auto-deleted {'section': 'rattail.batch', 'option': 'purchase.supported_vendors_only', 'type': bool}, @@ -2036,6 +1911,7 @@ class ReceivingBatchView(PurchasingBatchView): @classmethod def defaults(cls, config): cls._receiving_defaults(config) + cls._purchase_batch_defaults(config) cls._batch_defaults(config) cls._defaults(config) @@ -2043,17 +1919,11 @@ class ReceivingBatchView(PurchasingBatchView): def _receiving_defaults(cls, config): rattail_config = config.registry.settings.get('rattail_config') route_prefix = cls.get_route_prefix() - url_prefix = cls.get_url_prefix() instance_url_prefix = cls.get_instance_url_prefix() model_key = cls.get_model_key() model_title = cls.get_model_title() permission_prefix = cls.get_permission_prefix() - # new receiving batch using workflow X - config.add_route('{}.create_workflow'.format(route_prefix), '{}/new/{{workflow_key}}/{{vendor_uuid}}'.format(url_prefix)) - config.add_view(cls, attr='create', route_name='{}.create_workflow'.format(route_prefix), - permission='{}.create'.format(permission_prefix)) - # row-level receiving config.add_route('{}.receive_row'.format(route_prefix), '{}/rows/{{row_uuid}}/receive'.format(instance_url_prefix)) config.add_view(cls, attr='receive_row', route_name='{}.receive_row'.format(route_prefix), @@ -2106,33 +1976,6 @@ class ReceivingBatchView(PurchasingBatchView): permission='{}.auto_receive'.format(permission_prefix)) -@colander.deferred -def valid_workflow(node, kw): - """ - Deferred validator for ``workflow`` field, for new batches. - """ - valid_workflows = kw['valid_workflows'] - - def validate(node, value): - # we just need to provide possible values, and let stock validator - # handle the rest - oneof = colander.OneOf(valid_workflows) - return oneof(node, value) - - return validate - - -class NewReceivingBatch(colander.Schema): - """ - Schema for choosing which "type" of new receiving batch should be created. - """ - vendor = colander.SchemaNode(colander.String(), - label="Vendor") - - workflow = colander.SchemaNode(colander.String(), - validator=valid_workflow) - - class ReceiveRowForm(colander.MappingSchema): mode = colander.SchemaNode(colander.String(), From 535317e4f769b2f39121060f70ed7a1c4a013aed Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 22 Oct 2024 15:04:40 -0500 Subject: [PATCH 58/85] fix: avoid deprecated method to suggest username --- tailbone/views/people.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tailbone/views/people.py b/tailbone/views/people.py index b6a4c0b9..d288b551 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -1382,8 +1382,8 @@ class PersonView(MasterView): } if not context['users']: - context['suggested_username'] = auth.generate_unique_username(self.Session(), - person=person) + context['suggested_username'] = auth.make_unique_username(self.Session(), + person=person) return context From 28f90ad6b5777dfe1c91db2d90c5ccccc678ad5e Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 22 Oct 2024 17:09:29 -0500 Subject: [PATCH 59/85] =?UTF-8?q?bump:=20version=200.21.11=20=E2=86=92=200?= =?UTF-8?q?.22.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 ++++++++++ pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c31ae92..8ed82c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.22.0 (2024-10-22) + +### Feat + +- add support for new ordering batch from parsed file + +### Fix + +- avoid deprecated method to suggest username + ## v0.21.11 (2024-10-03) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 5b63a71f..b928ec9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.21.11" +version = "0.22.0" description = "Backoffice Web Application for Rattail" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 9a6f8970aeb6117d9240b4bd4f024bca4ee136cf Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 23 Oct 2024 09:46:14 -0500 Subject: [PATCH 60/85] fix: avoid deprecated grid method --- tailbone/views/master.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index baf63caa..2e7ac147 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -412,7 +412,7 @@ class MasterView(View): session = self.Session() kwargs.setdefault('paginated', False) grid = self.make_grid(session=session, **kwargs) - return grid.make_visible_data() + return grid.get_visible_data() def get_grid_columns(self): """ @@ -1710,7 +1710,7 @@ class MasterView(View): kwargs.setdefault('paginated', False) kwargs.setdefault('sortable', sort) grid = self.make_row_grid(session=session, **kwargs) - return grid.make_visible_data() + return grid.get_visible_data() @classmethod def get_row_url_prefix(cls): From 54220601edfde3435420d5e04b8e4883ae4b4d53 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Fri, 1 Nov 2024 17:47:46 -0500 Subject: [PATCH 61/85] fix: fix submit button for running problem report esp. on Chrome(-based) browsers --- tailbone/templates/reports/problems/view.mako | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tailbone/templates/reports/problems/view.mako b/tailbone/templates/reports/problems/view.mako index 00ac1503..5cdf2be5 100644 --- a/tailbone/templates/reports/problems/view.mako +++ b/tailbone/templates/reports/problems/view.mako @@ -45,11 +45,10 @@ <b-button @click="runReportShowDialog = false"> Cancel </b-button> - ${h.form(master.get_action_url('execute', instance))} + ${h.form(master.get_action_url('execute', instance), **{'@submit': 'runReportSubmitting = true'})} ${h.csrf_token(request)} <b-button type="is-primary" native-type="submit" - @click="runReportSubmitting = true" :disabled="runReportSubmitting" icon-pack="fas" icon-left="arrow-circle-right"> From 29743e70b7cba3a1b53917c24d0d5a1aaf70972e Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Sat, 2 Nov 2024 16:56:28 -0500 Subject: [PATCH 62/85] =?UTF-8?q?bump:=20version=200.22.0=20=E2=86=92=200.?= =?UTF-8?q?22.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed82c5d..4dde0159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.22.1 (2024-11-02) + +### Fix + +- fix submit button for running problem report +- avoid deprecated grid method + ## v0.22.0 (2024-10-22) ### Feat diff --git a/pyproject.toml b/pyproject.toml index b928ec9b..a4a64038 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.22.0" +version = "0.22.1" description = "Backoffice Web Application for Rattail" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 3f27f626df9f5d2ccb6ae6d52bba0abaa09ecca9 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Sun, 10 Nov 2024 19:16:45 -0600 Subject: [PATCH 63/85] fix: avoid deprecated import --- tailbone/api/master.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tailbone/api/master.py b/tailbone/api/master.py index 2d17339e..551d6428 100644 --- a/tailbone/api/master.py +++ b/tailbone/api/master.py @@ -26,7 +26,6 @@ Tailbone Web API - Master View import json -from rattail.config import parse_bool from rattail.db.util import get_fieldnames from cornice import resource, Service @@ -185,7 +184,7 @@ class APIMasterView(APIView): if sortcol: spec = { 'field': sortcol.field_name, - 'direction': 'asc' if parse_bool(self.request.params['ascending']) else 'desc', + 'direction': 'asc' if self.config.parse_bool(self.request.params['ascending']) else 'desc', } if sortcol.model_name: spec['model'] = sortcol.model_name From 772b6610cbd99199cd4aae9bf4bbc3c5b748d829 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 12 Nov 2024 18:26:36 -0600 Subject: [PATCH 64/85] fix: always define `app` attr for ViewSupplement --- tailbone/views/master.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 2e7ac147..21a5e58f 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -903,7 +903,7 @@ class MasterView(View): def valid_employee_uuid(self, node, value): if value: - model = self.model + model = self.app.model employee = self.Session.get(model.Employee, value) if not employee: node.raise_invalid("Employee not found") @@ -939,7 +939,7 @@ class MasterView(View): def valid_vendor_uuid(self, node, value): if value: - model = self.model + model = self.app.model vendor = self.Session.get(model.Vendor, value) if not vendor: node.raise_invalid("Vendor not found") @@ -1382,7 +1382,7 @@ class MasterView(View): return classes def make_revisions_grid(self, obj, empty_data=False): - model = self.model + model = self.app.model route_prefix = self.get_route_prefix() row_url = lambda txn, i: self.request.route_url(f'{route_prefix}.version', uuid=obj.uuid, @@ -2153,7 +2153,7 @@ class MasterView(View): Thread target for executing an object. """ app = self.get_rattail_app() - model = self.model + model = self.app.model session = app.make_session() obj = self.get_instance_for_key(key, session) user = session.get(model.User, user_uuid) @@ -2594,7 +2594,7 @@ class MasterView(View): """ # nb. self.Session may differ, so use tailbone.db.Session session = Session() - model = self.model + model = self.app.model route_prefix = self.get_route_prefix() info = session.query(model.TailbonePageHelp)\ @@ -2617,7 +2617,7 @@ class MasterView(View): """ # nb. self.Session may differ, so use tailbone.db.Session session = Session() - model = self.model + model = self.app.model route_prefix = self.get_route_prefix() info = session.query(model.TailbonePageHelp)\ @@ -2639,7 +2639,7 @@ class MasterView(View): # nb. self.Session may differ, so use tailbone.db.Session session = Session() - model = self.model + model = self.app.model route_prefix = self.get_route_prefix() schema = colander.Schema() @@ -2673,7 +2673,7 @@ class MasterView(View): # nb. self.Session may differ, so use tailbone.db.Session session = Session() - model = self.model + model = self.app.model route_prefix = self.get_route_prefix() schema = colander.Schema() @@ -5541,7 +5541,7 @@ class MasterView(View): input_file_templates=True, output_file_templates=True): app = self.get_rattail_app() - model = self.model + model = self.app.model names = [] if simple_settings is None: @@ -6100,7 +6100,7 @@ class MasterView(View): renderer='json') -class ViewSupplement(object): +class ViewSupplement: """ Base class for view "supplements" - which are sort of like plugins which can "supplement" certain aspects of the view. @@ -6127,6 +6127,7 @@ class ViewSupplement(object): def __init__(self, master): self.master = master self.request = master.request + self.app = master.app self.model = master.model self.rattail_config = master.rattail_config self.Session = master.Session @@ -6160,7 +6161,7 @@ class ViewSupplement(object): This is accomplished by subjecting the current base query to a join, e.g. something like:: - model = self.model + model = self.app.model query = query.outerjoin(model.MyExtension) return query """ From 9e55717041f9955cb61a971a62340acb5473ab5f Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 12 Nov 2024 18:28:41 -0600 Subject: [PATCH 65/85] fix: show continuum operation type when viewing version history --- tailbone/diffs.py | 6 +++++- tailbone/templates/master/view.mako | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tailbone/diffs.py b/tailbone/diffs.py index 98253c57..8303d9e9 100644 --- a/tailbone/diffs.py +++ b/tailbone/diffs.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2024 Lance Edgar # # This file is part of Rattail. # @@ -27,6 +27,8 @@ Tools for displaying data diffs import sqlalchemy as sa import sqlalchemy_continuum as continuum +from rattail.enum import CONTINUUM_OPERATION + from pyramid.renderers import render from webhelpers2.html import HTML @@ -273,6 +275,8 @@ class VersionDiff(Diff): return { 'key': id(self.version), 'model_title': self.title, + 'operation': CONTINUUM_OPERATION.get(self.version.operation_type, + self.version.operation_type), 'diff_class': self.nature, 'fields': self.fields, 'values': values, diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako index 0a1f9c62..118c028c 100644 --- a/tailbone/templates/master/view.mako +++ b/tailbone/templates/master/view.mako @@ -196,6 +196,7 @@ <p class="block has-text-weight-bold"> {{ version.model_title }} + ({{ version.operation }}) </p> <table class="diff monospace is-size-7" From 20b3f87dbef3346de939d5eabaa18224cc146cce Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 12 Nov 2024 18:30:50 -0600 Subject: [PATCH 66/85] fix: add basic master view for Product Costs --- tailbone/menus.py | 10 +++++ tailbone/views/products.py | 77 +++++++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/tailbone/menus.py b/tailbone/menus.py index 3ddee095..09d6f3f0 100644 --- a/tailbone/menus.py +++ b/tailbone/menus.py @@ -394,6 +394,11 @@ class TailboneMenuHandler(WuttaMenuHandler): 'route': 'products', 'perm': 'products.list', }, + { + 'title': "Product Costs", + 'route': 'product_costs', + 'perm': 'product_costs.list', + }, { 'title': "Departments", 'route': 'departments', @@ -451,6 +456,11 @@ class TailboneMenuHandler(WuttaMenuHandler): 'route': 'vendors', 'perm': 'vendors.list', }, + { + 'title': "Product Costs", + 'route': 'product_costs', + 'perm': 'product_costs.list', + }, {'type': 'sep'}, { 'title': "Ordering", diff --git a/tailbone/views/products.py b/tailbone/views/products.py index c546a0f4..ae6c550c 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -34,7 +34,7 @@ import sqlalchemy_continuum as continuum from rattail import enum, pod, sil from rattail.db import api, auth, Session as RattailSession -from rattail.db.model import Product, PendingProduct, CustomerOrderItem +from rattail.db.model import Product, PendingProduct, ProductCost, CustomerOrderItem from rattail.gpc import GPC from rattail.threads import Thread from rattail.exceptions import LabelPrintingError @@ -2668,6 +2668,78 @@ class PendingProductView(MasterView): permission=f'{permission_prefix}.ignore_product') +class ProductCostView(MasterView): + """ + Master view for Product Costs + """ + model_class = ProductCost + route_prefix = 'product_costs' + url_prefix = '/products/costs' + has_versions = True + + grid_columns = [ + '_product_key_', + 'vendor', + 'preference', + 'code', + 'case_size', + 'case_cost', + 'pack_size', + 'pack_cost', + 'unit_cost', + ] + + def query(self, session): + """ """ + query = super().query(session) + model = self.app.model + + # always join on Product + return query.join(model.Product) + + def configure_grid(self, g): + """ """ + super().configure_grid(g) + model = self.app.model + + # product key + field = self.get_product_key_field() + g.set_renderer(field, self.render_product_key) + g.set_sorter(field, getattr(model.Product, field)) + g.set_sort_defaults(field) + g.set_filter(field, getattr(model.Product, field)) + + # vendor + g.set_joiner('vendor', lambda q: q.join(model.Vendor)) + g.set_sorter('vendor', model.Vendor.name) + g.set_filter('vendor', model.Vendor.name, label="Vendor Name") + + def render_product_key(self, cost, field): + """ """ + handler = self.app.get_products_handler() + return handler.render_product_key(cost.product) + + def configure_form(self, f): + """ """ + super().configure_form(f) + + # product + f.set_renderer('product', self.render_product) + if 'product_uuid' in f and 'product' in f: + f.remove('product') + f.replace('product_uuid', 'product') + + # vendor + f.set_renderer('vendor', self.render_vendor) + if 'vendor_uuid' in f and 'vendor' in f: + f.remove('vendor') + f.replace('vendor_uuid', 'vendor') + + # futures + # TODO: should eventually show a subgrid here? + f.remove('futures') + + def defaults(config, **kwargs): base = globals() @@ -2677,6 +2749,9 @@ def defaults(config, **kwargs): PendingProductView = kwargs.get('PendingProductView', base['PendingProductView']) PendingProductView.defaults(config) + ProductCostView = kwargs.get('ProductCostView', base['ProductCostView']) + ProductCostView.defaults(config) + def includeme(config): defaults(config) From ac439c949b1760e46975292a7c19b81664b0b5f8 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 12 Nov 2024 19:45:24 -0600 Subject: [PATCH 67/85] fix: use local/custom enum for continuum operations since we can't rely on that existing in rattail proper, due to it not always having sqlalchemy --- tailbone/diffs.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tailbone/diffs.py b/tailbone/diffs.py index 8303d9e9..2e582b15 100644 --- a/tailbone/diffs.py +++ b/tailbone/diffs.py @@ -27,8 +27,6 @@ Tools for displaying data diffs import sqlalchemy as sa import sqlalchemy_continuum as continuum -from rattail.enum import CONTINUUM_OPERATION - from pyramid.renderers import render from webhelpers2.html import HTML @@ -272,11 +270,21 @@ class VersionDiff(Diff): for field in self.fields: values[field] = {'before': self.render_old_value(field), 'after': self.render_new_value(field)} + + operation = None + if self.version.operation_type == continuum.Operation.INSERT: + operation = 'INSERT' + elif self.version.operation_type == continuum.Operation.UPDATE: + operation = 'UPDATE' + elif self.version.operation_type == continuum.Operation.DELETE: + operation = 'DELETE' + else: + operation = self.version.operation_type + return { 'key': id(self.version), 'model_title': self.title, - 'operation': CONTINUUM_OPERATION.get(self.version.operation_type, - self.version.operation_type), + 'operation': operation, 'diff_class': self.nature, 'fields': self.fields, 'values': values, From bcaf0d08bcab4fe040504986eee3735b814b50d9 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 18 Nov 2024 14:08:10 -0600 Subject: [PATCH 68/85] =?UTF-8?q?bump:=20version=200.22.1=20=E2=86=92=200.?= =?UTF-8?q?22.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 ++++++++++ pyproject.toml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dde0159..b7167b3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.22.2 (2024-11-18) + +### Fix + +- use local/custom enum for continuum operations +- add basic master view for Product Costs +- show continuum operation type when viewing version history +- always define `app` attr for ViewSupplement +- avoid deprecated import + ## v0.22.1 (2024-11-02) ### Fix diff --git a/pyproject.toml b/pyproject.toml index a4a64038..ef7d3584 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.22.1" +version = "0.22.2" description = "Backoffice Web Application for Rattail" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 980031f5245f814b3313a4e0438cfae4218a72dc Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Mon, 18 Nov 2024 14:59:50 -0600 Subject: [PATCH 69/85] fix: avoid error for trainwreck query when not a customer when viewing a person's profile, who does not have a customer record, the trainwreck query can't really return anything since it normally should be matching on the customer ID --- tailbone/views/people.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tailbone/views/people.py b/tailbone/views/people.py index d288b551..405b1ca3 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -564,15 +564,19 @@ class PersonView(MasterView): Method which must return the base query for the profile's POS Transactions grid data. """ - app = self.get_rattail_app() - customer = app.get_customer(person) + customer = self.app.get_customer(person) - key_field = app.get_customer_key_field() - customer_key = getattr(customer, key_field) - if customer_key is not None: - customer_key = str(customer_key) + if customer: + key_field = self.app.get_customer_key_field() + customer_key = getattr(customer, key_field) + if customer_key is not None: + customer_key = str(customer_key) + else: + # nb. this should *not* match anything, so query returns + # no results.. + customer_key = person.uuid - trainwreck = app.get_trainwreck_handler() + trainwreck = self.app.get_trainwreck_handler() model = trainwreck.get_model() query = TrainwreckSession.query(model.Transaction)\ .filter(model.Transaction.customer_id == customer_key) From 993f066f2cb5da9bfabcf59a81627e5ff20dd7df Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 19 Nov 2024 15:45:37 -0600 Subject: [PATCH 70/85] =?UTF-8?q?bump:=20version=200.22.2=20=E2=86=92=200.?= =?UTF-8?q?22.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7167b3c..5ec4ef5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.22.3 (2024-11-19) + +### Fix + +- avoid error for trainwreck query when not a customer + ## v0.22.2 (2024-11-18) ### Fix diff --git a/pyproject.toml b/pyproject.toml index ef7d3584..2dca88db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.22.2" +version = "0.22.3" description = "Backoffice Web Application for Rattail" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 7171c7fb06fa634a0688f525202a4b898868a8d7 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Tue, 19 Nov 2024 20:53:23 -0600 Subject: [PATCH 71/85] fix: use vmodel for confirm password widget input since previously this did not work at all for butterball (vue3 + oruga) - although it was never clear why per se.. Refs: #1 --- tailbone/templates/deform/checked_password.pt | 4 +- tailbone/views/auth.py | 40 ++++++++----------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/tailbone/templates/deform/checked_password.pt b/tailbone/templates/deform/checked_password.pt index f78c0b85..2121f01d 100644 --- a/tailbone/templates/deform/checked_password.pt +++ b/tailbone/templates/deform/checked_password.pt @@ -1,6 +1,7 @@ <div i18n:domain="deform" tal:omit-tag="" tal:define="oid oid|field.oid; name name|field.name; + vmodel vmodel|'field_model_' + name; css_class css_class|field.widget.css_class; style style|field.widget.style;"> @@ -8,7 +9,7 @@ ${field.start_mapping()} <b-input type="password" name="${name}" - value="${field.widget.redisplay and cstruct or ''}" + v-model="${vmodel}" tal:attributes="class string: form-control ${css_class or ''}; style style; attributes|field.widget.attributes|{};" @@ -18,7 +19,6 @@ </b-input> <b-input type="password" name="${name}-confirm" - value="${field.widget.redisplay and confirm or ''}" tal:attributes="class string: form-control ${css_class or ''}; style style; confirm_attributes|field.widget.confirm_attributes|{};" diff --git a/tailbone/views/auth.py b/tailbone/views/auth.py index a54a19a9..1338c107 100644 --- a/tailbone/views/auth.py +++ b/tailbone/views/auth.py @@ -44,28 +44,6 @@ class UserLogin(colander.MappingSchema): widget=dfwidget.PasswordWidget()) -@colander.deferred -def current_password_correct(node, kw): - request = kw['request'] - app = request.rattail_config.get_app() - auth = app.get_auth_handler() - user = kw['user'] - def validate(node, value): - if not auth.authenticate_user(Session(), user.username, value): - raise colander.Invalid(node, "The password is incorrect") - return validate - - -class ChangePassword(colander.MappingSchema): - - current_password = colander.SchemaNode(colander.String(), - widget=dfwidget.PasswordWidget(), - validator=current_password_correct) - - new_password = colander.SchemaNode(colander.String(), - widget=dfwidget.CheckedPasswordWidget()) - - class AuthenticationView(View): def forbidden(self): @@ -181,7 +159,23 @@ class AuthenticationView(View): self.request.user)) return self.redirect(self.request.get_referrer()) - schema = ChangePassword().bind(user=self.request.user, request=self.request) + def check_user_password(node, value): + auth = self.app.get_auth_handler() + user = self.request.user + if not auth.check_user_password(user, value): + node.raise_invalid("The password is incorrect") + + schema = colander.Schema() + + schema.add(colander.SchemaNode(colander.String(), + name='current_password', + widget=dfwidget.PasswordWidget(), + validator=check_user_password)) + + schema.add(colander.SchemaNode(colander.String(), + name='new_password', + widget=dfwidget.CheckedPasswordWidget())) + form = forms.Form(schema=schema, request=self.request) if form.validate(): auth = self.app.get_auth_handler() From aace6033c5ba63f0ae5b6c7e458702483b2e6c5f Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Wed, 20 Nov 2024 20:16:06 -0600 Subject: [PATCH 72/85] fix: avoid error in product search for duplicated key --- tailbone/views/products.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tailbone/views/products.py b/tailbone/views/products.py index ae6c550c..8461ae03 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -1857,7 +1857,8 @@ class ProductView(MasterView): lookup_fields.append('alt_code') if lookup_fields: product = self.products_handler.locate_product_for_entry( - session, term, lookup_fields=lookup_fields) + session, term, lookup_fields=lookup_fields, + first_if_multiple=True) if product: final_results.append(self.search_normalize_result(product)) From f1c8ffedda2b88bd9b68faf3ec2161ede67ee972 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@edbob.org> Date: Fri, 22 Nov 2024 12:57:04 -0600 Subject: [PATCH 73/85] =?UTF-8?q?bump:=20version=200.22.3=20=E2=86=92=200.?= =?UTF-8?q?22.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec4ef5c..b3b51f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.22.4 (2024-11-22) + +### Fix + +- avoid error in product search for duplicated key +- use vmodel for confirm password widget input + ## v0.22.3 (2024-11-19) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 2dca88db..bde9bf89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.22.3" +version = "0.22.4" description = "Backoffice Web Application for Rattail" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 2c269b640b1f72ac2cf9fea6a051d496096e0a8c Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Sun, 1 Dec 2024 18:12:30 -0600 Subject: [PATCH 74/85] fix: let caller request safe HTML literal for rendered grid table mostly just for convenience --- tailbone/grids/core.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 73de42c6..134642dd 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -1223,6 +1223,7 @@ class Grid(WuttaGrid): def render_table_element(self, template='/grids/b-table.mako', data_prop='gridData', empty_labels=False, + literal=False, **kwargs): """ This is intended for ad-hoc "small" grids with static data. Renders @@ -1239,7 +1240,10 @@ class Grid(WuttaGrid): if context['paginated']: context.setdefault('per_page', 20) context['view_click_handler'] = self.get_view_click_handler() - return render(template, context) + result = render(template, context) + if literal: + result = HTML.literal(result) + return result def get_view_click_handler(self): """ """ From 23bdde245abae2721b02c06eec2e0e172c3e53c6 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Tue, 10 Dec 2024 12:34:34 -0600 Subject: [PATCH 75/85] fix: require newer wuttaweb --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bde9bf89..dc66e364 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ dependencies = [ "transaction", "waitress", "WebHelpers2", - "WuttaWeb>=0.14.0", + "WuttaWeb>=0.16.2", "zope.sqlalchemy>=1.5", ] From 7e559a01b3cdcfc3704b7ffa72cc2ec3df4c73f2 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Tue, 10 Dec 2024 12:52:49 -0600 Subject: [PATCH 76/85] fix: require newer rattail lib --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dc66e364..8c0c2c15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "pyramid_mako", "pyramid_retry", "pyramid_tm", - "rattail[db,bouncer]>=0.18.5", + "rattail[db,bouncer]>=0.21.1", "sa-filters", "simplejson", "transaction", From 358b3b75a534daa7c84decd64566aca5d1c29328 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Tue, 10 Dec 2024 13:05:32 -0600 Subject: [PATCH 77/85] fix: whoops this is latest rattail --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8c0c2c15..759510ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "pyramid_mako", "pyramid_retry", "pyramid_tm", - "rattail[db,bouncer]>=0.21.1", + "rattail[db,bouncer]>=0.20.1", "sa-filters", "simplejson", "transaction", From 950db697a0306a87306facf07ca32ad1614341c9 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Mon, 16 Dec 2024 12:46:45 -0600 Subject: [PATCH 78/85] =?UTF-8?q?bump:=20version=200.22.4=20=E2=86=92=200.?= =?UTF-8?q?22.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b51f8d..cbacf2a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.22.5 (2024-12-16) + +### Fix + +- whoops this is latest rattail +- require newer rattail lib +- require newer wuttaweb +- let caller request safe HTML literal for rendered grid table + ## v0.22.4 (2024-11-22) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 759510ba..9c164772 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.22.4" +version = "0.22.5" description = "Backoffice Web Application for Rattail" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From c7ee9de9eb3b86c40e99987c10843bd4bee142f9 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Sat, 28 Dec 2024 16:43:22 -0600 Subject: [PATCH 79/85] fix: register vue3 form component for products -> make batch --- tailbone/templates/products/batch.mako | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tailbone/templates/products/batch.mako b/tailbone/templates/products/batch.mako index 9f969468..db029e5a 100644 --- a/tailbone/templates/products/batch.mako +++ b/tailbone/templates/products/batch.mako @@ -55,19 +55,20 @@ </%def> <%def name="render_form_template()"> - <script type="text/x-template" id="${form.component}-template"> + <script type="text/x-template" id="${form.vue_tagname}-template"> ${self.render_form_innards()} </script> </%def> <%def name="modify_vue_vars()"> ${parent.modify_vue_vars()} + <% request.register_component(form.vue_tagname, form.vue_component) %> <script> ## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako) let ${form.vue_component} = { - template: '#${form.component}-template', + template: '#${form.vue_tagname}-template', methods: { ## TODO: deprecate / remove the latter option here From e0ebd43e7abaa3292dd252135bc2d880b6b312ca Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Sat, 1 Feb 2025 15:18:12 -0600 Subject: [PATCH 80/85] =?UTF-8?q?bump:=20version=200.22.5=20=E2=86=92=200.?= =?UTF-8?q?22.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbacf2a5..0b1726a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.22.6 (2025-02-01) + +### Fix + +- register vue3 form component for products -> make batch + ## v0.22.5 (2024-12-16) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 9c164772..9e83df80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.22.5" +version = "0.22.6" description = "Backoffice Web Application for Rattail" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -59,7 +59,7 @@ dependencies = [ "transaction", "waitress", "WebHelpers2", - "WuttaWeb>=0.16.2", + "WuttaWeb>=0.21.0", "zope.sqlalchemy>=1.5", ] From 4221fa50dd95771c84c20473381edcaff006043d Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Fri, 14 Feb 2025 11:37:21 -0600 Subject: [PATCH 81/85] fix: fix warning msg for deprecated Grid param --- tailbone/grids/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 134642dd..56b97b86 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -235,7 +235,7 @@ class Grid(WuttaGrid): if 'pageable' in kwargs: warnings.warn("pageable param is deprecated for Grid(); " - "please use vue_tagname param instead", + "please use paginated param instead", DeprecationWarning, stacklevel=2) kwargs.setdefault('paginated', kwargs.pop('pageable')) From 7348eec671542fa1317ad68a0816948ee96c76ac Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Tue, 18 Feb 2025 11:16:23 -0600 Subject: [PATCH 82/85] fix: stop using old config for logo image url on login page --- tailbone/views/auth.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tailbone/views/auth.py b/tailbone/views/auth.py index 1338c107..eceab803 100644 --- a/tailbone/views/auth.py +++ b/tailbone/views/auth.py @@ -94,10 +94,6 @@ class AuthenticationView(View): else: self.request.session.flash("Invalid username or password", 'error') - image_url = self.rattail_config.get( - 'tailbone', 'main_image_url', - default=self.request.static_url('tailbone:static/img/home_logo.png')) - # nb. hacky..but necessary, to add the refs, for autofocus # (also add key handler, so ENTER acts like TAB) dform = form.make_deform_form() @@ -110,7 +106,6 @@ class AuthenticationView(View): return { 'form': form, 'referrer': referrer, - 'image_url': image_url, 'index_title': app.get_node_title(), 'help_url': global_help_url(self.rattail_config), } From a6508154cb93a376a7ec93efa930534c674364f8 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Tue, 18 Feb 2025 12:13:28 -0600 Subject: [PATCH 83/85] docs: update intersphinx doc links per server migration --- docs/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 52e384f5..ade4c92a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,10 +27,10 @@ templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] intersphinx_mapping = { - 'rattail': ('https://rattailproject.org/docs/rattail/', None), + 'rattail': ('https://docs.wuttaproject.org/rattail/', None), 'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None), - 'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None), - 'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None), + 'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None), + 'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None), } # allow todo entries to show up From e2582ffec5f84f97df9cc7d2fdcdf5201b2d135f Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Wed, 19 Feb 2025 10:33:39 -0600 Subject: [PATCH 84/85] =?UTF-8?q?bump:=20version=200.22.6=20=E2=86=92=200.?= =?UTF-8?q?22.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b1726a4..c974b3a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to Tailbone will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## v0.22.7 (2025-02-19) + +### Fix + +- stop using old config for logo image url on login page +- fix warning msg for deprecated Grid param + ## v0.22.6 (2025-02-01) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 9e83df80..a7214a8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.22.6" +version = "0.22.7" description = "Backoffice Web Application for Rattail" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From e15045380171617b32f9dca6bcbda8b2c2472310 Mon Sep 17 00:00:00 2001 From: Lance Edgar <lance@wuttaproject.org> Date: Wed, 5 Mar 2025 10:34:52 -0600 Subject: [PATCH 85/85] fix: add startup hack for tempmon DB model --- tailbone/app.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tailbone/app.py b/tailbone/app.py index b7262866..d2d0c5ef 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -62,6 +62,17 @@ def make_rattail_config(settings): # nb. this is for compaibility with wuttaweb settings['wutta_config'] = rattail_config + # must import all sqlalchemy models before things get rolling, + # otherwise can have errors about continuum TransactionMeta class + # not yet mapped, when relevant pages are first requested... + # cf. https://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/database/sqlalchemy.html#importing-all-sqlalchemy-models + # hat tip to https://stackoverflow.com/a/59241485 + if getattr(rattail_config, 'tempmon_engine', None): + from rattail_tempmon.db import model as tempmon_model, Session as TempmonSession + tempmon_session = TempmonSession() + tempmon_session.query(tempmon_model.Appliance).first() + tempmon_session.close() + # configure database sessions if hasattr(rattail_config, 'appdb_engine'): tailbone.db.Session.configure(bind=rattail_config.appdb_engine)