From 25e62fe6ef06ae2c9366e6f0c9c4445771e5bb16 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 14 Jul 2024 11:47:15 -0500 Subject: [PATCH 001/142] fix: fix bug when making "integration" menus per recent refactor --- tailbone/menus.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tailbone/menus.py b/tailbone/menus.py index 0752c22d..9048ae43 100644 --- a/tailbone/menus.py +++ b/tailbone/menus.py @@ -281,8 +281,9 @@ class TailboneMenuHandler(WuttaMenuHandler): """ Make a set of menus for all registered system integrations. """ + tb = self.app.get_tailbone_handler() menus = [] - for provider in self.tb.iter_providers(): + for provider in tb.iter_providers(): menu = provider.make_integration_menu(request) if menu: menus.append(menu) From 5e1c0a5187ab5ab33a63f38cbb0c9da4a7a1f786 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 14 Jul 2024 12:41:08 -0500 Subject: [PATCH 002/142] fix: fix model reference in menu handler --- tailbone/menus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/menus.py b/tailbone/menus.py index 9048ae43..84c12343 100644 --- a/tailbone/menus.py +++ b/tailbone/menus.py @@ -96,7 +96,7 @@ class TailboneMenuHandler(WuttaMenuHandler): if not main_keys: return - model = self.model + model = self.app.model menus = [] # menu definition can come either from config file or db From ece29d7b6cfeb193e0fe7ee66a238f6dedba1144 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 14 Jul 2024 23:29:17 -0500 Subject: [PATCH 003/142] fix: update usage of auth handler, per rattail changes --- pyproject.toml | 2 +- tailbone/api/core.py | 4 ++-- tailbone/auth.py | 14 +++++++++----- tailbone/subscribers.py | 9 +++++++-- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index de65655a..22fa5676 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "pyramid_mako", "pyramid_retry", "pyramid_tm", - "rattail[db,bouncer]>=0.16.0", + "rattail[db,bouncer]>=0.17.0", "sa-filters", "simplejson", "transaction", diff --git a/tailbone/api/core.py b/tailbone/api/core.py index b278d4af..0d8eec32 100644 --- a/tailbone/api/core.py +++ b/tailbone/api/core.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. # @@ -102,7 +102,7 @@ class APIView(View): auth = app.get_auth_handler() # basic / default info - is_admin = user.is_admin() + is_admin = auth.user_is_admin(user) employee = app.get_employee(user) info = { 'uuid': user.uuid, diff --git a/tailbone/auth.py b/tailbone/auth.py index 5a35caa6..826c5d40 100644 --- a/tailbone/auth.py +++ b/tailbone/auth.py @@ -45,11 +45,12 @@ def login_user(request, user, timeout=NOTSET): Perform the steps necessary to login the given user. Note that this returns a ``headers`` dict which you should pass to the redirect. """ - app = request.rattail_config.get_app() + config = request.rattail_config + app = config.get_app() user.record_event(app.enum.USER_EVENT_LOGIN) headers = remember(request, user.uuid) if timeout is NOTSET: - timeout = session_timeout_for_user(user) + timeout = session_timeout_for_user(config, user) log.debug("setting session timeout for '{}' to {}".format(user.username, timeout)) set_session_timeout(request, timeout) return headers @@ -70,15 +71,18 @@ def logout_user(request): return headers -def session_timeout_for_user(user): +def session_timeout_for_user(config, user): """ Returns the "max" session timeout for the user, according to roles """ - from rattail.db.auth import authenticated_role + app = config.get_app() + auth = app.get_auth_handler() - roles = user.roles + [authenticated_role(Session())] + authenticated = auth.get_role_authenticated(Session()) + roles = user.roles + [authenticated] timeouts = [role.session_timeout for role in roles if role.session_timeout is not None] + if timeouts and 0 not in timeouts: return max(timeouts) diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py index 12e1e32a..181c84bc 100644 --- a/tailbone/subscribers.py +++ b/tailbone/subscribers.py @@ -98,10 +98,15 @@ def new_request(event): request = event.request # invoke upstream logic + # nb. this sets request.wutta_config base.new_request(event) + config = request.wutta_config + app = config.get_app() + auth = app.get_auth_handler() + # compatibility - rattail_config = request.wutta_config + rattail_config = config request.rattail_config = rattail_config def user(request): @@ -120,7 +125,7 @@ def new_request(event): # assign client IP address to the session, for sake of versioning Session().continuum_remote_addr = request.client_addr - request.is_admin = bool(request.user) and request.user.is_admin() + request.is_admin = auth.user_is_admin(request.user) request.is_root = request.is_admin and request.session.get('is_root', False) # TODO: why would this ever be null? From 57fdacdb834dabab7bd61d1d492bc2c2d41d42dd Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 14 Jul 2024 23:29:35 -0500 Subject: [PATCH 004/142] =?UTF-8?q?bump:=20version=200.14.0=20=E2=86=92=20?= =?UTF-8?q?0.14.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c5304d6..df38a20f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ 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.14.1 (2024-07-14) + +### Fix + +- update usage of auth handler, per rattail changes +- fix model reference in menu handler +- fix bug when making "integration" menus + ## v0.14.0 (2024-07-14) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 22fa5676..d7fa1c95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.14.0" +version = "0.14.1" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From be6eb5f8153e373772278f2786aa72b0c15f8daf Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 15 Jul 2024 21:51:45 -0500 Subject: [PATCH 005/142] fix: add null menu handler, for use with API apps --- tailbone/menus.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tailbone/menus.py b/tailbone/menus.py index 84c12343..abd0b58b 100644 --- a/tailbone/menus.py +++ b/tailbone/menus.py @@ -745,3 +745,18 @@ class MenuHandler(TailboneMenuHandler): "please use tailbone.menus.TailboneMenuHandler instead", DeprecationWarning, stacklevel=2) super().__init__(*args, **kwargs) + + +class NullMenuHandler(WuttaMenuHandler): + """ + Null menu handler which uses an empty menu set. + + .. note: + + This class shouldn't even exist, but for the moment, it is + useful to configure non-traditional (e.g. API) web apps to use + this, in order to avoid most of the overhead. + """ + + def make_menus(self, request, **kwargs): + return [] From af0f84762c5dfaecc0c29cf7431d84aa7231f666 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 15 Jul 2024 21:52:05 -0500 Subject: [PATCH 006/142] =?UTF-8?q?bump:=20version=200.14.1=20=E2=86=92=20?= =?UTF-8?q?0.14.2?= 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 df38a20f..c27cc130 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.14.2 (2024-07-15) + +### Fix + +- add null menu handler, for use with API apps + ## v0.14.1 (2024-07-14) ### Fix diff --git a/pyproject.toml b/pyproject.toml index d7fa1c95..c19bb3e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.14.1" +version = "0.14.2" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 3aafe578f03893e0f03fd8e6ff5d57408a0daa38 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 16 Jul 2024 18:59:35 -0500 Subject: [PATCH 007/142] fix: allow auto-collapse of header when viewing trainwreck txn --- tailbone/templates/form.mako | 60 +++++++++++++++++-- .../trainwreck/transactions/configure.mako | 13 ++++ tailbone/views/trainwreck/base.py | 12 ++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/tailbone/templates/form.mako b/tailbone/templates/form.mako index 0352b04c..9ce7039a 100644 --- a/tailbone/templates/form.mako +++ b/tailbone/templates/form.mako @@ -16,10 +16,53 @@ <%def name="page_content()"> -
-
- ${self.render_form()} -
+ % if main_form_collapsible: + <${b}-collapse class="panel" + % if request.use_oruga: + v-model:open="mainFormPanelOpen" + % else: + :open.sync="mainFormPanelOpen" + % endif + > + +
+
+
+ ${self.render_form()} +
+
+ + % else: +
+
+ ${self.render_form()} +
+ % endif <%def name="render_this_page()"> @@ -54,6 +97,15 @@ ${parent.render_this_page_template()} +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + % if main_form_collapsible: + + % endif + + <%def name="finalize_this_page_vars()"> ${parent.finalize_this_page_vars()} % if form is not Undefined: diff --git a/tailbone/templates/trainwreck/transactions/configure.mako b/tailbone/templates/trainwreck/transactions/configure.mako index 99b43fde..4569759b 100644 --- a/tailbone/templates/trainwreck/transactions/configure.mako +++ b/tailbone/templates/trainwreck/transactions/configure.mako @@ -3,6 +3,19 @@ <%def name="form_content()"> +

Display

+
+ + + + Auto-collapse header when viewing transaction + + +
+

Rotation

diff --git a/tailbone/views/trainwreck/base.py b/tailbone/views/trainwreck/base.py index 9a6086d7..f529eb66 100644 --- a/tailbone/views/trainwreck/base.py +++ b/tailbone/views/trainwreck/base.py @@ -256,6 +256,7 @@ class TransactionView(MasterView): def template_kwargs_view(self, **kwargs): kwargs = super().template_kwargs_view(**kwargs) + config = self.rattail_config form = kwargs['form'] if 'custorder_xref_markers' in form: @@ -268,6 +269,12 @@ class TransactionView(MasterView): }) kwargs['custorder_xref_markers_data'] = markers + # collapse header + kwargs['main_form_collapsible'] = True + kwargs['main_form_autocollapse'] = config.get_bool( + 'tailbone.trainwreck.view_txn.autocollapse_header', + default=False) + return kwargs def get_xref_buttons(self, txn): @@ -419,6 +426,11 @@ class TransactionView(MasterView): def configure_get_simple_settings(self): return [ + # display + {'section': 'tailbone', + 'option': 'trainwreck.view_txn.autocollapse_header', + 'type': bool}, + # rotation {'section': 'trainwreck', 'option': 'use_rotation', From e88b8fc9bc25ff8b7632756f193b00ead8246ae4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 16 Jul 2024 21:21:43 -0500 Subject: [PATCH 008/142] fix: fix auto-collapse title for viewing trainwreck txn --- tailbone/templates/form.mako | 2 +- tailbone/views/trainwreck/base.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tailbone/templates/form.mako b/tailbone/templates/form.mako index 9ce7039a..c9c8ea88 100644 --- a/tailbone/templates/form.mako +++ b/tailbone/templates/form.mako @@ -47,7 +47,7 @@   - Transaction Header + ${main_form_title}
diff --git a/tailbone/views/trainwreck/base.py b/tailbone/views/trainwreck/base.py index f529eb66..9c150c6a 100644 --- a/tailbone/views/trainwreck/base.py +++ b/tailbone/views/trainwreck/base.py @@ -270,6 +270,7 @@ class TransactionView(MasterView): kwargs['custorder_xref_markers_data'] = markers # collapse header + kwargs['main_form_title'] = "Transaction Header" kwargs['main_form_collapsible'] = True kwargs['main_form_autocollapse'] = config.get_bool( 'tailbone.trainwreck.view_txn.autocollapse_header', From 9c466796dae12c11e50cc6be04c5a467e478d255 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 17 Jul 2024 18:24:21 -0500 Subject: [PATCH 009/142] =?UTF-8?q?bump:=20version=200.14.2=20=E2=86=92=20?= =?UTF-8?q?0.14.3?= 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 c27cc130..70d9b6ec 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.14.3 (2024-07-17) + +### Fix + +- fix auto-collapse title for viewing trainwreck txn +- allow auto-collapse of header when viewing trainwreck txn + ## v0.14.2 (2024-07-15) ### Fix diff --git a/pyproject.toml b/pyproject.toml index c19bb3e2..e785fb0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.14.2" +version = "0.14.3" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From f4f79f170a5fefec8cbced39fab3f0eb6dff2873 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 17 Jul 2024 19:45:47 -0500 Subject: [PATCH 010/142] fix: fix modals for luigi tasks page, per oruga --- tailbone/templates/luigi/index.mako | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tailbone/templates/luigi/index.mako b/tailbone/templates/luigi/index.mako index bb8d1465..b5134c25 100644 --- a/tailbone/templates/luigi/index.mako +++ b/tailbone/templates/luigi/index.mako @@ -79,8 +79,13 @@ @click="overnightTaskLaunchInit(props.row)"> Launch - + <${b}-modal has-modal-card + % if request.use_oruga: + v-model:active="overnightTaskShowLaunchDialog" + % else: + :active.sync="overnightTaskShowLaunchDialog" + % endif + > - + - + <${b}-modal has-modal-card + % if request.use_oruga: + v-model:active="backfillTaskShowLaunchDialog" + % else: + :active.sync="backfillTaskShowLaunchDialog" + % endif + > - + % endif From 1bba6d994744585244f91c008fd93ec4ca2a9bc9 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 18 Jul 2024 17:58:59 -0500 Subject: [PATCH 011/142] fix: fix more settings persistence bug(s) for datasync/configure esp. for the profile consumers info --- tailbone/templates/datasync/configure.mako | 27 +++++++++++----------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tailbone/templates/datasync/configure.mako b/tailbone/templates/datasync/configure.mako index 0889b144..7922d189 100644 --- a/tailbone/templates/datasync/configure.mako +++ b/tailbone/templates/datasync/configure.mako @@ -734,16 +734,9 @@ this.editingProfilePendingConsumers = [] for (let consumer of row.consumers_data) { - let pending = { + const pending = { + ...consumer, original_key: consumer.key, - key: consumer.key, - consumer_spec: consumer.consumer_spec, - consumer_dbkey: consumer.consumer_dbkey, - consumer_delay: consumer.consumer_delay, - consumer_retry_attempts: consumer.consumer_retry_attempts, - consumer_retry_delay: consumer.consumer_retry_delay, - consumer_runas: consumer.consumer_runas, - enabled: consumer.enabled, } this.editingProfilePendingConsumers.push(pending) } @@ -791,8 +784,8 @@ this.editingProfilePendingWatcherKwargs.splice(i, 1) } - ThisPage.methods.findOriginalConsumer = function(key) { - for (let consumer of this.editingProfile.consumers_data) { + ThisPage.methods.findConsumer = function(profileConsumers, key) { + for (const consumer of profileConsumers) { if (consumer.key == key) { return consumer } @@ -803,9 +796,12 @@ const row = this.editingProfile const newRow = !row.key + let originalProfile = null if (newRow) { row.consumers_data = [] this.profilesData.push(row) + } else { + originalProfile = this.findProfile(row) } row.key = this.editingProfileKey @@ -853,7 +849,8 @@ for (let pending of this.editingProfilePendingConsumers) { persistentConsumers.push(pending.key) if (pending.original_key) { - let consumer = this.findOriginalConsumer(pending.original_key) + const consumer = this.findConsumer(originalProfile.consumers_data, + pending.original_key) consumer.key = pending.key consumer.consumer_spec = pending.consumer_spec consumer.consumer_dbkey = pending.consumer_dbkey @@ -941,8 +938,10 @@ } ThisPage.methods.updateConsumer = function() { - let pending = this.editingConsumer - let isNew = !pending.key + const pending = this.findConsumer( + this.editingProfilePendingConsumers, + this.editingConsumer.key) + const isNew = !pending.key pending.key = this.editingConsumerKey pending.consumer_spec = this.editingConsumerSpec From a9495b6a7059deb256059615eb2aabd3e2308790 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 18 Jul 2024 17:59:55 -0500 Subject: [PATCH 012/142] =?UTF-8?q?bump:=20version=200.14.3=20=E2=86=92=20?= =?UTF-8?q?0.14.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 70d9b6ec..44157ba6 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.14.4 (2024-07-18) + +### Fix + +- fix more settings persistence bug(s) for datasync/configure +- fix modals for luigi tasks page, per oruga + ## v0.14.3 (2024-07-17) ### Fix diff --git a/pyproject.toml b/pyproject.toml index e785fb0c..5cc0470b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.14.3" +version = "0.14.4" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 08a89c490a5ffa07599ec3bee928d07170ca4d78 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 21 Jul 2024 20:20:43 -0500 Subject: [PATCH 013/142] fix: avoid duplicate `partial` param when grid reloads data --- tailbone/templates/grids/complete.mako | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index e200cdc3..a0f927d3 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -480,7 +480,9 @@ } else { params = new URLSearchParams(params) } - params.append('partial', true) + if (!params.has('partial')) { + params.append('partial', true) + } params = params.toString() this.loading = true From 458c95696a1faab6ea1f567ca38c6b00046f98f4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 3 Aug 2024 14:13:16 -0500 Subject: [PATCH 014/142] fix: use auth handler instead of deprecated auth functions --- tailbone/views/users.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tailbone/views/users.py b/tailbone/views/users.py index dd3f7f7b..b641e578 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -28,8 +28,6 @@ import sqlalchemy as sa from sqlalchemy import orm from rattail.db.model import User, UserEvent -from rattail.db.auth import (administrator_role, guest_role, - authenticated_role, set_user_password) import colander from deform import widget as dfwidget @@ -360,17 +358,19 @@ class UserView(PrincipalMasterView): return tokens def get_possible_roles(self): - model = self.model + app = self.get_rattail_app() + auth = app.get_auth_handler() + model = app.model # some roles should never have users "belong" to them excluded = [ - guest_role(self.Session()).uuid, - authenticated_role(self.Session()).uuid, + auth.get_role_anonymous(self.Session()).uuid, + auth.get_role_authenticated(self.Session()).uuid, ] # only allow "root" user to change true admin role membership if not self.request.is_root: - excluded.append(administrator_role(self.Session()).uuid) + excluded.append(auth.get_role_administrator(self.Session()).uuid) # basic list, minus exclusions so far roles = self.Session.query(model.Role)\ @@ -385,7 +385,9 @@ class UserView(PrincipalMasterView): return roles.order_by(model.Role.name) def objectify(self, form, data=None): - model = self.model + app = self.get_rattail_app() + auth = app.get_auth_handler() + model = app.model # create/update user as per normal if data is None: @@ -420,7 +422,7 @@ class UserView(PrincipalMasterView): # maybe set user password if 'set_password' in form and data['set_password']: - set_user_password(user, data['set_password']) + auth.set_user_password(user, data['set_password']) # update roles for user self.update_roles(user, data) @@ -433,10 +435,12 @@ class UserView(PrincipalMasterView): if 'roles' not in data: return - model = self.model + app = self.get_rattail_app() + auth = app.get_auth_handler() + model = app.model old_roles = set([r.uuid for r in user.roles]) new_roles = data['roles'] - admin = administrator_role(self.Session()) + admin = auth.get_role_administrator(self.Session()) # add any new roles for the user, taking care not to add the admin role # unless acting as root From 5ec899cf084b67806ae6e21578c6c04071fa5f22 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 3 Aug 2024 17:43:46 -0500 Subject: [PATCH 015/142] =?UTF-8?q?bump:=20version=200.14.4=20=E2=86=92=20?= =?UTF-8?q?0.14.5?= 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 44157ba6..412e6e4a 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.14.5 (2024-08-03) + +### Fix + +- use auth handler instead of deprecated auth functions +- avoid duplicate `partial` param when grid reloads data + ## v0.14.4 (2024-07-18) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 5cc0470b..0783f2bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.14.4" +version = "0.14.5" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 3b92bb3a9e365a761b48335d42cca4d6f86e01b8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 4 Aug 2024 14:56:12 -0500 Subject: [PATCH 016/142] fix: use wuttaweb logic for `util.get_form_data()` --- docs/api/util.rst | 6 ++++ docs/index.rst | 1 + tailbone/forms/core.py | 10 +++++-- tailbone/util.py | 18 ++++++------ tailbone/views/purchasing/receiving.py | 8 +++--- tests/test_util.py | 39 ++++++++++++++++++++++++++ 6 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 docs/api/util.rst create mode 100644 tests/test_util.py diff --git a/docs/api/util.rst b/docs/api/util.rst new file mode 100644 index 00000000..35e66ed3 --- /dev/null +++ b/docs/api/util.rst @@ -0,0 +1,6 @@ + +``tailbone.util`` +================= + +.. automodule:: tailbone.util + :members: diff --git a/docs/index.rst b/docs/index.rst index 3ca6d4e2..d964086f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -52,6 +52,7 @@ Package API: api/grids.core api/progress api/subscribers + api/util api/views/batch api/views/batch.vendorcatalog api/views/core diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 11d489a7..60c2f61b 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -35,7 +35,7 @@ from sqlalchemy import orm from sqlalchemy.ext.associationproxy import AssociationProxy, ASSOCIATION_PROXY from wuttjamaican.util import UNSPECIFIED -from rattail.util import prettify, pretty_boolean +from rattail.util import pretty_boolean from rattail.db.util import get_fieldnames import colander @@ -47,8 +47,10 @@ from pyramid_deform import SessionFileUploadTempStore from pyramid.renderers import render from webhelpers2.html import tags, HTML +from wuttaweb.util import get_form_data + from tailbone.db import Session -from tailbone.util import raw_datetime, get_form_data, render_markdown +from tailbone.util import raw_datetime, render_markdown from tailbone.forms import types from tailbone.forms.widgets import (ReadonlyWidget, PlainDateWidget, JQueryDateWidget, JQueryTimeWidget, @@ -570,7 +572,9 @@ class Form(object): self.schema[key].title = label def get_label(self, key): - return self.labels.get(key, prettify(key)) + config = self.request.rattail_config + app = config.get_app() + return self.labels.get(key, app.make_title(key)) def set_readonly(self, key, readonly=True): if readonly: diff --git a/tailbone/util.py b/tailbone/util.py index c1a0e1d5..9a0314a0 100644 --- a/tailbone/util.py +++ b/tailbone/util.py @@ -39,6 +39,8 @@ from pyramid.renderers import get_renderer from pyramid.interfaces import IRoutesMapper from webhelpers2.html import HTML, tags +from wuttaweb.util import get_form_data as wutta_get_form_data + log = logging.getLogger(__name__) @@ -75,17 +77,13 @@ def csrf_token(request, name='_csrf'): def get_form_data(request): """ - Returns the effective form data for the given request. Mostly - this is a convenience, to return either POST or JSON depending on - the type of request. + DEPECATED - use :func:`wuttaweb:wuttaweb.util.get_form_data()` + instead. """ - # nb. we prefer JSON only if no POST is present - # TODO: this seems to work for our use case at least, but perhaps - # there is a better way? see also - # https://docs.pylonsproject.org/projects/pyramid/en/latest/api/request.html#pyramid.request.Request.is_xhr - if (request.is_xhr or request.content_type == 'application/json') and not request.POST: - return request.json_body - return request.POST + warnings.warn("tailbone.util.get_form_data() is deprecated; " + "please use wuttaweb.util.get_form_data() instead", + DeprecationWarning, stacklevel=2) + return wutta_get_form_data(request) def get_global_search_options(request): diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index be15c1a8..55936184 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -25,22 +25,22 @@ Views for 'receiving' (purchasing) batches """ import os -import re import decimal import logging from collections import OrderedDict -import humanize +# import humanize from rattail import pod -from rattail.util import prettify, simple_error +from rattail.util import simple_error import colander from deform import widget as dfwidget from webhelpers2.html import tags, HTML +from wuttaweb.util import get_form_data + from tailbone import forms, grids -from tailbone.util import get_form_data from tailbone.views.purchasing import PurchasingBatchView diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 00000000..46684f0c --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8; -*- + +from unittest import TestCase + +from pyramid import testing + +from rattail.config import RattailConfig + +from tailbone import util + + +class TestGetFormData(TestCase): + + def setUp(self): + self.config = RattailConfig() + + def make_request(self, **kwargs): + kwargs.setdefault('wutta_config', self.config) + kwargs.setdefault('rattail_config', self.config) + kwargs.setdefault('is_xhr', None) + kwargs.setdefault('content_type', None) + kwargs.setdefault('POST', {'foo1': 'bar'}) + kwargs.setdefault('json_body', {'foo2': 'baz'}) + return testing.DummyRequest(**kwargs) + + def test_default(self): + request = self.make_request() + data = util.get_form_data(request) + self.assertEqual(data, {'foo1': 'bar'}) + + def test_is_xhr(self): + request = self.make_request(POST=None, is_xhr=True) + data = util.get_form_data(request) + self.assertEqual(data, {'foo2': 'baz'}) + + def test_content_type(self): + request = self.make_request(POST=None, content_type='application/json') + data = util.get_form_data(request) + self.assertEqual(data, {'foo2': 'baz'}) From 9d2684046ff4e5bf4b0c0da979e2cb604a915638 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 5 Aug 2024 15:00:11 -0500 Subject: [PATCH 017/142] feat: move more subscriber logic to wuttaweb --- tailbone/subscribers.py | 66 ++++++++++------------------------------- 1 file changed, 15 insertions(+), 51 deletions(-) diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py index 181c84bc..c783287b 100644 --- a/tailbone/subscribers.py +++ b/tailbone/subscribers.py @@ -52,30 +52,17 @@ def new_request(event): """ Event hook called when processing a new request. - This first invokes the upstream hook: - :func:`wuttaweb:wuttaweb.subscribers.new_request()` + This first invokes the upstream hooks: + + * :func:`wuttaweb:wuttaweb.subscribers.new_request()` + * :func:`wuttaweb:wuttaweb.subscribers.new_request_set_user()` It then adds more things to the request object; among them: .. attribute:: request.rattail_config Reference to the app :term:`config object`. Note that this - will be the same as ``request.wutta_config``. - - .. attribute:: request.user - - Reference to the current authenticated user, or ``None``. - - .. attribute:: request.is_admin - - Flag indicating whether current user is a member of the - Administrator role. - - .. attribute:: request.is_root - - Flag indicating whether user is currently elevated to root - privileges. This is only possible if ``request.is_admin = - True``. + will be the same as :attr:`wuttaweb:request.wutta_config`. .. method:: request.has_perm(name) @@ -94,10 +81,9 @@ def new_request(event): then in the base template all registered components will be properly loaded. """ - # log.debug("new request: %s", event) request = event.request - # invoke upstream logic + # invoke main upstream logic # nb. this sets request.wutta_config base.new_request(event) @@ -109,25 +95,20 @@ def new_request(event): rattail_config = config request.rattail_config = rattail_config - def user(request): - user = None - uuid = request.authenticated_userid - if uuid: - app = request.rattail_config.get_app() - model = app.model - user = Session.get(model.User, uuid) - if user: - Session().set_continuum_user(user) - return user + def user_getter(request, db_session=None): + user = base.default_user_getter(request, db_session=db_session) + if user: + # nb. we also assign continuum user to session + session = db_session or Session() + session.set_continuum_user(user) + return user - request.set_property(user, reify=True) + # invoke upstream hook to set user + base.new_request_set_user(event, user_getter=user_getter, db_session=Session()) # assign client IP address to the session, for sake of versioning Session().continuum_remote_addr = request.client_addr - request.is_admin = auth.user_is_admin(request.user) - request.is_root = request.is_admin and request.session.get('is_root', False) - # TODO: why would this ever be null? if rattail_config: @@ -286,27 +267,10 @@ def context_found(event): The following is attached to the request: - * ``get_referrer()`` function - * ``get_session_timeout()`` function """ request = event.request - def get_referrer(default=None, **kwargs): - if request.params.get('referrer'): - return request.params['referrer'] - if request.session.get('referrer'): - return request.session.pop('referrer') - referrer = request.referrer - if (not referrer or referrer == request.current_route_url() - or not referrer.startswith(request.host_url)): - if default: - referrer = default - else: - referrer = request.route_url('home') - return referrer - request.get_referrer = get_referrer - def get_session_timeout(): """ Returns the timeout in effect for the current session From 2903b376b5038a495feaef5a70c3a31a75466476 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 5 Aug 2024 15:35:06 -0500 Subject: [PATCH 018/142] =?UTF-8?q?bump:=20version=200.14.5=20=E2=86=92=20?= =?UTF-8?q?0.15.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 412e6e4a..6f1e1ac3 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.15.0 (2024-08-05) + +### Feat + +- move more subscriber logic to wuttaweb + +### Fix + +- use wuttaweb logic for `util.get_form_data()` + ## v0.14.5 (2024-08-03) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 0783f2bc..1d05052d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.14.5" +version = "0.15.0" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 91ea9021d7aaceb17a7cd56cd48ece52a71abb31 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 5 Aug 2024 21:50:22 -0500 Subject: [PATCH 019/142] fix: move magic `b` template context var to wuttaweb --- tailbone/subscribers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py index c783287b..02c4e518 100644 --- a/tailbone/subscribers.py +++ b/tailbone/subscribers.py @@ -186,7 +186,6 @@ def before_render(event): # theme - we only want do this for classic web app, *not* API # TODO: so, clearly we need a better way to distinguish the two if 'tailbone.theme' in request.registry.settings: - renderer_globals['b'] = 'o' if request.use_oruga else 'b' # for buefy renderer_globals['theme'] = request.registry.settings['tailbone.theme'] # note, this is just a global flag; user still needs permission to see picker expose_picker = config.get_bool('tailbone.themes.expose_picker', From bd1993f44029d4c0546a5d5224ef06680ce74ca6 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 5 Aug 2024 22:57:02 -0500 Subject: [PATCH 020/142] =?UTF-8?q?bump:=20version=200.15.0=20=E2=86=92=20?= =?UTF-8?q?0.15.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 6f1e1ac3..6a02e734 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.15.1 (2024-08-05) + +### Fix + +- move magic `b` template context var to wuttaweb + ## v0.15.0 (2024-08-05) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 1d05052d..9e68e401 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.15.0" +version = "0.15.1" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 518c108c883a3bcceb431c10394d3176922f4658 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Aug 2024 10:36:20 -0500 Subject: [PATCH 021/142] fix: use auth handler, avoid legacy calls for role/perm checks --- tailbone/views/principal.py | 2 +- tailbone/views/roles.py | 57 +++++++++++++++++++++++-------------- tailbone/views/users.py | 2 +- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/tailbone/views/principal.py b/tailbone/views/principal.py index fb09306b..b053453d 100644 --- a/tailbone/views/principal.py +++ b/tailbone/views/principal.py @@ -194,7 +194,7 @@ class PermissionsRenderer(Object): rendered = False for key in sorted(perms, key=lambda p: perms[p]['label'].lower()): checked = auth.has_permission(Session(), principal, key, - include_guest=self.include_guest, + include_anonymous=self.include_guest, include_authenticated=self.include_authenticated) if checked: label = perms[key]['label'] diff --git a/tailbone/views/roles.py b/tailbone/views/roles.py index 0316ea87..09633c6e 100644 --- a/tailbone/views/roles.py +++ b/tailbone/views/roles.py @@ -30,7 +30,6 @@ from sqlalchemy import orm from openpyxl.styles import Font, PatternFill from rattail.db.model import Role -from rattail.db.auth import administrator_role, guest_role, authenticated_role from rattail.excel import ExcelWriter import colander @@ -107,8 +106,11 @@ class RoleView(PrincipalMasterView): if role.node_type and role.node_type != self.rattail_config.node_type(): return False + app = self.get_rattail_app() + auth = app.get_auth_handler() + # only "root" can edit Administrator - if role is administrator_role(self.Session()): + if role is auth.get_role_administrator(self.Session()): return self.request.is_root # only "admin" can edit "admin-ish" roles @@ -116,11 +118,11 @@ class RoleView(PrincipalMasterView): return self.request.is_admin # can edit Authenticated only if user has permission - if role is authenticated_role(self.Session()): + if role is auth.get_role_authenticated(self.Session()): return self.has_perm('edit_authenticated') # can edit Guest only if user has permission - if role is guest_role(self.Session()): + if role is auth.get_role_anonymous(self.Session()): return self.has_perm('edit_guest') # current user can edit their own roles, only if they have permission @@ -139,11 +141,14 @@ class RoleView(PrincipalMasterView): if role.node_type and role.node_type != self.rattail_config.node_type(): return False - if role is administrator_role(self.Session()): + app = self.get_rattail_app() + auth = app.get_auth_handler() + + if role is auth.get_role_administrator(self.Session()): return False - if role is authenticated_role(self.Session()): + if role is auth.get_role_authenticated(self.Session()): return False - if role is guest_role(self.Session()): + if role is auth.get_role_anonymous(self.Session()): return False # only "admin" can delete "admin-ish" roles @@ -186,17 +191,17 @@ class RoleView(PrincipalMasterView): # session_timeout f.set_renderer('session_timeout', self.render_session_timeout) - if self.editing and role is guest_role(self.Session()): + if self.editing and role is auth.get_role_anonymous(self.Session()): f.set_readonly('session_timeout') # sync_me, node_type if not self.creating: include = True - if role is administrator_role(self.Session()): + if role is auth.get_role_administrator(self.Session()): include = False - elif role is authenticated_role(self.Session()): + elif role is auth.get_role_authenticated(self.Session()): include = False - elif role is guest_role(self.Session()): + elif role is auth.get_role_anonymous(self.Session()): include = False if not include: f.remove('sync_me', 'sync_users', 'node_type') @@ -227,7 +232,7 @@ class RoleView(PrincipalMasterView): for groupkey in self.tailbone_permissions: for key in self.tailbone_permissions[groupkey]['perms']: if auth.has_permission(self.Session(), role, key, - include_guest=False, + include_anonymous=False, include_authenticated=False): granted.append(key) f.set_default('permissions', granted) @@ -235,12 +240,14 @@ class RoleView(PrincipalMasterView): f.remove_field('permissions') def render_users(self, role, field): + app = self.get_rattail_app() + auth = app.get_auth_handler() - if role is guest_role(self.Session()): + if role is auth.get_role_anonymous(self.Session()): return ("The guest role is implied for all anonymous users, " "i.e. when not logged in.") - if role is authenticated_role(self.Session()): + if role is auth.get_role_authenticated(self.Session()): return ("The authenticated role is implied for all users, " "but only when logged in.") @@ -308,7 +315,9 @@ class RoleView(PrincipalMasterView): return available def render_session_timeout(self, role, field): - if role is guest_role(self.Session()): + app = self.get_rattail_app() + auth = app.get_auth_handler() + if role is auth.get_role_anonymous(self.Session()): return "(not applicable)" if role.session_timeout is None: return "" @@ -347,6 +356,8 @@ class RoleView(PrincipalMasterView): auth.revoke_permission(role, pkey) def template_kwargs_view(self, **kwargs): + app = self.get_rattail_app() + auth = app.get_auth_handler() model = self.model role = kwargs['instance'] if role.users: @@ -362,8 +373,8 @@ class RoleView(PrincipalMasterView): else: kwargs['users'] = None - kwargs['guest_role'] = guest_role(self.Session()) - kwargs['authenticated_role'] = authenticated_role(self.Session()) + kwargs['guest_role'] = auth.get_role_anonymous(self.Session()) + kwargs['authenticated_role'] = auth.get_role_authenticated(self.Session()) role = kwargs['instance'] if role not in (kwargs['guest_role'], kwargs['authenticated_role']): @@ -384,9 +395,11 @@ class RoleView(PrincipalMasterView): return kwargs def before_delete(self, role): - admin = administrator_role(self.Session()) - guest = guest_role(self.Session()) - authenticated = authenticated_role(self.Session()) + app = self.get_rattail_app() + auth = app.get_auth_handler() + admin = auth.get_role_administrator(self.Session()) + guest = auth.get_role_anonymous(self.Session()) + authenticated = auth.get_role_authenticated(self.Session()) if role in (admin, guest, authenticated): self.request.session.flash("You may not delete the {} role.".format(role.name), 'error') return self.redirect(self.request.get_referrer(default=self.request.route_url('roles'))) @@ -402,7 +415,7 @@ class RoleView(PrincipalMasterView): .options(orm.joinedload(model.Role._permissions)) roles = [] for role in all_roles: - if auth.has_permission(session, role, permission, include_guest=False): + if auth.has_permission(session, role, permission, include_anonymous=False): roles.append(role) return roles @@ -475,7 +488,7 @@ class RoleView(PrincipalMasterView): # and show an 'X' for any role which has this perm for col, role in enumerate(roles, 2): if auth.has_permission(self.Session(), role, key, - include_guest=False): + include_anonymous=False): sheet.cell(row=writing_row, column=col, value="X") writing_row += 1 diff --git a/tailbone/views/users.py b/tailbone/views/users.py index b641e578..1012575a 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -279,7 +279,7 @@ class UserView(PrincipalMasterView): permissions = self.request.registry.settings.get('tailbone_permissions', {}) f.set_renderer('permissions', PermissionsRenderer(request=self.request, permissions=permissions, - include_guest=True, + include_anonymous=True, include_authenticated=True)) else: f.remove('permissions') From 80dc4eb7a9a619ba1fa39372045f63f7894aeff1 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 6 Aug 2024 23:19:14 -0500 Subject: [PATCH 022/142] =?UTF-8?q?bump:=20version=200.15.1=20=E2=86=92=20?= =?UTF-8?q?0.15.2?= 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 6a02e734..733d990b 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.15.2 (2024-08-06) + +### Fix + +- use auth handler, avoid legacy calls for role/perm checks + ## v0.15.1 (2024-08-05) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 9e68e401..54f4df73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.15.1" +version = "0.15.2" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From ffd694e7b72ae11faf09a086d7f36681f12094e7 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 8 Aug 2024 19:39:01 -0500 Subject: [PATCH 023/142] fix: fix timepicker `parseTime()` when value is null --- tailbone/templates/themes/butterball/field-components.mako | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tailbone/templates/themes/butterball/field-components.mako b/tailbone/templates/themes/butterball/field-components.mako index d79c88f4..917083c4 100644 --- a/tailbone/templates/themes/butterball/field-components.mako +++ b/tailbone/templates/themes/butterball/field-components.mako @@ -517,6 +517,9 @@ }, parseTime(value) { + if (!value) { + return value + } if (value.getHours) { return value From 0b8315fc7876ca0cc43547bb0df21e80559a33cb Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 8 Aug 2024 19:39:36 -0500 Subject: [PATCH 024/142] =?UTF-8?q?bump:=20version=200.15.2=20=E2=86=92=20?= =?UTF-8?q?0.15.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 733d990b..7cce885b 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.15.3 (2024-08-08) + +### Fix + +- fix timepicker `parseTime()` when value is null + ## v0.15.2 (2024-08-06) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 54f4df73..800e8ab0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.15.2" +version = "0.15.3" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 7e683dfc4af7a5e9830a4fb6d70e153b917b0519 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 9 Aug 2024 10:11:38 -0500 Subject: [PATCH 025/142] fix: avoid bug when checking current theme this check is happening not only for classic views but API as well, which doesn't really have a theme.. probably need a proper fix in wuttaweb but this should be okay for now --- tailbone/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tailbone/util.py b/tailbone/util.py index 9a0314a0..eb6fb8a8 100644 --- a/tailbone/util.py +++ b/tailbone/util.py @@ -459,8 +459,8 @@ def should_use_oruga(request): supports (and therefore should use) Oruga + Vue 3 as opposed to the default of Buefy + Vue 2. """ - theme = request.registry.settings['tailbone.theme'] - if 'butterball' in theme: + theme = request.registry.settings.get('tailbone.theme') + if theme and 'butterball' in theme: return True return False From b5f0ecb165fd9d480577b561ca0cff49ba0dea96 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 9 Aug 2024 10:13:00 -0500 Subject: [PATCH 026/142] =?UTF-8?q?bump:=20version=200.15.3=20=E2=86=92=20?= =?UTF-8?q?0.15.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 7cce885b..05648c25 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.15.4 (2024-08-09) + +### Fix + +- avoid bug when checking current theme + ## v0.15.3 (2024-08-08) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 800e8ab0..4478aef5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.15.3" +version = "0.15.4" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From f2fce2e30526db7c85c69b0dfc6162c4d2f7e6b0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 9 Aug 2024 19:22:26 -0500 Subject: [PATCH 027/142] fix: assign convenience attrs for all views (config, app, enum, model) --- tailbone/views/core.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tailbone/views/core.py b/tailbone/views/core.py index b0658d80..88b2519f 100644 --- a/tailbone/views/core.py +++ b/tailbone/views/core.py @@ -58,9 +58,10 @@ class View: config = self.rattail_config if config: - app = config.get_app() - self.model = app.model - self.enum = config.get_enum() + self.config = config + self.app = self.config.get_app() + self.model = self.app.model + self.enum = self.app.enum @property def rattail_config(self): From d57efba3811bc286fe49290f66a69df04b814633 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 9 Aug 2024 19:48:51 -0500 Subject: [PATCH 028/142] =?UTF-8?q?bump:=20version=200.15.4=20=E2=86=92=20?= =?UTF-8?q?0.15.5?= 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 05648c25..de92a834 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.15.5 (2024-08-09) + +### Fix + +- assign convenience attrs for all views (config, app, enum, model) + ## v0.15.4 (2024-08-09) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 4478aef5..c4335903 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.15.4" +version = "0.15.5" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 2c46fde74288da664561277f9637a571a494dcaf Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 10 Aug 2024 08:43:54 -0500 Subject: [PATCH 029/142] fix: simplify verbiage for batch execution panel --- tailbone/templates/batch/view.mako | 2 -- 1 file changed, 2 deletions(-) diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako index 5e3328d9..63cb9056 100644 --- a/tailbone/templates/batch/view.mako +++ b/tailbone/templates/batch/view.mako @@ -85,13 +85,11 @@
% if batch.executed:

- Batch was executed ${h.pretty_datetime(request.rattail_config, batch.executed)} by ${batch.executed_by}

% elif master.handler.executable(batch): % if master.has_perm('execute'): -

Batch has not yet been executed.

Date: Sat, 10 Aug 2024 13:49:41 -0500 Subject: [PATCH 030/142] fix: avoid `before_render` subscriber hook for web API the purpose of that function is to setup extra template context, but API views always render as 'json' with no template --- tailbone/webapi.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tailbone/webapi.py b/tailbone/webapi.py index 1c2fa106..7c0e9b41 100644 --- a/tailbone/webapi.py +++ b/tailbone/webapi.py @@ -91,15 +91,21 @@ def make_pyramid_config(settings): return pyramid_config -def main(global_config, **settings): +def main(global_config, views='tailbone.api', **settings): """ This function returns a Pyramid WSGI application. """ rattail_config = make_rattail_config(settings) pyramid_config = make_pyramid_config(settings) - # bring in some Tailbone - pyramid_config.include('tailbone.subscribers') - pyramid_config.include('tailbone.api') + # event hooks + pyramid_config.add_subscriber('tailbone.subscribers.new_request', + 'pyramid.events.NewRequest') + # TODO: is this really needed? + pyramid_config.add_subscriber('tailbone.subscribers.context_found', + 'pyramid.events.ContextFound') + + # views + pyramid_config.include(views) return pyramid_config.make_wsgi_app() From b53479f8e46db1e622a773c8367f719d289185f8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 13 Aug 2024 11:21:38 -0500 Subject: [PATCH 031/142] =?UTF-8?q?bump:=20version=200.15.5=20=E2=86=92=20?= =?UTF-8?q?0.15.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++++++ pyproject.toml | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de92a834..3836ff08 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.15.6 (2024-08-13) + +### Fix + +- avoid `before_render` subscriber hook for web API +- simplify verbiage for batch execution panel + ## v0.15.5 (2024-08-09) ### Fix diff --git a/pyproject.toml b/pyproject.toml index c4335903..e515a0d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.15.5" +version = "0.15.6" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -53,7 +53,7 @@ dependencies = [ "pyramid_mako", "pyramid_retry", "pyramid_tm", - "rattail[db,bouncer]>=0.17.0", + "rattail[db,bouncer]>=0.17.11", "sa-filters", "simplejson", "transaction", From a6ce5eb21d7ba61f187ac1093abc08b4d9ccdb01 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 15 Aug 2024 14:34:20 -0500 Subject: [PATCH 032/142] feat: refactor forms/grids/views/templates per wuttaweb compat this starts to get things more aligned between wuttaweb and tailbone. the use case in mind so far is for a wuttaweb view to be included in a tailbone app. form and grid classes now have some new methods to match wuttaweb, so templates call the shared method names where possible. templates can no longer assume they have tailbone-native master view, form, grid etc. so must inspect context more closely in some cases. --- tailbone/app.py | 13 +- tailbone/auth.py | 29 +--- tailbone/config.py | 5 +- tailbone/forms/core.py | 112 +++++++++++-- tailbone/grids/core.py | 88 +++++++++- tailbone/subscribers.py | 71 +++----- tailbone/templates/base.mako | 8 +- tailbone/templates/form.mako | 8 +- tailbone/templates/forms/deform.mako | 41 +++-- tailbone/templates/forms/vue_template.mako | 3 + tailbone/templates/grids/complete.mako | 94 +++++------ tailbone/templates/grids/vue_template.mako | 3 + tailbone/templates/master/create.mako | 2 +- tailbone/templates/master/delete.mako | 10 +- tailbone/templates/master/form.mako | 6 +- tailbone/templates/master/index.mako | 128 +++++++-------- tailbone/templates/master/view.mako | 10 +- tailbone/templates/people/index.mako | 4 +- tailbone/templates/people/view.mako | 4 +- .../templates/principal/find_by_perm.mako | 4 +- .../templates/themes/butterball/base.mako | 28 ++-- tailbone/views/master.py | 4 +- tailbone/views/principal.py | 2 +- tailbone/views/roles.py | 4 +- tailbone/views/users.py | 2 +- tests/__init__.py | 3 - tests/forms/__init__.py | 0 tests/forms/test_core.py | 153 ++++++++++++++++++ tests/grids/__init__.py | 0 tests/grids/test_core.py | 139 ++++++++++++++++ tests/test_app.py | 43 +++-- tests/test_auth.py | 3 + tests/test_config.py | 12 ++ tests/test_subscribers.py | 58 +++++++ tests/util.py | 75 +++++++++ tests/views/test_master.py | 26 +++ tests/views/test_principal.py | 29 ++++ tests/views/test_roles.py | 80 +++++++++ tests/views/test_users.py | 33 ++++ 39 files changed, 1037 insertions(+), 300 deletions(-) create mode 100644 tailbone/templates/forms/vue_template.mako create mode 100644 tailbone/templates/grids/vue_template.mako create mode 100644 tests/forms/__init__.py create mode 100644 tests/forms/test_core.py create mode 100644 tests/grids/__init__.py create mode 100644 tests/grids/test_core.py create mode 100644 tests/test_auth.py create mode 100644 tests/test_config.py create mode 100644 tests/test_subscribers.py create mode 100644 tests/util.py create mode 100644 tests/views/test_master.py create mode 100644 tests/views/test_principal.py create mode 100644 tests/views/test_roles.py create mode 100644 tests/views/test_users.py diff --git a/tailbone/app.py b/tailbone/app.py index b7220703..5e8e49d9 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -189,9 +189,16 @@ def make_pyramid_config(settings, configure_csrf=True): for spec in includes: config.include(spec) - # Add some permissions magic. - config.add_directive('add_tailbone_permission_group', 'tailbone.auth.add_permission_group') - config.add_directive('add_tailbone_permission', 'tailbone.auth.add_permission') + # add some permissions magic + config.add_directive('add_wutta_permission_group', + 'wuttaweb.auth.add_permission_group') + config.add_directive('add_wutta_permission', + 'wuttaweb.auth.add_permission') + # TODO: deprecate / remove these + config.add_directive('add_tailbone_permission_group', + 'wuttaweb.auth.add_permission_group') + config.add_directive('add_tailbone_permission', + 'wuttaweb.auth.add_permission') # and some similar magic for certain master views config.add_directive('add_tailbone_index_page', 'tailbone.app.add_index_page') diff --git a/tailbone/auth.py b/tailbone/auth.py index 826c5d40..fbe6bf2f 100644 --- a/tailbone/auth.py +++ b/tailbone/auth.py @@ -27,7 +27,7 @@ Authentication & Authorization import logging import re -from rattail.util import prettify, NOTSET +from rattail.util import NOTSET from zope.interface import implementer from pyramid.authentication import SessionAuthenticationHelper @@ -159,30 +159,3 @@ class TailboneSecurityPolicy: user = self.identity(request) return auth.has_permission(Session(), user, permission) - - -def add_permission_group(config, key, label=None, overwrite=True): - """ - Add a permission group to the app configuration. - """ - def action(): - perms = config.get_settings().get('tailbone_permissions', {}) - if key not in perms or overwrite: - group = perms.setdefault(key, {'key': key}) - group['label'] = label or prettify(key) - config.add_settings({'tailbone_permissions': perms}) - config.action(None, action) - - -def add_permission(config, groupkey, key, label=None): - """ - Add a permission to the app configuration. - """ - def action(): - perms = config.get_settings().get('tailbone_permissions', {}) - group = perms.setdefault(groupkey, {'key': groupkey}) - group.setdefault('label', prettify(groupkey)) - perm = group.setdefault('perms', {}).setdefault(key, {'key': key}) - perm['label'] = label or prettify(key) - config.add_settings({'tailbone_permissions': perms}) - config.action(None, action) diff --git a/tailbone/config.py b/tailbone/config.py index ce1691ae..8392ba0a 100644 --- a/tailbone/config.py +++ b/tailbone/config.py @@ -26,13 +26,14 @@ Rattail config extension for Tailbone import warnings -from rattail.config import ConfigExtension as BaseExtension +from wuttjamaican.conf import WuttaConfigExtension + from rattail.db.config import configure_session from tailbone.db import Session -class ConfigExtension(BaseExtension): +class ConfigExtension(WuttaConfigExtension): """ Rattail config extension for Tailbone. Does the following: diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 60c2f61b..eeae4537 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -47,7 +47,7 @@ from pyramid_deform import SessionFileUploadTempStore from pyramid.renderers import render from webhelpers2.html import tags, HTML -from wuttaweb.util import get_form_data +from wuttaweb.util import get_form_data, make_json_safe from tailbone.db import Session from tailbone.util import raw_datetime, render_markdown @@ -328,7 +328,7 @@ class Form(object): """ Base class for all forms. """ - save_label = "Save" + save_label = "Submit" update_label = "Save" show_cancel = True auto_disable = True @@ -339,10 +339,12 @@ class Form(object): model_instance=None, model_class=None, appstruct=UNSPECIFIED, nodes={}, enums={}, labels={}, assume_local_times=False, renderers=None, renderer_kwargs={}, hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None, - action_url=None, cancel_url=None, component='tailbone-form', + action_url=None, cancel_url=None, + vue_tagname=None, vuejs_component_kwargs=None, vuejs_field_converters={}, json_data={}, included_templates={}, # TODO: ugh this is getting out hand! can_edit_help=False, edit_help_url=None, route_prefix=None, + **kwargs ): self.fields = None if fields is not None: @@ -380,7 +382,17 @@ class Form(object): self.focus_spec = focus_spec self.action_url = action_url self.cancel_url = cancel_url - self.component = component + + # vue_tagname + self.vue_tagname = vue_tagname + if not self.vue_tagname and kwargs.get('component'): + warnings.warn("component kwarg is deprecated for Form(); " + "please use vue_tagname param instead", + DeprecationWarning, stacklevel=2) + self.vue_tagname = kwargs['component'] + if not self.vue_tagname: + self.vue_tagname = 'tailbone-form' + self.vuejs_component_kwargs = vuejs_component_kwargs or {} self.vuejs_field_converters = vuejs_field_converters or {} self.json_data = json_data or {} @@ -393,10 +405,54 @@ class Form(object): return iter(self.fields) @property - def component_studly(self): - words = self.component.split('-') + def vue_component(self): + """ + String name for the Vue component, e.g. ``'TailboneGrid'``. + + This is a generated value based on :attr:`vue_tagname`. + """ + words = self.vue_tagname.split('-') return ''.join([word.capitalize() for word in words]) + @property + def component(self): + """ + DEPRECATED - use :attr:`vue_tagname` instead. + """ + warnings.warn("Form.component is deprecated; " + "please use vue_tagname instead", + DeprecationWarning, stacklevel=2) + return self.vue_tagname + + @property + def component_studly(self): + """ + DEPRECATED - use :attr:`vue_component` instead. + """ + warnings.warn("Form.component_studly is deprecated; " + "please use vue_component instead", + DeprecationWarning, stacklevel=2) + return self.vue_component + + def get_button_label_submit(self): + """ """ + if hasattr(self, '_button_label_submit'): + return self._button_label_submit + + label = getattr(self, 'submit_label', None) + if label: + return label + + return self.save_label + + def set_button_label_submit(self, value): + """ """ + self._button_label_submit = value + + # wutta compat + button_label_submit = property(get_button_label_submit, + set_button_label_submit) + def __contains__(self, item): return item in self.fields @@ -805,6 +861,10 @@ class Form(object): DeprecationWarning, stacklevel=2) return self.render_deform(**kwargs) + def get_deform(self): + """ """ + return self.make_deform_form() + def make_deform_form(self): if not hasattr(self, 'deform_form'): @@ -843,6 +903,10 @@ class Form(object): return self.deform_form + def render_vue_template(self, template='/forms/deform.mako', **context): + """ """ + return self.render_deform(template=template, **context) + def render_deform(self, dform=None, template=None, **kwargs): if not template: template = '/forms/deform.mako' @@ -865,8 +929,8 @@ class Form(object): context.setdefault('form_kwargs', {}) # TODO: deprecate / remove the latter option here if self.auto_disable_save or self.auto_disable: - context['form_kwargs'].setdefault('ref', self.component_studly) - context['form_kwargs']['@submit'] = 'submit{}'.format(self.component_studly) + context['form_kwargs'].setdefault('ref', self.vue_component) + context['form_kwargs']['@submit'] = 'submit{}'.format(self.vue_component) if self.focus_spec: context['form_kwargs']['data-focus'] = self.focus_spec context['request'] = self.request @@ -878,12 +942,13 @@ class Form(object): return dict([(field, self.get_label(field)) for field in self]) - def get_field_markdowns(self): + def get_field_markdowns(self, session=None): app = self.request.rattail_config.get_app() model = app.model + session = session or Session() if not hasattr(self, 'field_markdowns'): - infos = Session.query(model.TailboneFieldInfo)\ + infos = session.query(model.TailboneFieldInfo)\ .filter(model.TailboneFieldInfo.route_prefix == self.route_prefix)\ .all() self.field_markdowns = dict([(info.field_name, info.markdown_text) @@ -891,6 +956,18 @@ class Form(object): return self.field_markdowns + def get_vue_field_value(self, key): + """ """ + if key not in self.fields: + return + + dform = self.get_deform() + if key not in dform: + return + + field = dform[key] + return make_json_safe(field.cstruct) + def get_vuejs_model_value(self, field): """ This method must return "raw" JS which will be assigned as the initial @@ -957,6 +1034,10 @@ class Form(object): def set_vuejs_component_kwargs(self, **kwargs): self.vuejs_component_kwargs.update(kwargs) + def render_vue_tag(self, **kwargs): + """ """ + return self.render_vuejs_component() + def render_vuejs_component(self): """ Render the Vue.js component HTML for the form. @@ -971,7 +1052,7 @@ class Form(object): kwargs = dict(self.vuejs_component_kwargs) if self.can_edit_help: kwargs.setdefault(':configure-fields-help', 'configureFieldsHelp') - return HTML.tag(self.component, **kwargs) + return HTML.tag(self.vue_tagname, **kwargs) def set_json_data(self, key, value): """ @@ -997,7 +1078,12 @@ class Form(object): templates.append(HTML.literal(render(template, context))) return HTML.literal('\n').join(templates) - def render_field_complete(self, fieldname, bfield_attrs={}): + def render_vue_field(self, fieldname, **kwargs): + """ """ + return self.render_field_complete(fieldname, **kwargs) + + def render_field_complete(self, fieldname, bfield_attrs={}, + session=None): """ Render the given field completely, i.e. with ```` wrapper. Note that this is meant to render *editable* fields, @@ -1015,7 +1101,7 @@ class Form(object): if self.field_visible(fieldname): label = self.get_label(fieldname) - markdowns = self.get_field_markdowns() + markdowns = self.get_field_markdowns(session=session) # these attrs will be for the (*not* the widget) attrs = { diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index b4610a18..3f1769cf 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -198,7 +198,8 @@ class Grid: checkable=None, row_uuid_getter=None, clicking_row_checks_box=False, click_handlers=None, main_actions=[], more_actions=[], delete_speedbump=False, - ajax_data_url=None, component='tailbone-grid', + ajax_data_url=None, + vue_tagname=None, expose_direct_link=False, **kwargs): @@ -268,19 +269,63 @@ class Grid: if ajax_data_url: self.ajax_data_url = ajax_data_url elif self.request: - self.ajax_data_url = self.request.current_route_url(_query=None) + self.ajax_data_url = self.request.path_url else: self.ajax_data_url = '' - self.component = component + # vue_tagname + self.vue_tagname = vue_tagname + if not self.vue_tagname and kwargs.get('component'): + warnings.warn("component kwarg is deprecated for Grid(); " + "please use vue_tagname param instead", + DeprecationWarning, stacklevel=2) + self.vue_tagname = kwargs['component'] + if not self.vue_tagname: + self.vue_tagname = 'tailbone-grid' + self.expose_direct_link = expose_direct_link self._whgrid_kwargs = kwargs @property - def component_studly(self): - words = self.component.split('-') + def vue_component(self): + """ + String name for the Vue component, e.g. ``'TailboneGrid'``. + + This is a generated value based on :attr:`vue_tagname`. + """ + words = self.vue_tagname.split('-') return ''.join([word.capitalize() for word in words]) + @property + def component(self): + """ + DEPRECATED - use :attr:`vue_tagname` instead. + """ + warnings.warn("Grid.component is deprecated; " + "please use vue_tagname instead", + DeprecationWarning, stacklevel=2) + return self.vue_tagname + + @property + def component_studly(self): + """ + DEPRECATED - use :attr:`vue_component` instead. + """ + warnings.warn("Grid.component_studly is deprecated; " + "please use vue_component instead", + DeprecationWarning, stacklevel=2) + return self.vue_component + + @property + def actions(self): + """ """ + actions = [] + if self.main_actions: + actions.extend(self.main_actions) + if self.more_actions: + actions.extend(self.more_actions) + return actions + def make_columns(self): """ Return a default list of columns, based on :attr:`model_class`. @@ -1334,6 +1379,21 @@ class Grid: data = self.pager return data + def render_vue_tag(self, master=None, **kwargs): + """ """ + kwargs.setdefault('ref', 'grid') + kwargs.setdefault(':csrftoken', 'csrftoken') + + if (master and master.deletable and master.has_perm('delete') + and master.delete_confirm == 'simple'): + kwargs.setdefault('@deleteActionClicked', 'deleteObject') + + return HTML.tag(self.vue_tagname, **kwargs) + + def render_vue_template(self, template='/grids/complete.mako', **context): + """ """ + return self.render_complete(template=template, **context) + def render_complete(self, template='/grids/complete.mako', **kwargs): """ Render the grid, complete with filters. Note that this also @@ -1359,7 +1419,8 @@ class Grid: context['request'] = self.request context.setdefault('allow_save_defaults', True) context.setdefault('view_click_handler', self.get_view_click_handler()) - return render(template, context) + html = render(template, context) + return HTML.literal(html) def render_buefy(self, **kwargs): warnings.warn("Grid.render_buefy() is deprecated; " @@ -1575,6 +1636,10 @@ class Grid: return True return False + def get_vue_columns(self): + """ """ + return self.get_table_columns() + def get_table_columns(self): """ Return a list of dicts representing all grid columns. Meant @@ -1600,11 +1665,19 @@ class Grid: if hasattr(rowobj, 'uuid'): return rowobj.uuid + def get_vue_data(self): + """ """ + table_data = self.get_table_data() + return table_data['data'] + def get_table_data(self): """ Returns a list of data rows for the grid, for use with client-side JS table. """ + if hasattr(self, '_table_data'): + return self._table_data + # filter / sort / paginate to get "visible" data raw_data = self.make_visible_data() data = [] @@ -1704,7 +1777,8 @@ class Grid: else: results['total_items'] = count - return results + self._table_data = results + return self._table_data def set_action_urls(self, row, rowobj, i): """ diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py index 02c4e518..268d4818 100644 --- a/tailbone/subscribers.py +++ b/tailbone/subscribers.py @@ -48,7 +48,7 @@ from tailbone.util import get_available_themes, get_global_search_options log = logging.getLogger(__name__) -def new_request(event): +def new_request(event, session=None): """ Event hook called when processing a new request. @@ -64,15 +64,6 @@ def new_request(event): Reference to the app :term:`config object`. Note that this will be the same as :attr:`wuttaweb:request.wutta_config`. - .. method:: request.has_perm(name) - - Function to check if current user has the given permission. - - .. method:: request.has_any_perm(*names) - - Function to check if current user has any of the given - permissions. - .. method:: request.register_component(tagname, classname) Function to register a Vue component for use with the app. @@ -90,6 +81,7 @@ def new_request(event): config = request.wutta_config app = config.get_app() auth = app.get_auth_handler() + session = session or Session() # compatibility rattail_config = config @@ -104,50 +96,31 @@ def new_request(event): return user # invoke upstream hook to set user - base.new_request_set_user(event, user_getter=user_getter, db_session=Session()) + base.new_request_set_user(event, user_getter=user_getter, db_session=session) # assign client IP address to the session, for sake of versioning - Session().continuum_remote_addr = request.client_addr + if hasattr(request, 'client_addr'): + session.continuum_remote_addr = request.client_addr - # TODO: why would this ever be null? - if rattail_config: + # request.register_component() + def register_component(tagname, classname): + """ + Register a Vue 3 component, so the base template knows to + declare it for use within the app (page). + """ + if not hasattr(request, '_tailbone_registered_components'): + request._tailbone_registered_components = OrderedDict() - app = rattail_config.get_app() - auth = app.get_auth_handler() - request.tailbone_cached_permissions = auth.get_permissions( - Session(), request.user) + if tagname in request._tailbone_registered_components: + log.warning("component with tagname '%s' already registered " + "with class '%s' but we are replacing that with " + "class '%s'", + tagname, + request._tailbone_registered_components[tagname], + classname) - def has_perm(name): - if name in request.tailbone_cached_permissions: - return True - return request.is_root - request.has_perm = has_perm - - def has_any_perm(*names): - for name in names: - if has_perm(name): - return True - return False - request.has_any_perm = has_any_perm - - def register_component(tagname, classname): - """ - Register a Vue 3 component, so the base template knows to - declare it for use within the app (page). - """ - if not hasattr(request, '_tailbone_registered_components'): - request._tailbone_registered_components = OrderedDict() - - if tagname in request._tailbone_registered_components: - log.warning("component with tagname '%s' already registered " - "with class '%s' but we are replacing that with " - "class '%s'", - tagname, - request._tailbone_registered_components[tagname], - classname) - - request._tailbone_registered_components[tagname] = classname - request.register_component = register_component + request._tailbone_registered_components[tagname] = classname + request.register_component = register_component def before_render(event): diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index c4cbd648..6811397b 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -153,12 +153,16 @@ @@ -856,7 +860,7 @@ feedbackMessage: "", % if expose_theme_picker and request.has_perm('common.change_app_theme'): - globalTheme: ${json.dumps(theme)|n}, + globalTheme: ${json.dumps(theme or None)|n}, referrer: location.href, % endif @@ -866,7 +870,7 @@ globalSearchActive: false, globalSearchTerm: '', - globalSearchData: ${json.dumps(global_search_data)|n}, + globalSearchData: ${json.dumps(global_search_data or [])|n}, mountedHooks: [], } diff --git a/tailbone/templates/form.mako b/tailbone/templates/form.mako index c9c8ea88..fec721fd 100644 --- a/tailbone/templates/form.mako +++ b/tailbone/templates/form.mako @@ -6,12 +6,12 @@ <%def name="render_form_buttons()"> <%def name="render_form_template()"> - ${form.render_deform(buttons=capture(self.render_form_buttons))|n} + ${form.render_vue_template(buttons=capture(self.render_form_buttons))|n} <%def name="render_form()">
- ${form.render_vuejs_component()} + ${form.render_vue_tag()}
@@ -111,9 +111,9 @@ % if form is not Undefined: % endif diff --git a/tailbone/templates/forms/deform.mako b/tailbone/templates/forms/deform.mako index 00cf2c50..26c8b4ee 100644 --- a/tailbone/templates/forms/deform.mako +++ b/tailbone/templates/forms/deform.mako @@ -1,19 +1,19 @@ ## -*- coding: utf-8; -*- -<% request.register_component(form.component, form.component_studly) %> +<% request.register_component(form.vue_tagname, form.vue_component) %> - diff --git a/tailbone/templates/master/form.mako b/tailbone/templates/master/form.mako index dfe56fa8..dc9743ea 100644 --- a/tailbone/templates/master/form.mako +++ b/tailbone/templates/master/form.mako @@ -6,13 +6,13 @@ - % if form is not Undefined: + % if form is not Undefined and hasattr(form, 'render_included_templates'): ${form.render_included_templates()} % endif diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako index 33592559..81c11213 100644 --- a/tailbone/templates/master/index.mako +++ b/tailbone/templates/master/index.mako @@ -15,7 +15,7 @@ <%def name="grid_tools()"> ## grid totals - % if master.supports_grid_totals: + % if getattr(master, 'supports_grid_totals', False):
<%def name="make_grid_component()"> - ## TODO: stop using |n filter? - ${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n} + ${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())} <%def name="render_grid_component()"> - <${grid.component} ref="grid" :csrftoken="csrftoken" - % if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple': - @deleteActionClicked="deleteObject" - % endif - > - + ${grid.render_vue_tag()} <%def name="make_this_page_component()"> @@ -313,10 +307,8 @@ ## finalize grid @@ -328,11 +320,11 @@ ${parent.modify_this_page_vars()} + <%def name="extra_javascript()"> @@ -171,7 +171,7 @@ ${h.stylesheet_link(user_css)} % else: ## upstream Buefy CSS - ${h.stylesheet_link(h.get_liburl(request, 'buefy.css'))} + ${h.stylesheet_link(h.get_liburl(request, 'buefy.css', prefix='tailbone'))} % endif diff --git a/tailbone/templates/themes/butterball/base.mako b/tailbone/templates/themes/butterball/base.mako index f06b45f9..306b3430 100644 --- a/tailbone/templates/themes/butterball/base.mako +++ b/tailbone/templates/themes/butterball/base.mako @@ -71,12 +71,12 @@ { ## TODO: eventually version / url should be configurable "imports": { - "vue": "${h.get_liburl(request, 'bb_vue')}", - "@oruga-ui/oruga-next": "${h.get_liburl(request, 'bb_oruga')}", - "@oruga-ui/theme-bulma": "${h.get_liburl(request, 'bb_oruga_bulma')}", - "@fortawesome/fontawesome-svg-core": "${h.get_liburl(request, 'bb_fontawesome_svg_core')}", - "@fortawesome/free-solid-svg-icons": "${h.get_liburl(request, 'bb_free_solid_svg_icons')}", - "@fortawesome/vue-fontawesome": "${h.get_liburl(request, 'bb_vue_fontawesome')}" + "vue": "${h.get_liburl(request, 'bb_vue', prefix='tailbone')}", + "@oruga-ui/oruga-next": "${h.get_liburl(request, 'bb_oruga', prefix='tailbone')}", + "@oruga-ui/theme-bulma": "${h.get_liburl(request, 'bb_oruga_bulma', prefix='tailbone')}", + "@fortawesome/fontawesome-svg-core": "${h.get_liburl(request, 'bb_fontawesome_svg_core', prefix='tailbone')}", + "@fortawesome/free-solid-svg-icons": "${h.get_liburl(request, 'bb_free_solid_svg_icons', prefix='tailbone')}", + "@fortawesome/vue-fontawesome": "${h.get_liburl(request, 'bb_vue_fontawesome', prefix='tailbone')}" } } @@ -92,7 +92,7 @@ % if user_css: ${h.stylesheet_link(user_css)} % else: - ${h.stylesheet_link(h.get_liburl(request, 'bb_oruga_bulma_css'))} + ${h.stylesheet_link(h.get_liburl(request, 'bb_oruga_bulma_css', prefix='tailbone'))} % endif diff --git a/tailbone/util.py b/tailbone/util.py index eb6fb8a8..594fd69b 100644 --- a/tailbone/util.py +++ b/tailbone/util.py @@ -39,7 +39,9 @@ from pyramid.renderers import get_renderer from pyramid.interfaces import IRoutesMapper from webhelpers2.html import HTML, tags -from wuttaweb.util import get_form_data as wutta_get_form_data +from wuttaweb.util import (get_form_data as wutta_get_form_data, + get_libver as wutta_get_libver, + get_liburl as wutta_get_liburl) log = logging.getLogger(__name__) @@ -103,154 +105,32 @@ def get_global_search_options(request): return options -def get_libver(request, key, fallback=True, default_only=False): +def get_libver(request, key, fallback=True, default_only=False): # pragma: no cover """ - Return the appropriate URL for the library identified by ``key``. + DEPRECATED - use :func:`wuttaweb:wuttaweb.util.get_libver()` + instead. """ - config = request.rattail_config + warnings.warn("tailbone.util.get_libver() is deprecated; " + "please use wuttaweb.util.get_libver() instead", + DeprecationWarning, stacklevel=2) - if not default_only: - version = config.get('tailbone', 'libver.{}'.format(key)) - if version: - return version - - if not fallback and not default_only: - - if key == 'buefy': - version = config.get('tailbone', 'buefy_version') - if version: - return version - - elif key == 'buefy.css': - version = get_libver(request, 'buefy', fallback=False) - if version: - return version - - elif key == 'vue': - version = config.get('tailbone', 'vue_version') - if version: - return version - - return - - if key == 'buefy': - if not default_only: - version = config.get('tailbone', 'buefy_version') - if version: - return version - return 'latest' - - elif key == 'buefy.css': - version = get_libver(request, 'buefy', default_only=default_only) - if version: - return version - return 'latest' - - elif key == 'vue': - if not default_only: - version = config.get('tailbone', 'vue_version') - if version: - return version - return '2.6.14' - - elif key == 'vue_resource': - return 'latest' - - elif key == 'fontawesome': - return '5.3.1' - - elif key == 'bb_vue': - return '3.4.31' - - elif key == 'bb_oruga': - return '0.8.12' - - elif key in ('bb_oruga_bulma', 'bb_oruga_bulma_css'): - return '0.3.0' - - elif key == 'bb_fontawesome_svg_core': - return '6.5.2' - - elif key == 'bb_free_solid_svg_icons': - return '6.5.2' - - elif key == 'bb_vue_fontawesome': - return '3.0.6' + return wutta_get_libver(request, key, prefix='tailbone', + configured_only=not fallback, + default_only=default_only) -def get_liburl(request, key, fallback=True): +def get_liburl(request, key, fallback=True): # pragma: no cover """ - Return the appropriate URL for the library identified by ``key``. + DEPRECATED - use :func:`wuttaweb:wuttaweb.util.get_liburl()` + instead. """ - config = request.rattail_config + warnings.warn("tailbone.util.get_liburl() is deprecated; " + "please use wuttaweb.util.get_liburl() instead", + DeprecationWarning, stacklevel=2) - url = config.get('tailbone', 'liburl.{}'.format(key)) - if url: - return url - - if not fallback: - return - - version = get_libver(request, key) - - static = config.get('tailbone.static_libcache.module') - if static: - static = importlib.import_module(static) - needed = request.environ['fanstatic.needed'] - liburl = needed.library_url(static.libcache) + '/' - # nb. add custom url prefix if needed, e.g. /theo - if request.script_name: - liburl = request.script_name + liburl - - if key == 'buefy': - return 'https://unpkg.com/buefy@{}/dist/buefy.min.js'.format(version) - - elif key == 'buefy.css': - return 'https://unpkg.com/buefy@{}/dist/buefy.min.css'.format(version) - - elif key == 'vue': - return 'https://unpkg.com/vue@{}/dist/vue.min.js'.format(version) - - elif key == 'vue_resource': - return 'https://cdn.jsdelivr.net/npm/vue-resource@{}'.format(version) - - elif key == 'fontawesome': - return 'https://use.fontawesome.com/releases/v{}/js/all.js'.format(version) - - elif key == 'bb_vue': - if static and hasattr(static, 'bb_vue_js'): - return liburl + static.bb_vue_js.relpath - return f'https://unpkg.com/vue@{version}/dist/vue.esm-browser.prod.js' - - elif key == 'bb_oruga': - if static and hasattr(static, 'bb_oruga_js'): - return liburl + static.bb_oruga_js.relpath - return f'https://unpkg.com/@oruga-ui/oruga-next@{version}/dist/oruga.mjs' - - elif key == 'bb_oruga_bulma': - if static and hasattr(static, 'bb_oruga_bulma_js'): - return liburl + static.bb_oruga_bulma_js.relpath - return f'https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.mjs' - - elif key == 'bb_oruga_bulma_css': - if static and hasattr(static, 'bb_oruga_bulma_css'): - return liburl + static.bb_oruga_bulma_css.relpath - return f'https://unpkg.com/@oruga-ui/theme-bulma@{version}/dist/bulma.css' - - elif key == 'bb_fontawesome_svg_core': - if static and hasattr(static, 'bb_fontawesome_svg_core_js'): - return liburl + static.bb_fontawesome_svg_core_js.relpath - return f'https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-svg-core@{version}/+esm' - - elif key == 'bb_free_solid_svg_icons': - if static and hasattr(static, 'bb_free_solid_svg_icons_js'): - return liburl + static.bb_free_solid_svg_icons_js.relpath - return f'https://cdn.jsdelivr.net/npm/@fortawesome/free-solid-svg-icons@{version}/+esm' - - elif key == 'bb_vue_fontawesome': - if static and hasattr(static, 'bb_vue_fontawesome_js'): - return liburl + static.bb_vue_fontawesome_js.relpath - return f'https://cdn.jsdelivr.net/npm/@fortawesome/vue-fontawesome@{version}/+esm' + return wutta_get_liburl(request, key, prefix='tailbone', + configured_only=not fallback, + default_only=False) def pretty_datetime(config, value): diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py index 8d389530..9d7f6e02 100644 --- a/tailbone/views/settings.py +++ b/tailbone/views/settings.py @@ -24,24 +24,23 @@ Settings Views """ +import json import os import re import subprocess import sys from collections import OrderedDict -import json +import colander from rattail.db.model import Setting from rattail.settings import Setting as AppSetting from rattail.util import import_module_path -import colander - from tailbone import forms from tailbone.db import Session from tailbone.views import MasterView, View -from tailbone.util import get_libver, get_liburl +from wuttaweb.util import get_libver, get_liburl class AppInfoView(MasterView): @@ -99,10 +98,9 @@ class AppInfoView(MasterView): kwargs['configure_button_title'] = "Configure App" return kwargs - def configure_get_context(self, **kwargs): - context = super().configure_get_context(**kwargs) - - weblibs = OrderedDict([ + def get_weblibs(self): + """ """ + return OrderedDict([ ('vue', "Vue"), ('vue_resource', "vue-resource"), ('buefy', "Buefy"), @@ -117,6 +115,12 @@ class AppInfoView(MasterView): ('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() + for key in weblibs: title = weblibs[key] weblibs[key] = { @@ -125,19 +129,33 @@ class AppInfoView(MasterView): # nb. these values are exactly as configured, and are # used for editing the settings - 'configured_version': get_libver(self.request, key, fallback=False), - 'configured_url': get_liburl(self.request, key, fallback=False), + '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, default_only=True), - 'live_url': get_liburl(self.request, key), + 'default_version': get_libver(self.request, key, + prefix='tailbone', + default_only=True), + 'live_url': get_liburl(self.request, key, + prefix='tailbone'), } + # TODO: this is only needed to migrate legacy settings to + # use the newer wutaweb 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'] + context['weblibs'] = list(weblibs.values()) return context def configure_get_simple_settings(self): - return [ + """ """ + simple_settings = [ # basics {'section': 'rattail', @@ -167,63 +185,6 @@ class AppInfoView(MasterView): # 'type': int }, - # web libs - {'section': 'tailbone', - 'option': 'libver.vue'}, - {'section': 'tailbone', - 'option': 'liburl.vue'}, - {'section': 'tailbone', - 'option': 'libver.vue_resource'}, - {'section': 'tailbone', - 'option': 'liburl.vue_resource'}, - {'section': 'tailbone', - 'option': 'libver.buefy'}, - {'section': 'tailbone', - 'option': 'liburl.buefy'}, - {'section': 'tailbone', - 'option': 'libver.buefy.css'}, - {'section': 'tailbone', - 'option': 'liburl.buefy.css'}, - {'section': 'tailbone', - 'option': 'libver.fontawesome'}, - {'section': 'tailbone', - 'option': 'liburl.fontawesome'}, - - {'section': 'tailbone', - 'option': 'libver.bb_vue'}, - {'section': 'tailbone', - 'option': 'liburl.bb_vue'}, - - {'section': 'tailbone', - 'option': 'libver.bb_oruga'}, - {'section': 'tailbone', - 'option': 'liburl.bb_oruga'}, - - {'section': 'tailbone', - 'option': 'libver.bb_oruga_bulma'}, - {'section': 'tailbone', - 'option': 'liburl.bb_oruga_bulma'}, - - {'section': 'tailbone', - 'option': 'libver.bb_oruga_bulma_css'}, - {'section': 'tailbone', - 'option': 'liburl.bb_oruga_bulma_css'}, - - {'section': 'tailbone', - 'option': 'libver.bb_fontawesome_svg_core'}, - {'section': 'tailbone', - 'option': 'liburl.bb_fontawesome_svg_core'}, - - {'section': 'tailbone', - 'option': 'libver.bb_free_solid_svg_icons'}, - {'section': 'tailbone', - 'option': 'liburl.bb_free_solid_svg_icons'}, - - {'section': 'tailbone', - 'option': 'libver.bb_vue_fontawesome'}, - {'section': 'tailbone', - 'option': 'liburl.bb_vue_fontawesome'}, - # nb. these are no longer used (deprecated), but we keep # them defined here so the tool auto-deletes them {'section': 'tailbone', @@ -233,6 +194,36 @@ class AppInfoView(MasterView): ] + 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}", + }) + + return simple_settings + class SettingView(MasterView): """ diff --git a/tests/views/test_settings.py b/tests/views/test_settings.py new file mode 100644 index 00000000..b8523729 --- /dev/null +++ b/tests/views/test_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8; -*- + +from tailbone.views import settings as mod +from tests.util import WebTestCase + + +class TestSettingView(WebTestCase): + + def test_includeme(self): + self.pyramid_config.include('tailbone.views.settings') From bbd98e7b2f0ec57c3b4ffcd0b30786e8f0449504 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 15 Aug 2024 23:15:25 -0500 Subject: [PATCH 040/142] =?UTF-8?q?bump:=20version=200.16.1=20=E2=86=92=20?= =?UTF-8?q?0.17.0?= 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 f532ae03..5724e685 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.17.0 (2024-08-15) + +### Feat + +- use wuttaweb for `get_liburl()` logic + ## v0.16.1 (2024-08-15) ### Fix diff --git a/pyproject.toml b/pyproject.toml index 69c35a68..31c7ef8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.16.1" +version = "0.17.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.7.0", + "WuttaWeb>=0.8.1", "zope.sqlalchemy>=1.5", ] From 09612b1921af0a7b3bb7141381c3bb861b4d64ab Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 15 Aug 2024 23:46:58 -0500 Subject: [PATCH 041/142] fix: fix some more wutta compat for base template missed those earlier --- tailbone/templates/base.mako | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index 27e900e4..3a12859e 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -280,7 +280,7 @@ ${index_title} - % if master.creatable and master.show_create_link and master.has_perm('create'): + % if master.creatable and getattr(master, 'show_create_link', True) and master.has_perm('create'): ${h.link_to(instance_title, instance_url)} - % elif master.creatable and master.show_create_link and master.has_perm('create'): + % elif master.creatable and getattr(master, 'show_create_link', True) and master.has_perm('create'): % if not request.matched_route.name.endswith('.create'): Date: Fri, 16 Aug 2024 11:56:12 -0500 Subject: [PATCH 042/142] feat: inherit most logic from wuttaweb, for GridAction --- tailbone/grids/core.py | 65 ++++++++++---------------- tailbone/templates/grids/b-table.mako | 11 ++--- tailbone/templates/grids/complete.mako | 8 +--- tailbone/views/master.py | 8 +++- tailbone/views/people.py | 2 +- tailbone/views/purchasing/receiving.py | 4 +- tailbone/views/roles.py | 2 +- tests/grids/test_core.py | 17 +++++++ tests/views/test_master.py | 9 ++++ 9 files changed, 65 insertions(+), 61 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 3f1769cf..b9254c18 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -38,6 +38,7 @@ from pyramid.renderers import render from webhelpers2.html import HTML, tags from paginate_sqlalchemy import SqlalchemyOrmPage +from wuttaweb.grids import GridAction as WuttaGridAction from . import filters as gridfilters from tailbone.db import Session from tailbone.util import raw_datetime @@ -1801,18 +1802,20 @@ class Grid: return False -class GridAction(object): +class GridAction(WuttaGridAction): """ - Represents an action available to a grid. This is used to construct the - 'actions' column when rendering the grid. + Represents a "row action" hyperlink within a grid context. - :param key: Key for the action (e.g. ``'edit'``), unique within - the grid. + This is a subclass of + :class:`wuttaweb:wuttaweb.grids.base.GridAction`. - :param label: Label to be displayed for the action. If not set, - will be a capitalized version of ``key``. + .. warning:: - :param icon: Icon name for the action. + This class remains for now, to retain compatibility with + existing code. But at some point the WuttaWeb class will + supersede this one entirely. + + :param target: HTML "target" attribute for the ```` tag. :param click_handler: Optional JS click handler for the action. This value will be rendered as-is within the final grid @@ -1824,41 +1827,23 @@ class GridAction(object): * ``$emit('do-something', props.row)`` """ - def __init__(self, key, label=None, url='#', icon=None, target=None, - link_class=None, click_handler=None): - self.key = key - self.label = label or prettify(key) - self.icon = icon - self.url = url + def __init__( + self, + request, + key, + target=None, + click_handler=None, + **kwargs, + ): + # TODO: previously url default was '#' - but i don't think we + # need that anymore? guess we'll see.. + #kwargs.setdefault('url', '#') + + super().__init__(request, key, **kwargs) + self.target = target - self.link_class = link_class self.click_handler = click_handler - def get_url(self, row, i): - """ - Returns an action URL for the given row. - """ - if callable(self.url): - return self.url(row, i) - return self.url - - def render_icon(self): - """ - Render the HTML snippet for the action link icon. - """ - return HTML.tag('i', class_='fas fa-{}'.format(self.icon)) - - def render_label(self): - """ - Render the label "text" within the actions column of a grid - row. Most actions have a static label that never varies, but - you can override this to add e.g. HTML content. Note that the - return value will be treated / rendered as HTML whether or not - it contains any, so perhaps be careful that it is trusted - content. - """ - return self.label - class URLMaker(object): """ diff --git a/tailbone/templates/grids/b-table.mako b/tailbone/templates/grids/b-table.mako index 632193b5..da9f2aae 100644 --- a/tailbone/templates/grids/b-table.mako +++ b/tailbone/templates/grids/b-table.mako @@ -53,11 +53,11 @@ % endfor - % if grid.main_actions or grid.more_actions: + % if grid.actions: <${b}-table-column field="actions" label="Actions" v-slot="props"> - % for action in grid.main_actions: + % for action in grid.actions: - % if request.use_oruga: - - % else: - - % endif - ${action.label} + ${action.render_icon_and_label()}   % endfor diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index fc48916b..93bb6c26 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -163,13 +163,7 @@ target="${action.target}" % endif > - % if request.use_oruga: - - ${action.render_label()|n} - % else: - ${action.render_icon()|n} - ${action.render_label()|n} - % endif + ${action.render_icon_and_label()}   % endfor diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 0d322da3..097cb229 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -3220,14 +3220,18 @@ class MasterView(View): def make_action(self, key, url=None, factory=None, **kwargs): """ - Make a new :class:`GridAction` instance for the current grid. + Make and return a new :class:`~tailbone.grids.core.GridAction` + instance. + + This can be called to make actions for any grid, not just the + one from :meth:`index()`. """ if url is None: route = '{}.{}'.format(self.get_route_prefix(), key) url = lambda r, i: self.request.route_url(route, **self.get_action_route_kwargs(r)) if not factory: factory = grids.GridAction - return factory(key, url=url, **kwargs) + return factory(self.request, key, url=url, **kwargs) def get_action_route_kwargs(self, obj): """ diff --git a/tailbone/views/people.py b/tailbone/views/people.py index 94c85821..163a9a52 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -552,7 +552,7 @@ class PersonView(MasterView): if self.request.has_perm('trainwreck.transactions.view'): url = lambda row, i: self.request.route_url('trainwreck.transactions.view', uuid=row.uuid) - g.main_actions.append(grids.GridAction('view', icon='eye', url=url)) + g.main_actions.append(self.make_action('view', icon='eye', url=url)) g.load_settings() g.set_enum('system', self.enum.TRAINWRECK_SYSTEM) diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 55936184..0a305f0a 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -40,7 +40,7 @@ from webhelpers2.html import tags, HTML from wuttaweb.util import get_form_data -from tailbone import forms, grids +from tailbone import forms from tailbone.views.purchasing import PurchasingBatchView @@ -1031,7 +1031,7 @@ class ReceivingBatchView(PurchasingBatchView): if batch.is_truck_dump_parent(): permission_prefix = self.get_permission_prefix() if self.request.has_perm('{}.edit_row'.format(permission_prefix)): - transform = grids.GridAction('transform', + transform = self.make_action('transform', icon='shuffle', label="Transform to Unit", url=self.transform_unit_url) diff --git a/tailbone/views/roles.py b/tailbone/views/roles.py index b34b3673..fb834479 100644 --- a/tailbone/views/roles.py +++ b/tailbone/views/roles.py @@ -363,7 +363,7 @@ class RoleView(PrincipalMasterView): if role.users: users = sorted(role.users, key=lambda u: u.username) actions = [ - grids.GridAction('view', icon='zoomin', + self.make_action('view', icon='zoomin', url=lambda r, i: self.request.route_url('users.view', uuid=r.uuid)) ] kwargs['users'] = grids.Grid(None, users, ['username', 'active'], diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index e6f9d675..0a8d5d66 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -137,3 +137,20 @@ class TestGrid(WebTestCase): # calling again returns same data data2 = grid.get_vue_data() self.assertIs(data2, data) + + +class TestGridAction(WebTestCase): + + def test_constructor(self): + + # null by default + action = mod.GridAction(self.request, 'view') + self.assertIsNone(action.target) + self.assertIsNone(action.click_handler) + + # but can set them + action = mod.GridAction(self.request, 'view', + target='_blank', + click_handler='doSomething(props.row)') + self.assertEqual(action.target, '_blank') + self.assertEqual(action.click_handler, 'doSomething(props.row)') diff --git a/tests/views/test_master.py b/tests/views/test_master.py index 19321496..572875a0 100644 --- a/tests/views/test_master.py +++ b/tests/views/test_master.py @@ -3,6 +3,7 @@ from unittest.mock import patch from tailbone.views import master as mod +from wuttaweb.grids import GridAction from tests.util import WebTestCase @@ -24,3 +25,11 @@ class TestMasterView(WebTestCase): # sanity / coverage check kw = view.make_form_kwargs(model_instance=setting) self.assertIsNotNone(kw['action_url']) + + def test_make_action(self): + model = self.app.model + with patch.multiple(mod.MasterView, create=True, + model_class=model.Setting): + view = self.make_view() + action = view.make_action('view') + self.assertIsInstance(action, GridAction) From f7641218cb44c6ad18d6672361d1f1243c05e397 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 11:56:54 -0500 Subject: [PATCH 043/142] fix: avoid route error in user view, when using wutta people view kind of a temporary edge case here, can eventually change it back --- tailbone/views/users.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tailbone/views/users.py b/tailbone/views/users.py index f8bcb1b8..9eae74d8 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -208,9 +208,13 @@ class UserView(PrincipalMasterView): person_display = str(person) elif self.editing: person_display = str(user.person or '') - people_url = self.request.route_url('people.autocomplete') - f.set_widget('person_uuid', forms.widgets.JQueryAutocompleteWidget( - field_display=person_display, service_url=people_url)) + try: + people_url = self.request.route_url('people.autocomplete') + except KeyError: + pass # TODO: wutta compat + else: + f.set_widget('person_uuid', forms.widgets.JQueryAutocompleteWidget( + field_display=person_display, service_url=people_url)) f.set_validator('person_uuid', self.valid_person) f.set_label('person_uuid', "Person") From 2a0b6da2f9169c22c099ca2c367a3ab2d89fa6e2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 14:34:50 -0500 Subject: [PATCH 044/142] feat: inherit from wutta base class for Grid --- tailbone/grids/core.py | 241 ++++++++++--------------- tailbone/views/batch/core.py | 8 +- tailbone/views/batch/pos.py | 1 + tailbone/views/customers.py | 19 +- tailbone/views/custorders/items.py | 1 + tailbone/views/custorders/orders.py | 71 ++++---- tailbone/views/departments.py | 8 +- tailbone/views/email.py | 2 +- tailbone/views/employees.py | 3 +- tailbone/views/master.py | 48 +++-- tailbone/views/members.py | 3 +- tailbone/views/people.py | 19 +- tailbone/views/poser/reports.py | 2 +- tailbone/views/principal.py | 6 +- tailbone/views/products.py | 28 +-- tailbone/views/purchasing/batch.py | 4 +- tailbone/views/purchasing/receiving.py | 18 +- tailbone/views/reports.py | 12 +- tailbone/views/roles.py | 15 +- tailbone/views/tempmon/core.py | 6 +- tailbone/views/trainwreck/base.py | 12 +- tailbone/views/users.py | 15 +- tests/grids/test_core.py | 49 ++++- 23 files changed, 317 insertions(+), 274 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index b9254c18..a5617215 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -38,7 +38,7 @@ from pyramid.renderers import render from webhelpers2.html import HTML, tags from paginate_sqlalchemy import SqlalchemyOrmPage -from wuttaweb.grids import GridAction as WuttaGridAction +from wuttaweb.grids import Grid as WuttaGrid, GridAction as WuttaGridAction from . import filters as gridfilters from tailbone.db import Session from tailbone.util import raw_datetime @@ -61,7 +61,7 @@ class FieldList(list): self.insert(i + 1, newfield) -class Grid: +class Grid(WuttaGrid): """ Core grid class. In sore need of documentation. @@ -186,32 +186,59 @@ class Grid: grid.row_uuid_getter = fake_uuid """ - def __init__(self, key, data, columns=None, width='auto', request=None, - model_class=None, model_title=None, model_title_plural=None, - enums={}, labels={}, assume_local_times=False, renderers={}, invisible=[], - raw_renderers={}, - extra_row_class=None, linked_columns=[], url='#', - joiners={}, filterable=False, filters={}, use_byte_string_filters=False, - searchable={}, - sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc', - pageable=False, default_pagesize=None, default_page=1, - checkboxes=False, checked=None, check_handler=None, check_all_handler=None, - checkable=None, row_uuid_getter=None, - clicking_row_checks_box=False, click_handlers=None, - main_actions=[], more_actions=[], delete_speedbump=False, - ajax_data_url=None, - vue_tagname=None, - expose_direct_link=False, - **kwargs): + def __init__( + self, + request, + key=None, + data=None, + width='auto', + model_title=None, + model_title_plural=None, + enums={}, + assume_local_times=False, + invisible=[], + raw_renderers={}, + extra_row_class=None, + url='#', + joiners={}, + filterable=False, + filters={}, + use_byte_string_filters=False, + searchable={}, + sortable=False, + sorters={}, + default_sortkey=None, + default_sortdir='asc', + pageable=False, + default_pagesize=None, + default_page=1, + checkboxes=False, + checked=None, + check_handler=None, + check_all_handler=None, + checkable=None, + row_uuid_getter=None, + clicking_row_checks_box=False, + click_handlers=None, + main_actions=[], + more_actions=[], + delete_speedbump=False, + ajax_data_url=None, + expose_direct_link=False, + **kwargs, + ): + if kwargs.get('component'): + warnings.warn("component param is deprecated for Grid(); " + "please use vue_tagname param instead", + DeprecationWarning, stacklevel=2) + kwargs.setdefault('vue_tagname', kwargs.pop('component')) - self.key = key - self.data = data - self.columns = FieldList(columns) if columns is not None else None - self.width = width - self.request = request - self.model_class = model_class - if self.model_class and self.columns is None: - self.columns = self.make_columns() + # TODO: pretty sure this should go away? + kwargs.setdefault('vue_tagname', 'tailbone-grid') + + kwargs['key'] = key + kwargs['data'] = data + super().__init__(request, **kwargs) self.model_title = model_title if not self.model_title and self.model_class and hasattr(self.model_class, 'get_model_title'): @@ -224,15 +251,13 @@ class Grid: if not self.model_title_plural: self.model_title_plural = '{}s'.format(self.model_title) + self.width = width self.enums = enums or {} - - self.labels = labels or {} self.assume_local_times = assume_local_times - self.renderers = self.make_default_renderers(renderers or {}) + self.renderers = self.make_default_renderers(self.renderers) self.raw_renderers = raw_renderers or {} self.invisible = invisible or [] self.extra_row_class = extra_row_class - self.linked_columns = linked_columns or [] self.url = url self.joiners = joiners or {} @@ -263,8 +288,6 @@ class Grid: self.click_handlers = click_handlers or {} - self.main_actions = main_actions or [] - self.more_actions = more_actions or [] self.delete_speedbump = delete_speedbump if ajax_data_url: @@ -274,29 +297,22 @@ class Grid: else: self.ajax_data_url = '' - # vue_tagname - self.vue_tagname = vue_tagname - if not self.vue_tagname and kwargs.get('component'): - warnings.warn("component kwarg is deprecated for Grid(); " - "please use vue_tagname param instead", + self.main_actions = main_actions or [] + if self.main_actions: + warnings.warn("main_actions param is deprecated for Grdi(); " + "please use actions param instead", DeprecationWarning, stacklevel=2) - self.vue_tagname = kwargs['component'] - if not self.vue_tagname: - self.vue_tagname = 'tailbone-grid' + self.actions.extend(self.main_actions) + self.more_actions = more_actions or [] + if self.more_actions: + warnings.warn("more_actions param is deprecated for Grdi(); " + "please use actions param instead", + DeprecationWarning, stacklevel=2) + self.actions.extend(self.more_actions) self.expose_direct_link = expose_direct_link self._whgrid_kwargs = kwargs - @property - def vue_component(self): - """ - String name for the Vue component, e.g. ``'TailboneGrid'``. - - This is a generated value based on :attr:`vue_tagname`. - """ - words = self.vue_tagname.split('-') - return ''.join([word.capitalize() for word in words]) - @property def component(self): """ @@ -317,34 +333,6 @@ class Grid: DeprecationWarning, stacklevel=2) return self.vue_component - @property - def actions(self): - """ """ - actions = [] - if self.main_actions: - actions.extend(self.main_actions) - if self.more_actions: - actions.extend(self.more_actions) - return actions - - def make_columns(self): - """ - Return a default list of columns, based on :attr:`model_class`. - """ - if not self.model_class: - raise ValueError("Must define model_class to use make_columns()") - - mapper = orm.class_mapper(self.model_class) - return [prop.key for prop in mapper.iterate_properties] - - def remove(self, *keys): - """ - This *removes* some column(s) from the grid, altogether. - """ - for key in keys: - if key in self.columns: - self.columns.remove(key) - def hide_column(self, key): """ This *removes* a column from the grid, altogether. @@ -377,9 +365,6 @@ class Grid: if key in self.invisible: self.invisible.remove(key) - def append(self, field): - self.columns.append(field) - def insert_before(self, field, newfield): self.columns.insert_before(field, newfield) @@ -430,24 +415,22 @@ class Grid: self.filters.pop(key, None) def set_label(self, key, label, column_only=False): - self.labels[key] = label + """ + 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 get_label(self, key): - """ - Returns the label text for given field key. - """ - return self.labels.get(key, prettify(key)) - - def set_link(self, key, link=True): - if link: - if key not in self.linked_columns: - self.linked_columns.append(key) - else: # unlink - if self.linked_columns and key in self.linked_columns: - self.linked_columns.remove(key) - def set_click_handler(self, key, handler): if handler: self.click_handlers[key] = handler @@ -457,9 +440,6 @@ class Grid: def has_click_handler(self, key): return key in self.click_handlers - def set_renderer(self, key, renderer): - self.renderers[key] = renderer - def set_raw_renderer(self, key, renderer): """ Set or remove the "raw" renderer for the given field. @@ -1450,22 +1430,13 @@ class Grid: return render(template, context) def get_view_click_handler(self): - + """ """ # locate the 'view' action # TODO: this should be easier, and/or moved elsewhere? view = None - for action in self.main_actions: + for action in self.actions: if action.key == 'view': - view = action - break - if not view: - for action in self.more_actions: - if action.key == 'view': - view = action - break - - if view: - return view.click_handler + return action.click_handler def set_filters_sequence(self, filters, only=False): """ @@ -1561,26 +1532,21 @@ class Grid: kwargs['form'] = form return render(template, kwargs) - def render_actions(self, row, i): - """ - Returns the rendered contents of the 'actions' column for a given row. - """ - main_actions = [self.render_action(a, row, i) - for a in self.main_actions] - main_actions = [a for a in main_actions if a] - more_actions = [self.render_action(a, row, i) - for a in self.more_actions] - more_actions = [a for a in more_actions if a] - if more_actions: - icon = HTML.tag('span', class_='ui-icon ui-icon-carat-1-e') - link = tags.link_to("More" + icon, '#', class_='more') - main_actions.append(HTML.literal('  ') + link + HTML.tag('div', class_='more', c=more_actions)) - return HTML.literal('').join(main_actions) + def render_actions(self, row, i): # pragma: no cover + """ """ + warnings.warn("grid.render_actions() is deprecated!", + DeprecationWarning, stacklevel=2) + + actions = [self.render_action(a, row, i) + for a in self.actions] + actions = [a for a in actions if a] + return HTML.literal('').join(actions) + + def render_action(self, action, row, i): # pragma: no cover + """ """ + warnings.warn("grid.render_action() is deprecated!", + DeprecationWarning, stacklevel=2) - def render_action(self, action, row, i): - """ - Renders an action menu item (link) for the given row. - """ url = action.get_url(row, i) if url: kwargs = {'class_': action.key, 'target': action.target} @@ -1786,21 +1752,10 @@ class Grid: Pre-generate all action URLs for the given data row. Meant for use with client-side table, since we can't generate URLs from JS. """ - for action in (self.main_actions + self.more_actions): + for action in self.actions: url = action.get_url(rowobj, i) row['_action_url_{}'.format(action.key)] = url - def is_linked(self, name): - """ - Should return ``True`` if the given column name is configured to be - "linked" (i.e. table cell should contain a link to "view object"), - otherwise ``False``. - """ - if self.linked_columns: - if name in self.linked_columns: - return True - return False - class GridAction(WuttaGridAction): """ diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index f4f74a34..5dd7b548 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -186,7 +186,9 @@ class BatchMasterView(MasterView): breakdown = self.make_status_breakdown(batch) factory = self.get_grid_factory() - g = factory('batch_row_status_breakdown', [], + g = factory(self.request, + key='batch_row_status_breakdown', + data=[], columns=['title', 'count']) g.set_click_handler('title', "autoFilterStatus(props.row)") kwargs['status_breakdown_data'] = breakdown @@ -693,7 +695,7 @@ class BatchMasterView(MasterView): batch = self.get_instance() # TODO: most of this logic is copied from MasterView, should refactor/merge somehow... - if 'main_actions' not in kwargs: + if 'actions' not in kwargs: actions = [] # view action @@ -714,7 +716,7 @@ class BatchMasterView(MasterView): actions.append(self.make_action('delete', icon='trash', url=self.row_delete_action_url)) kwargs.setdefault('delete_speedbump', self.rows_deletable_speedbump) - kwargs['main_actions'] = actions + kwargs['actions'] = actions return super().make_row_grid_kwargs(**kwargs) diff --git a/tailbone/views/batch/pos.py b/tailbone/views/batch/pos.py index 11031353..b6fef6c8 100644 --- a/tailbone/views/batch/pos.py +++ b/tailbone/views/batch/pos.py @@ -195,6 +195,7 @@ class POSBatchView(BatchMasterView): factory = self.get_grid_factory() g = factory( + self.request, key=f'{route_prefix}.taxes', data=[], columns=[ diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py index 2958a98a..7e49ccef 100644 --- a/tailbone/views/customers.py +++ b/tailbone/views/customers.py @@ -208,8 +208,7 @@ class CustomerView(MasterView): url = lambda r, i: self.request.route_url( f'{route_prefix}.view', **self.get_action_route_kwargs(r)) # nb. insert to slot 1, just after normal View action - g.main_actions.insert(1, self.make_action( - 'view_raw', url=url, icon='eye')) + g.actions.insert(1, self.make_action('view_raw', url=url, icon='eye')) g.set_link('name') g.set_link('person') @@ -471,7 +470,8 @@ class CustomerView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.people'.format(route_prefix), + self.request, + key=f'{route_prefix}.people', data=[], columns=[ 'shopper_number', @@ -500,7 +500,8 @@ class CustomerView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.people'.format(route_prefix), + self.request, + key=f'{route_prefix}.people', data=[], columns=[ 'full_name', @@ -512,13 +513,13 @@ class CustomerView(MasterView): ) if self.request.has_perm('people.view'): - g.main_actions.append(self.make_action('view', icon='eye')) + g.actions.append(self.make_action('view', icon='eye')) if self.request.has_perm('people.edit'): - g.main_actions.append(self.make_action('edit', icon='edit')) + g.actions.append(self.make_action('edit', icon='edit')) if self.people_detachable and self.has_perm('detach_person'): - g.main_actions.append(self.make_action('detach', icon='minus-circle', - link_class='has-text-warning', - click_handler="$emit('detach-person', props.row._action_url_detach)")) + g.actions.append(self.make_action('detach', icon='minus-circle', + link_class='has-text-warning', + click_handler="$emit('detach-person', props.row._action_url_detach)")) return HTML.literal( g.render_table_element(data_prop='peopleData')) diff --git a/tailbone/views/custorders/items.py b/tailbone/views/custorders/items.py index d8e39f55..e7edf3aa 100644 --- a/tailbone/views/custorders/items.py +++ b/tailbone/views/custorders/items.py @@ -385,6 +385,7 @@ class CustomerOrderItemView(MasterView): factory = self.get_grid_factory() g = factory( + self.request, key=f'{route_prefix}.events', data=[], columns=[ diff --git a/tailbone/views/custorders/orders.py b/tailbone/views/custorders/orders.py index f76d4d93..b1a9831a 100644 --- a/tailbone/views/custorders/orders.py +++ b/tailbone/views/custorders/orders.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. # @@ -29,13 +29,12 @@ import logging from sqlalchemy import orm -from rattail.db import model -from rattail.util import pretty_quantity, simple_error +from rattail.db.model import CustomerOrder, CustomerOrderItem +from rattail.util import simple_error from rattail.batch import get_batch_handler from webhelpers2.html import tags, HTML -from tailbone.db import Session from tailbone.views import MasterView @@ -46,7 +45,7 @@ class CustomerOrderView(MasterView): """ Master view for customer orders """ - model_class = model.CustomerOrder + model_class = CustomerOrder route_prefix = 'custorders' editable = False configurable = True @@ -80,7 +79,7 @@ class CustomerOrderView(MasterView): ] has_rows = True - model_row_class = model.CustomerOrderItem + model_row_class = CustomerOrderItem rows_viewable = False row_labels = { @@ -116,15 +115,17 @@ class CustomerOrderView(MasterView): ] def __init__(self, request): - super(CustomerOrderView, self).__init__(request) + super().__init__(request) self.batch_handler = self.get_batch_handler() def query(self, session): + model = self.app.model return session.query(model.CustomerOrder)\ .options(orm.joinedload(model.CustomerOrder.customer)) def configure_grid(self, g): super().configure_grid(g) + model = self.app.model # id g.set_link('id') @@ -163,7 +164,7 @@ class CustomerOrderView(MasterView): return f"#{order.id} for {order.customer or order.person}" def configure_form(self, f): - super(CustomerOrderView, self).configure_form(f) + super().configure_form(f) order = f.model_instance f.set_readonly('id') @@ -233,6 +234,7 @@ class CustomerOrderView(MasterView): class_='has-background-warning') def get_row_data(self, order): + model = self.app.model return self.Session.query(model.CustomerOrderItem)\ .filter(model.CustomerOrderItem.order == order) @@ -240,11 +242,13 @@ class CustomerOrderView(MasterView): return item.order def make_row_grid_kwargs(self, **kwargs): - kwargs = super(CustomerOrderView, self).make_row_grid_kwargs(**kwargs) + kwargs = super().make_row_grid_kwargs(**kwargs) - assert not kwargs['main_actions'] - kwargs['main_actions'].append( - self.make_action('view', icon='eye', url=self.row_view_action_url)) + actions = kwargs.get('actions', []) + if not actions: + actions.append(self.make_action('view', icon='eye', + url=self.row_view_action_url)) + kwargs['actions'] = actions return kwargs @@ -253,7 +257,7 @@ class CustomerOrderView(MasterView): return self.request.route_url('custorders.items.view', uuid=item.uuid) def configure_row_grid(self, g): - super(CustomerOrderView, self).configure_row_grid(g) + super().configure_row_grid(g) app = self.get_rattail_app() handler = app.get_batch_handler( 'custorder', @@ -423,6 +427,7 @@ class CustomerOrderView(MasterView): if not user: raise RuntimeError("this feature requires a user to be logged in") + model = self.app.model try: # there should be at most *one* new batch per user batch = self.Session.query(model.CustomerOrderBatch)\ @@ -488,6 +493,7 @@ class CustomerOrderView(MasterView): if not uuid: return {'error': "Must specify a customer UUID"} + model = self.app.model customer = self.Session.get(model.Customer, uuid) if not customer: return {'error': "Customer not found"} @@ -508,6 +514,7 @@ class CustomerOrderView(MasterView): return info def assign_contact(self, batch, data): + model = self.app.model kwargs = {} # this will either be a Person or Customer UUID @@ -662,6 +669,7 @@ class CustomerOrderView(MasterView): if not uuid: return {'error': "Must specify a product UUID"} + model = self.app.model product = self.Session.get(model.Product, uuid) if not product: return {'error': "Product not found"} @@ -725,8 +733,7 @@ class CustomerOrderView(MasterView): return app.render_currency(obj.unit_price) def normalize_row(self, row): - app = self.get_rattail_app() - products_handler = app.get_products_handler() + products_handler = self.app.get_products_handler() data = { 'uuid': row.uuid, @@ -742,20 +749,20 @@ class CustomerOrderView(MasterView): 'product_size': row.product_size, 'product_weighed': row.product_weighed, - 'case_quantity': pretty_quantity(row.case_quantity), - 'cases_ordered': pretty_quantity(row.cases_ordered), - 'units_ordered': pretty_quantity(row.units_ordered), - 'order_quantity': pretty_quantity(row.order_quantity), + 'case_quantity': self.app.render_quantity(row.case_quantity), + 'cases_ordered': self.app.render_quantity(row.cases_ordered), + 'units_ordered': self.app.render_quantity(row.units_ordered), + 'order_quantity': self.app.render_quantity(row.order_quantity), 'order_uom': row.order_uom, 'order_uom_choices': self.uom_choices_for_row(row), - 'discount_percent': pretty_quantity(row.discount_percent), + 'discount_percent': self.app.render_quantity(row.discount_percent), 'department_display': row.department_name, 'unit_price': float(row.unit_price) if row.unit_price is not None else None, 'unit_price_display': self.get_unit_price_display(row), 'total_price': float(row.total_price) if row.total_price is not None else None, - 'total_price_display': app.render_currency(row.total_price), + 'total_price_display': self.app.render_currency(row.total_price), 'status_code': row.status_code, 'status_text': row.status_text, @@ -763,15 +770,15 @@ class CustomerOrderView(MasterView): if row.unit_regular_price: data['unit_regular_price'] = float(row.unit_regular_price) - data['unit_regular_price_display'] = app.render_currency(row.unit_regular_price) + data['unit_regular_price_display'] = self.app.render_currency(row.unit_regular_price) if row.unit_sale_price: data['unit_sale_price'] = float(row.unit_sale_price) - data['unit_sale_price_display'] = app.render_currency(row.unit_sale_price) + data['unit_sale_price_display'] = self.app.render_currency(row.unit_sale_price) if row.sale_ends: - sale_ends = app.localtime(row.sale_ends, from_utc=True).date() + sale_ends = self.app.localtime(row.sale_ends, from_utc=True).date() data['sale_ends'] = str(sale_ends) - data['sale_ends_display'] = app.render_date(sale_ends) + data['sale_ends_display'] = self.app.render_date(sale_ends) if row.unit_sale_price and row.unit_price == row.unit_sale_price: data['pricing_reflects_sale'] = True @@ -808,12 +815,12 @@ class CustomerOrderView(MasterView): case_price = self.batch_handler.get_case_price_for_row(row) data['case_price'] = float(case_price) if case_price is not None else None - data['case_price_display'] = app.render_currency(case_price) + data['case_price_display'] = self.app.render_currency(case_price) if self.batch_handler.product_price_may_be_questionable(): data['price_needs_confirmation'] = row.price_needs_confirmation - key = app.get_product_key_field() + key = self.app.get_product_key_field() if key == 'upc': data['product_key'] = data['product_upc_pretty'] elif key == 'item_id': @@ -837,7 +844,7 @@ class CustomerOrderView(MasterView): case_qty = unit_qty = '??' else: case_qty = data['case_quantity'] - unit_qty = pretty_quantity(row.order_quantity * row.case_quantity) + unit_qty = self.app.render_quantity(row.order_quantity * row.case_quantity) data.update({ 'order_quantity_display': "{} {} (× {} {} = {} {})".format( data['order_quantity'], @@ -850,14 +857,14 @@ class CustomerOrderView(MasterView): else: data.update({ 'order_quantity_display': "{} {}".format( - pretty_quantity(row.order_quantity), + self.app.render_quantity(row.order_quantity), self.enum.UNIT_OF_MEASURE[unit_uom]), }) return data def add_item(self, batch, data): - app = self.get_rattail_app() + model = self.app.model order_quantity = decimal.Decimal(data.get('order_quantity') or '0') order_uom = data.get('order_uom') @@ -888,7 +895,7 @@ class CustomerOrderView(MasterView): pending_info = dict(data['pending_product']) if 'upc' in pending_info: - pending_info['upc'] = app.make_gpc(pending_info['upc']) + pending_info['upc'] = self.app.make_gpc(pending_info['upc']) for field in ('unit_cost', 'regular_price_amount', 'case_size'): if field in pending_info: @@ -917,6 +924,7 @@ class CustomerOrderView(MasterView): if not uuid: return {'error': "Must specify a row UUID"} + model = self.app.model row = self.Session.get(model.CustomerOrderBatchRow, uuid) if not row: return {'error': "Row not found"} @@ -975,6 +983,7 @@ class CustomerOrderView(MasterView): if not uuid: return {'error': "Must specify a row UUID"} + model = self.app.model row = self.Session.get(model.CustomerOrderBatchRow, uuid) if not row: return {'error': "Row not found"} diff --git a/tailbone/views/departments.py b/tailbone/views/departments.py index 6ee1439f..47de8dca 100644 --- a/tailbone/views/departments.py +++ b/tailbone/views/departments.py @@ -128,8 +128,8 @@ class DepartmentView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.employees'.format(route_prefix), - request=self.request, + self.request, + key=f'{route_prefix}.employees', data=[], columns=[ 'first_name', @@ -140,9 +140,9 @@ class DepartmentView(MasterView): ) if self.request.has_perm('employees.view'): - g.main_actions.append(self.make_action('view', icon='eye')) + g.actions.append(self.make_action('view', icon='eye')) if self.request.has_perm('employees.edit'): - g.main_actions.append(self.make_action('edit', icon='edit')) + g.actions.append(self.make_action('edit', icon='edit')) return HTML.literal( g.render_table_element(data_prop='employeesData')) diff --git a/tailbone/views/email.py b/tailbone/views/email.py index 4014c05e..a99e8553 100644 --- a/tailbone/views/email.py +++ b/tailbone/views/email.py @@ -141,7 +141,7 @@ class EmailSettingView(MasterView): # toggle hidden if self.has_perm('configure'): - g.main_actions.append( + g.actions.append( self.make_action('toggle_hidden', url='#', icon='ban', click_handler='toggleHidden(props.row)', factory=ToggleHidden)) diff --git a/tailbone/views/employees.py b/tailbone/views/employees.py index f4f99058..debd8fcb 100644 --- a/tailbone/views/employees.py +++ b/tailbone/views/employees.py @@ -167,8 +167,7 @@ class EmployeeView(MasterView): url = lambda r, i: self.request.route_url( f'{route_prefix}.view', **self.get_action_route_kwargs(r)) # nb. insert to slot 1, just after normal View action - g.main_actions.insert(1, self.make_action( - 'view_raw', url=url, icon='eye')) + g.actions.insert(1, self.make_action('view_raw', url=url, icon='eye')) def default_view_url(self): if (self.request.has_perm('people.view_profile') diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 097cb229..8f65fc88 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -392,9 +392,8 @@ class MasterView(View): if columns is None: columns = self.get_grid_columns() - kwargs.setdefault('request', self.request) kwargs = self.make_grid_kwargs(**kwargs) - grid = factory(key, data, columns, **kwargs) + grid = factory(self.request, key=key, data=data, columns=columns, **kwargs) self.configure_grid(grid) grid.load_settings() return grid @@ -454,10 +453,26 @@ class MasterView(View): if self.sortable or self.pageable or self.filterable: defaults['expose_direct_link'] = True - if 'main_actions' not in kwargs and 'more_actions' not in kwargs: - main, more = self.get_grid_actions() - defaults['main_actions'] = main - defaults['more_actions'] = more + if 'actions' not in kwargs: + + if 'main_actions' in kwargs: + warnings.warn("main_actions param is deprecated for make_grid_kwargs(); " + "please use actions param instead", + DeprecationWarning, stacklevel=2) + main = kwargs.pop('main_actions') + else: + main = self.get_main_actions() + + if 'more_actions' in kwargs: + warnings.warn("more_actions param is deprecated for make_grid_kwargs(); " + "please use actions param instead", + DeprecationWarning, stacklevel=2) + more = kwargs.pop('more_actions') + else: + more = self.get_more_actions() + + defaults['actions'] = main + more + defaults.update(kwargs) return defaults @@ -548,9 +563,8 @@ class MasterView(View): if columns is None: columns = self.get_row_grid_columns() - kwargs.setdefault('request', self.request) kwargs = self.make_row_grid_kwargs(**kwargs) - grid = factory(key, data, columns, **kwargs) + grid = factory(self.request, key=key, data=data, columns=columns, **kwargs) self.configure_row_grid(grid) grid.load_settings() return grid @@ -577,7 +591,7 @@ class MasterView(View): if self.rows_default_pagesize: defaults['default_pagesize'] = self.rows_default_pagesize - if self.has_rows and 'main_actions' not in defaults: + if self.has_rows and 'actions' not in defaults: actions = [] # view action @@ -595,7 +609,7 @@ class MasterView(View): actions.append(self.make_action('delete', icon='trash', url=self.row_delete_action_url)) defaults['delete_speedbump'] = self.rows_deletable_speedbump - defaults['main_actions'] = actions + defaults['actions'] = actions defaults.update(kwargs) return defaults @@ -630,9 +644,8 @@ class MasterView(View): if columns is None: columns = self.get_version_grid_columns() - kwargs.setdefault('request', self.request) kwargs = self.make_version_grid_kwargs(**kwargs) - grid = factory(key, data, columns, **kwargs) + grid = factory(self.request, key=key, data=data, columns=columns, **kwargs) self.configure_version_grid(grid) grid.load_settings() return grid @@ -661,9 +674,9 @@ class MasterView(View): 'pageable': True, 'url': lambda txn: self.request.route_url(route, uuid=instance.uuid, txnid=txn.id), } - if 'main_actions' not in kwargs: + if 'actions' not in kwargs: url = lambda txn, i: self.request.route_url(route, uuid=instance.uuid, txnid=txn.id) - defaults['main_actions'] = [ + defaults['actions'] = [ self.make_action('view', icon='eye', url=url), ] defaults.update(kwargs) @@ -1372,7 +1385,7 @@ class MasterView(View): 'sortable': True, 'default_sortkey': 'changed', 'default_sortdir': 'desc', - 'main_actions': [ + 'actions': [ self.make_action('view', icon='eye', url='#', click_handler='viewRevision(props.row)'), self.make_action('view_separate', url=row_url, target='_blank', @@ -3111,6 +3124,11 @@ class MasterView(View): return key def get_grid_actions(self): + """ """ + warnings.warn("get_grid_actions() method is deprecated; " + "please use get_main_actions() or get_more_actions() instead", + DeprecationWarning, stacklevel=2) + main, more = self.get_main_actions(), self.get_more_actions() if len(more) == 1: main, more = main + more, [] diff --git a/tailbone/views/members.py b/tailbone/views/members.py index de844eb7..46ed7e4b 100644 --- a/tailbone/views/members.py +++ b/tailbone/views/members.py @@ -229,8 +229,7 @@ class MemberView(MasterView): url = lambda r, i: self.request.route_url( f'{route_prefix}.view', **self.get_action_route_kwargs(r)) # nb. insert to slot 1, just after normal View action - g.main_actions.insert(1, self.make_action( - 'view_raw', url=url, icon='eye')) + g.actions.insert(1, self.make_action('view_raw', url=url, icon='eye')) # equity_total # TODO: should make this configurable diff --git a/tailbone/views/people.py b/tailbone/views/people.py index 163a9a52..020babc5 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -175,8 +175,7 @@ class PersonView(MasterView): url = lambda r, i: self.request.route_url( f'{route_prefix}.view', **self.get_action_route_kwargs(r)) # nb. insert to slot 1, just after normal View action - g.main_actions.insert(1, self.make_action( - 'view_raw', url=url, icon='eye')) + g.actions.insert(1, self.make_action('view_raw', url=url, icon='eye')) g.set_link('display_name') g.set_link('first_name') @@ -522,9 +521,9 @@ class PersonView(MasterView): data = self.profile_transactions_query(person) factory = self.get_grid_factory() g = factory( - f'{route_prefix}.profile.transactions.{person.uuid}', - data, - request=self.request, + self.request, + key=f'{route_prefix}.profile.transactions.{person.uuid}', + data=data, model_class=model.Transaction, ajax_data_url=self.get_action_url('view_profile_transactions', person), columns=[ @@ -552,7 +551,7 @@ class PersonView(MasterView): if self.request.has_perm('trainwreck.transactions.view'): url = lambda row, i: self.request.route_url('trainwreck.transactions.view', uuid=row.uuid) - g.main_actions.append(self.make_action('view', icon='eye', url=url)) + g.actions.append(self.make_action('view', icon='eye', url=url)) g.load_settings() g.set_enum('system', self.enum.TRAINWRECK_SYSTEM) @@ -1413,9 +1412,9 @@ class PersonView(MasterView): route_prefix = self.get_route_prefix() factory = self.get_grid_factory() g = factory( - '{}.profile.revisions'.format(route_prefix), - [], # start with empty data! - request=self.request, + self.request, + key=f'{route_prefix}.profile.revisions', + data=[], # start with empty data! columns=[ 'changed', 'changed_by', @@ -1430,7 +1429,7 @@ class PersonView(MasterView): 'changed_by', 'comment', ], - main_actions=[ + actions=[ self.make_action('view', icon='eye', url='#', click_handler='viewRevision(props.row)'), ], diff --git a/tailbone/views/poser/reports.py b/tailbone/views/poser/reports.py index 462df51d..ded80b18 100644 --- a/tailbone/views/poser/reports.py +++ b/tailbone/views/poser/reports.py @@ -110,7 +110,7 @@ class PoserReportView(PoserMasterView): g.set_searchable('description') if self.request.has_perm('report_output.create'): - g.more_actions.append(self.make_action( + g.actions.append(self.make_action( 'generate', icon='arrow-circle-right', url=self.get_generate_url)) diff --git a/tailbone/views/principal.py b/tailbone/views/principal.py index bb799efc..3986f8b0 100644 --- a/tailbone/views/principal.py +++ b/tailbone/views/principal.py @@ -124,11 +124,11 @@ class PrincipalMasterView(MasterView): def find_by_perm_make_results_grid(self, principals): route_prefix = self.get_route_prefix() factory = self.get_grid_factory() - g = factory(key=f'{route_prefix}.results', - request=self.request, + g = factory(self.request, + key=f'{route_prefix}.results', data=[], columns=[], - main_actions=[ + actions=[ self.make_action('view', icon='eye', click_handler='navigateTo(props.row._url)'), ]) diff --git a/tailbone/views/products.py b/tailbone/views/products.py index bf2d7f14..c546a0f4 100644 --- a/tailbone/views/products.py +++ b/tailbone/views/products.py @@ -384,7 +384,7 @@ class ProductView(MasterView): g.set_filter('report_code_name', model.ReportCode.name) if self.expose_label_printing and self.has_perm('print_labels'): - g.more_actions.append(self.make_action( + g.actions.append(self.make_action( 'print_label', icon='print', url='#', click_handler='quickLabelPrint(props.row)')) @@ -1197,8 +1197,9 @@ class ProductView(MasterView): # regular price data = [] # defer fetching until user asks for it - grid = grids.Grid('products.regular_price_history', data, - request=self.request, + grid = grids.Grid(self.request, + key='products.regular_price_history', + data=data, columns=[ 'price', 'since', @@ -1211,8 +1212,9 @@ class ProductView(MasterView): # current price data = [] # defer fetching until user asks for it - grid = grids.Grid('products.current_price_history', data, - request=self.request, + grid = grids.Grid(self.request, + key='products.current_price_history', + data=data, columns=[ 'price', 'price_type', @@ -1229,8 +1231,9 @@ class ProductView(MasterView): # suggested price data = [] # defer fetching until user asks for it - grid = grids.Grid('products.suggested_price_history', data, - request=self.request, + grid = grids.Grid(self.request, + key='products.suggested_price_history', + data=data, columns=[ 'price', 'since', @@ -1243,8 +1246,9 @@ class ProductView(MasterView): # cost history data = [] # defer fetching until user asks for it - grid = grids.Grid('products.cost_history', data, - request=self.request, + grid = grids.Grid(self.request, + key='products.cost_history', + data=data, columns=[ 'cost', 'vendor', @@ -1335,7 +1339,8 @@ class ProductView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.vendor_sources'.format(route_prefix), + self.request, + key=f'{route_prefix}.vendor_sources', data=[], columns=columns, labels={ @@ -1376,7 +1381,8 @@ class ProductView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.lookup_codes'.format(route_prefix), + self.request, + key=f'{route_prefix}.lookup_codes', data=[], columns=[ 'sequence', diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py index 1d11130c..590b9af5 100644 --- a/tailbone/views/purchasing/batch.py +++ b/tailbone/views/purchasing/batch.py @@ -793,8 +793,8 @@ class PurchasingBatchView(BatchMasterView): factory = self.get_grid_factory() g = factory( - key='{}.row_credits'.format(route_prefix), - request=self.request, + self.request, + key=f'{route_prefix}.row_credits', data=[], columns=[ 'credit_type', diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 0a305f0a..de19a2b9 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -774,8 +774,10 @@ class ReceivingBatchView(PurchasingBatchView): breakdown = self.make_po_vs_invoice_breakdown(batch) factory = self.get_grid_factory() - g = factory('batch_po_vs_invoice_breakdown', [], - columns=['title', 'count']) + g = factory(self.request, + key='batch_po_vs_invoice_breakdown', + data=[], + columns=['title', 'count']) g.set_click_handler('title', "autoFilterPoVsInvoice(props.row)") kwargs['po_vs_invoice_breakdown_data'] = breakdown kwargs['po_vs_invoice_breakdown_grid'] = HTML.literal( @@ -1035,10 +1037,12 @@ class ReceivingBatchView(PurchasingBatchView): icon='shuffle', label="Transform to Unit", url=self.transform_unit_url) - g.more_actions.append(transform) - if g.main_actions and g.main_actions[-1].key == 'delete': - delete = g.main_actions.pop() - g.more_actions.append(delete) + if g.actions and g.actions[-1].key == 'delete': + delete = g.actions.pop() + g.actions.append(transform) + g.actions.append(delete) + else: + g.actions.append(transform) # truck_dump_status if not batch.is_truck_dump_parent(): @@ -1111,7 +1115,7 @@ class ReceivingBatchView(PurchasingBatchView): and self.row_editable(row)): # add the Un-Declare action - g.main_actions.append(self.make_action( + g.actions.append(self.make_action( 'remove', label="Un-Declare", url='#', icon='trash', link_class='has-text-danger', diff --git a/tailbone/views/reports.py b/tailbone/views/reports.py index aedda61c..099224be 100644 --- a/tailbone/views/reports.py +++ b/tailbone/views/reports.py @@ -308,7 +308,8 @@ class ReportOutputView(ExportMasterView): route_prefix = self.get_route_prefix() factory = self.get_grid_factory() g = factory( - key='{}.params'.format(route_prefix), + self.request, + key=f'{route_prefix}.params', data=params, columns=['key', 'value'], labels={'key': "Name"}, @@ -705,9 +706,12 @@ class ProblemReportView(MasterView): return ', '.join(recips) def render_days(self, report_info, field): - g = self.get_grid_factory()('days', [], - columns=['weekday_name', 'enabled'], - labels={'weekday_name': "Weekday"}) + factory = self.get_grid_factory() + g = factory(self.request, + key='days', + data=[], + columns=['weekday_name', 'enabled'], + labels={'weekday_name': "Weekday"}) return HTML.literal(g.render_table_element(data_prop='weekdaysData')) def template_kwargs_view(self, **kwargs): diff --git a/tailbone/views/roles.py b/tailbone/views/roles.py index fb834479..e8a6d8a2 100644 --- a/tailbone/views/roles.py +++ b/tailbone/views/roles.py @@ -255,8 +255,8 @@ class RoleView(PrincipalMasterView): permission_prefix = self.get_permission_prefix() factory = self.get_grid_factory() g = factory( - key='{}.users'.format(route_prefix), - request=self.request, + self.request, + key=f'{route_prefix}.users', data=[], columns=[ 'full_name', @@ -269,9 +269,9 @@ class RoleView(PrincipalMasterView): ) if self.request.has_perm('users.view'): - g.main_actions.append(self.make_action('view', icon='eye')) + g.actions.append(self.make_action('view', icon='eye')) if self.request.has_perm('users.edit'): - g.main_actions.append(self.make_action('edit', icon='edit')) + g.actions.append(self.make_action('edit', icon='edit')) return HTML.literal( g.render_table_element(data_prop='usersData')) @@ -366,10 +366,11 @@ class RoleView(PrincipalMasterView): self.make_action('view', icon='zoomin', url=lambda r, i: self.request.route_url('users.view', uuid=r.uuid)) ] - kwargs['users'] = grids.Grid(None, users, ['username', 'active'], - request=self.request, + kwargs['users'] = grids.Grid(self.request, + data=users, + columns=['username', 'active'], model_class=model.User, - main_actions=actions) + actions=actions) else: kwargs['users'] = None diff --git a/tailbone/views/tempmon/core.py b/tailbone/views/tempmon/core.py index d551d6e6..7540abbe 100644 --- a/tailbone/views/tempmon/core.py +++ b/tailbone/views/tempmon/core.py @@ -77,8 +77,8 @@ class MasterView(views.MasterView): factory = self.get_grid_factory() g = factory( - key='{}.probes'.format(route_prefix), - request=self.request, + self.request, + key=f'{route_prefix}.probes', data=[], columns=[ 'description', @@ -96,7 +96,7 @@ class MasterView(views.MasterView): 'critical_temp_max': "Crit. Max", }, linked_columns=['description'], - main_actions=actions, + actions=actions, ) return HTML.literal( g.render_table_element(data_prop='probesData')) diff --git a/tailbone/views/trainwreck/base.py b/tailbone/views/trainwreck/base.py index 9c150c6a..d5f077aa 100644 --- a/tailbone/views/trainwreck/base.py +++ b/tailbone/views/trainwreck/base.py @@ -246,10 +246,10 @@ class TransactionView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.custorder_xref_markers'.format(route_prefix), + self.request, + key=f'{route_prefix}.custorder_xref_markers', data=[], - columns=['custorder_xref', 'custorder_item_xref'], - request=self.request) + columns=['custorder_xref', 'custorder_item_xref']) return HTML.literal( g.render_table_element(data_prop='custorderXrefMarkersData')) @@ -355,11 +355,11 @@ class TransactionView(MasterView): factory = self.get_grid_factory() g = factory( - key='{}.discounts'.format(route_prefix), + self.request, + key=f'{route_prefix}.discounts', data=[], columns=['discount_type', 'description', 'amount'], - labels={'discount_type': "Type"}, - request=self.request) + labels={'discount_type': "Type"}) return HTML.literal( g.render_table_element(data_prop='discountsData')) diff --git a/tailbone/views/users.py b/tailbone/views/users.py index 9eae74d8..9b533efe 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -44,9 +44,6 @@ class UserView(PrincipalMasterView): Master view for the User model. """ model_class = User - has_rows = True - rows_title = "User Events" - model_row_class = UserEvent has_versions = True touchable = True mergeable = True @@ -77,6 +74,11 @@ class UserView(PrincipalMasterView): 'permissions', ] + has_rows = True + model_row_class = UserEvent + rows_title = "User Events" + rows_viewable = False + row_grid_columns = [ 'type_code', 'occurred', @@ -297,11 +299,11 @@ class UserView(PrincipalMasterView): factory = self.get_grid_factory() g = factory( - request=self.request, - key='{}.api_tokens'.format(route_prefix), + self.request, + key=f'{route_prefix}.api_tokens', data=[], columns=['description', 'created'], - main_actions=[ + actions=[ self.make_action('delete', icon='trash', click_handler="$emit('api-token-delete', props.row)")]) @@ -514,7 +516,6 @@ class UserView(PrincipalMasterView): g.set_sort_defaults('occurred', 'desc') g.set_enum('type_code', self.enum.USER_EVENT) g.set_label('type_code', "Event Type") - g.main_actions = [] def get_version_child_classes(self): model = self.model diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index 0a8d5d66..0d0fe112 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -12,9 +12,8 @@ class TestGrid(WebTestCase): self.setup_web() self.config.setdefault('rattail.web.menus.handler_spec', 'tests.util:NullMenuHandler') - def make_grid(self, key, data=[], **kwargs): - kwargs.setdefault('request', self.request) - return mod.Grid(key, data=data, **kwargs) + def make_grid(self, key=None, data=[], **kwargs): + return mod.Grid(self.request, key=key, data=data, **kwargs) def test_basic(self): grid = self.make_grid('foo') @@ -90,6 +89,50 @@ class TestGrid(WebTestCase): grid = self.make_grid('foo', main_actions=['foo'], more_actions=['bar']) self.assertEqual(grid.actions, ['foo', 'bar']) + def test_set_label(self): + model = self.app.model + grid = self.make_grid(model_class=model.Setting) + self.assertEqual(grid.labels, {}) + + # basic + grid.set_label('name', "NAME COL") + self.assertEqual(grid.labels['name'], "NAME COL") + + # can replace label + grid.set_label('name', "Different") + self.assertEqual(grid.labels['name'], "Different") + self.assertEqual(grid.get_label('name'), "Different") + + # can update only column, not filter + self.assertEqual(grid.labels, {'name': "Different"}) + self.assertIn('name', grid.filters) + self.assertEqual(grid.filters['name'].label, "Different") + grid.set_label('name', "COLUMN ONLY", column_only=True) + self.assertEqual(grid.get_label('name'), "COLUMN ONLY") + self.assertEqual(grid.filters['name'].label, "Different") + + def test_get_view_click_handler(self): + model = self.app.model + grid = self.make_grid(model_class=model.Setting) + + grid.actions.append( + mod.GridAction(self.request, 'view', + click_handler='clickHandler(props.row)')) + + handler = grid.get_view_click_handler() + self.assertEqual(handler, 'clickHandler(props.row)') + + def test_set_action_urls(self): + model = self.app.model + grid = self.make_grid(model_class=model.Setting) + + grid.actions.append( + mod.GridAction(self.request, 'view', url='/blarg')) + + setting = {'name': 'foo', 'value': 'bar'} + grid.set_action_urls(setting, setting, 0) + self.assertEqual(setting['_action_url_view'], '/blarg') + def test_render_vue_tag(self): model = self.app.model From 9da2a148c65ebde63b39903028bdf77577d53780 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 18:45:04 -0500 Subject: [PATCH 045/142] feat: move "basic" grid pagination logic to wuttaweb so far only "simple" pagination is supported by wuttaweb, so basically the main feature flag, page size, current page. in this scenario *all* data is written to client-side JSON and Buefy handles the actual pagination. backend pagination coming soon for wuttaweb but for now tailbone still handles all that. --- tailbone/grids/core.py | 130 +++++++++++++++++-------- tailbone/templates/grids/complete.mako | 18 ++-- tailbone/views/master.py | 4 +- tailbone/views/wutta/people.py | 4 + tests/grids/test_core.py | 86 ++++++++++++++++ 5 files changed, 195 insertions(+), 47 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index a5617215..0b23fb78 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -31,6 +31,7 @@ import logging import sqlalchemy as sa from sqlalchemy import orm +from wuttjamaican.util import UNSPECIFIED from rattail.db.types import GPCType from rattail.util import prettify, pretty_boolean @@ -209,9 +210,6 @@ class Grid(WuttaGrid): sorters={}, default_sortkey=None, default_sortdir='asc', - pageable=False, - default_pagesize=None, - default_page=1, checkboxes=False, checked=None, check_handler=None, @@ -233,7 +231,26 @@ class Grid(WuttaGrid): DeprecationWarning, stacklevel=2) kwargs.setdefault('vue_tagname', kwargs.pop('component')) - # TODO: pretty sure this should go away? + if kwargs.get('pageable'): + warnings.warn("component 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'): + 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'): + warnings.warn("default_page param is deprecated for Grid(); " + "please use page param instead", + DeprecationWarning, stacklevel=2) + kwargs.setdefault('page', kwargs.pop('default_page')) + + # TODO: this should not be needed once all templates correctly + # reference grid.vue_component etc. kwargs.setdefault('vue_tagname', 'tailbone-grid') kwargs['key'] = key @@ -272,10 +289,6 @@ class Grid(WuttaGrid): self.default_sortkey = default_sortkey self.default_sortdir = default_sortdir - self.pageable = pageable - self.default_pagesize = default_pagesize - self.default_page = default_page - self.checkboxes = checkboxes self.checked = checked if self.checked is None: @@ -333,6 +346,16 @@ class Grid(WuttaGrid): DeprecationWarning, stacklevel=2) return self.vue_component + def get_pageable(self): + """ """ + return self.paginated + + def set_pageable(self, value): + """ """ + self.paginated = value + + pageable = property(get_pageable, set_pageable) + def hide_column(self, key): """ This *removes* a column from the grid, altogether. @@ -756,18 +779,61 @@ class Grid(WuttaGrid): keyfunc = lambda v: v[key] return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc') - def get_default_pagesize(self): + def get_pagesize_options(self, default=None): + """ """ + # let upstream check config + options = super().get_pagesize_options(default=UNSPECIFIED) + if options is not UNSPECIFIED: + return options + + # fallback to legacy config + options = self.config.get_list('tailbone.grid.pagesize_options') + if options: + warnings.warn("tailbone.grid.pagesize_options setting is deprecated; " + "please set wuttaweb.grids.default_pagesize_options instead", + DeprecationWarning) + options = [int(size) for size in options + if size.isdigit()] + if options: + return options + + if default: + return default + + # use upstream default + return super().get_pagesize_options() + + def get_pagesize(self, default=None): + """ """ + # let upstream check config + pagesize = super().get_pagesize(default=UNSPECIFIED) + if pagesize is not UNSPECIFIED: + return pagesize + + # fallback to legacy config + pagesize = self.config.get_int('tailbone.grid.default_pagesize') + if pagesize: + warnings.warn("tailbone.grid.default_pagesize setting is deprecated; " + "please use wuttaweb.grids.default_pagesize instead", + DeprecationWarning) + return pagesize + + if default: + return default + + # use upstream default + return super().get_pagesize() + + def get_default_pagesize(self): # pragma: no cover + """ """ + warnings.warn("Grid.get_default_pagesize() method is deprecated; " + "please use Grid.get_pagesize() of Grid.page instead", + DeprecationWarning, stacklevel=2) + if self.default_pagesize: return self.default_pagesize - pagesize = self.request.rattail_config.getint('tailbone', - 'grid.default_pagesize', - default=0) - if pagesize: - return pagesize - - options = self.get_pagesize_options() - return options[0] + return self.get_pagesize() def load_settings(self, store=True): """ @@ -789,9 +855,9 @@ class Grid(WuttaGrid): settings['sorters.1.dir'] = self.default_sortdir else: settings['sorters.length'] = 0 - if self.pageable: - settings['pagesize'] = self.get_default_pagesize() - settings['page'] = self.default_page + if self.paginated: + settings['pagesize'] = self.pagesize + settings['page'] = self.page if self.filterable: for filtr in self.iter_filters(): settings['filter.{}.active'.format(filtr.key)] = filtr.default_active @@ -867,7 +933,7 @@ class Grid(WuttaGrid): 'field': settings[f'sorters.{i}.key'], 'order': settings[f'sorters.{i}.dir'], }) - if self.pageable: + if self.paginated: self.pagesize = settings['pagesize'] self.page = settings['page'] @@ -971,7 +1037,7 @@ class Grid(WuttaGrid): merge(f'sorters.{i}.key') merge(f'sorters.{i}.dir') - if self.pageable: + if self.paginated: merge('pagesize', int) merge('page', int) @@ -1154,7 +1220,7 @@ class Grid(WuttaGrid): :param settings: Dictionary of initial settings, which is to be updated. """ - if not self.pageable: + if not self.paginated: return pagesize = self.request.GET.get('pagesize') @@ -1231,7 +1297,7 @@ class Grid(WuttaGrid): persist(f'sorters.{i}.key') persist(f'sorters.{i}.dir') - if self.pageable: + if self.paginated: persist('pagesize') persist('page') @@ -1355,7 +1421,7 @@ class Grid(WuttaGrid): data = self.filter_data(data) if self.sortable: data = self.sort_data(data) - if self.pageable: + if self.paginated: self.pager = self.paginate_data(data) data = self.pager return data @@ -1580,18 +1646,6 @@ class Grid(WuttaGrid): return tags.checkbox('checkbox-{}-{}'.format(self.key, self.get_row_key(item)), checked=self.checked(item)) - def get_pagesize_options(self): - - # use values from config, if defined - options = self.request.rattail_config.getlist('tailbone', 'grid.pagesize_options') - if options: - options = [int(size) for size in options - if size.isdigit()] - if options: - return options - - return [5, 10, 20, 50, 100, 200] - def has_static_data(self): """ Should return ``True`` if the grid data can be considered "static" @@ -1734,7 +1788,7 @@ class Grid(WuttaGrid): results['checked_rows_code'] = '[{}]'.format( ', '.join(['{}[{}]'.format(var, i) for i in checked])) - if self.pageable and self.pager is not None: + if self.paginated and self.pager is not None: results['total_items'] = self.pager.item_count results['per_page'] = self.pager.items_per_page results['page'] = self.pager.page diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index 93bb6c26..53043803 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -107,12 +107,14 @@ @cellclick="cellClick" % endif + % if grid.paginated: :paginated="paginated" :per-page="perPage" :current-page="currentPage" backend-pagination :total="total" @page-change="onPageChange" + % endif ## TODO: should let grid (or master view) decide how to set these? icon-pack="fas" @@ -203,7 +205,7 @@
% endif - % if getattr(grid, 'pageable', False): + % if grid.paginated:
@@ -255,12 +257,14 @@ checkedRows: ${grid_data['checked_rows_code']|n}, % endif - paginated: ${json.dumps(getattr(grid, 'pageable', False))|n}, + % if grid.paginated: + paginated: ${json.dumps(grid.paginated)|n}, total: ${len(grid_data['data']) if static_data else (grid_data['total_items'] if grid_data is not Undefined else 0)}, - perPage: ${json.dumps(grid.pagesize if getattr(grid, 'pageable', False) else None)|n}, - currentPage: ${json.dumps(grid.page if getattr(grid, 'pageable', False) else None)|n}, - firstItem: ${json.dumps(grid_data['first_item'] if getattr(grid, 'pageable', False) else None)|n}, - lastItem: ${json.dumps(grid_data['last_item'] if getattr(grid, 'pageable', False) else None)|n}, + perPage: ${json.dumps(grid.pagesize if grid.paginated else None)|n}, + currentPage: ${json.dumps(grid.page if grid.paginated else None)|n}, + firstItem: ${json.dumps(grid_data['first_item'] if grid.paginated else None)|n}, + lastItem: ${json.dumps(grid_data['last_item'] if grid.paginated else None)|n}, + % endif % if getattr(grid, 'sortable', False): @@ -439,7 +443,7 @@ params['sort'+i+'dir'] = this.backendSorters[i-1].order } % endif - % if getattr(grid, 'pageable', False): + % if grid.paginated: params.pagesize = this.perPage params.page = this.currentPage % endif diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 8f65fc88..58b93568 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -439,7 +439,7 @@ class MasterView(View): 'filterable': self.filterable, 'use_byte_string_filters': self.use_byte_string_filters, 'sortable': self.sortable, - 'pageable': self.pageable, + 'paginated': self.pageable, 'extra_row_class': self.grid_extra_class, 'url': lambda obj: self.get_action_url('view', obj), 'checkboxes': checkboxes, @@ -589,7 +589,7 @@ class MasterView(View): } if self.rows_default_pagesize: - defaults['default_pagesize'] = self.rows_default_pagesize + defaults['pagesize'] = self.rows_default_pagesize if self.has_rows and 'actions' not in defaults: actions = [] diff --git a/tailbone/views/wutta/people.py b/tailbone/views/wutta/people.py index c92e34ae..3158b478 100644 --- a/tailbone/views/wutta/people.py +++ b/tailbone/views/wutta/people.py @@ -45,6 +45,10 @@ class PersonView(wutta.PersonView): model_class = Person Session = Session + # TODO: /grids/complete.mako is too aggressive for the + # limited support we have in wuttaweb thus far + paginated = False + labels = { 'display_name': "Full Name", } diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index 0d0fe112..7cba917a 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -19,6 +19,32 @@ class TestGrid(WebTestCase): grid = self.make_grid('foo') self.assertIsInstance(grid, mod.Grid) + def test_deprecated_params(self): + + # component + grid = self.make_grid() + self.assertEqual(grid.vue_tagname, 'tailbone-grid') + grid = self.make_grid(component='blarg') + self.assertEqual(grid.vue_tagname, 'blarg') + + # pageable + grid = self.make_grid() + self.assertFalse(grid.paginated) + grid = self.make_grid(pageable=True) + self.assertTrue(grid.paginated) + + # default_pagesize + grid = self.make_grid() + self.assertEqual(grid.pagesize, 20) + grid = self.make_grid(default_pagesize=15) + self.assertEqual(grid.pagesize, 15) + + # default_page + grid = self.make_grid() + self.assertEqual(grid.page, 1) + grid = self.make_grid(default_page=42) + self.assertEqual(grid.page, 42) + def test_vue_tagname(self): # default @@ -133,6 +159,66 @@ class TestGrid(WebTestCase): grid.set_action_urls(setting, setting, 0) self.assertEqual(setting['_action_url_view'], '/blarg') + def test_pageable(self): + grid = self.make_grid() + self.assertFalse(grid.paginated) + grid.pageable = True + self.assertTrue(grid.paginated) + grid.paginated = False + self.assertFalse(grid.pageable) + + def test_get_pagesize_options(self): + grid = self.make_grid() + + # default + options = grid.get_pagesize_options() + self.assertEqual(options, [5, 10, 20, 50, 100, 200]) + + # override default + options = grid.get_pagesize_options(default=[42]) + self.assertEqual(options, [42]) + + # from legacy config + self.config.setdefault('tailbone.grid.pagesize_options', '1 2 3') + grid = self.make_grid() + options = grid.get_pagesize_options() + self.assertEqual(options, [1, 2, 3]) + + # from new config + self.config.setdefault('wuttaweb.grids.default_pagesize_options', '4, 5, 6') + grid = self.make_grid() + options = grid.get_pagesize_options() + self.assertEqual(options, [4, 5, 6]) + + def test_get_pagesize(self): + grid = self.make_grid() + + # default + size = grid.get_pagesize() + self.assertEqual(size, 20) + + # override default + size = grid.get_pagesize(default=42) + self.assertEqual(size, 42) + + # override default options + self.config.setdefault('wuttaweb.grids.default_pagesize_options', '10 15 30') + grid = self.make_grid() + size = grid.get_pagesize() + self.assertEqual(size, 10) + + # from legacy config + self.config.setdefault('tailbone.grid.default_pagesize', '12') + grid = self.make_grid() + size = grid.get_pagesize() + self.assertEqual(size, 12) + + # from new config + self.config.setdefault('wuttaweb.grids.default_pagesize', '15') + grid = self.make_grid() + size = grid.get_pagesize() + self.assertEqual(size, 15) + def test_render_vue_tag(self): model = self.app.model From f4c8176d8325f052e4aa46666b6ae9d5a5779e75 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 16 Aug 2024 22:54:22 -0500 Subject: [PATCH 046/142] =?UTF-8?q?bump:=20version=200.17.0=20=E2=86=92=20?= =?UTF-8?q?0.18.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 13 +++++++++++++ pyproject.toml | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5724e685..0671e03b 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.18.0 (2024-08-16) + +### Feat + +- move "basic" grid pagination logic to wuttaweb +- inherit from wutta base class for Grid +- inherit most logic from wuttaweb, for GridAction + +### Fix + +- avoid route error in user view, when using wutta people view +- fix some more wutta compat for base template + ## v0.17.0 (2024-08-15) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 31c7ef8d..bd4882c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.17.0" +version = "0.18.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.8.1", + "WuttaWeb>=0.9.0", "zope.sqlalchemy>=1.5", ] From 5e82fe3946d4a65c67527b704198ac5a8d73c6e1 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Aug 2024 10:20:09 -0500 Subject: [PATCH 047/142] fix: fix broken permission directives in web api startup --- tailbone/webapi.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tailbone/webapi.py b/tailbone/webapi.py index 7c0e9b41..d0edb412 100644 --- a/tailbone/webapi.py +++ b/tailbone/webapi.py @@ -85,8 +85,15 @@ def make_pyramid_config(settings): provider.configure_db_sessions(rattail_config, pyramid_config) # add some permissions magic - pyramid_config.add_directive('add_tailbone_permission_group', 'tailbone.auth.add_permission_group') - pyramid_config.add_directive('add_tailbone_permission', 'tailbone.auth.add_permission') + pyramid_config.add_directive('add_wutta_permission_group', + 'wuttaweb.auth.add_permission_group') + pyramid_config.add_directive('add_wutta_permission', + 'wuttaweb.auth.add_permission') + # TODO: deprecate / remove these + pyramid_config.add_directive('add_tailbone_permission_group', + 'wuttaweb.auth.add_permission_group') + pyramid_config.add_directive('add_tailbone_permission', + 'wuttaweb.auth.add_permission') return pyramid_config From c95e42bf828b93f22247660e15df67f2c431a5c4 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 17 Aug 2024 11:05:15 -0500 Subject: [PATCH 048/142] fix: fix misc. errors in grid template per wuttaweb --- tailbone/templates/grids/complete.mako | 91 +++++++++++++++++++------- tailbone/views/master.py | 5 +- tailbone/views/wutta/people.py | 10 --- 3 files changed, 70 insertions(+), 36 deletions(-) diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index 53043803..d3981a16 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -107,13 +107,17 @@ @cellclick="cellClick" % endif + ## paging % if grid.paginated: - :paginated="paginated" - :per-page="perPage" - :current-page="currentPage" - backend-pagination - :total="total" - @page-change="onPageChange" + paginated + pagination-size="is-small" + :per-page="perPage" + :current-page="currentPage" + @page-change="onPageChange" + % if grid.paginate_on_backend: + backend-pagination + :total="pagerStats.item_count" + % endif % endif ## TODO: should let grid (or master view) decide how to set these? @@ -206,12 +210,13 @@ % endif % if grid.paginated: -
showing - {{ firstItem.toLocaleString('en') }} - {{ lastItem.toLocaleString('en') }} - of {{ total.toLocaleString('en') }} results; + {{ renderNumber(pagerStats.first_item) }} + - {{ renderNumber(pagerStats.last_item) }} + of {{ renderNumber(pagerStats.item_count) }} results; data.length) { + last = data.length + } + return { + 'item_count': data.length, + 'items_per_page': this.perPage, + 'page': this.currentPage, + 'first_item': first, + 'last_item': last, + } + }, + + % endif + addFilterChoices() { // nb. this returns all choices available for "Add Filter" operation @@ -373,6 +405,12 @@ methods: { + renderNumber(value) { + if (value != undefined) { + return value.toLocaleString('en') + } + }, + formatAddFilterItem(filtr) { if (!filtr.key) { filtr = this.filters[filtr] @@ -486,23 +524,23 @@ params = params.toString() this.loading = true - this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(({ data }) => { - if (!data.error) { - ${grid.vue_component}CurrentData = data.data + this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(response => { + if (!response.data.error) { + ${grid.vue_component}CurrentData = response.data.data.data this.data = ${grid.vue_component}CurrentData - this.rowStatusMap = data.row_status_map - this.total = data.total_items - this.firstItem = data.first_item - this.lastItem = data.last_item + % if grid.paginated and grid.paginate_on_backend: + this.pagerStats = response.data.pager_stats + % endif + this.rowStatusMap = response.data.data.row_status_map this.loading = false this.savingDefaults = false - this.checkedRows = this.locateCheckedRows(data.checked_rows) + this.checkedRows = this.locateCheckedRows(response.data.data.checked_rows) if (success) { success() } } else { this.$buefy.toast.open({ - message: data.error, + message: response.data.error, type: 'is-danger', duration: 2000, // 4 seconds }) @@ -514,8 +552,11 @@ } }) .catch((error) => { + ${grid.vue_component}CurrentData = [] this.data = [] - this.total = 0 + % if grid.paginated and grid.paginate_on_backend: + this.pagerStats = {} + % endif this.loading = false this.savingDefaults = false if (failure) { diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 58b93568..1fa0ae40 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -346,7 +346,10 @@ class MasterView(View): # return grid data only, if partial page was requested if self.request.params.get('partial'): - return self.json_response(grid.get_table_data()) + context = {'data': 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 = { 'grid': grid, diff --git a/tailbone/views/wutta/people.py b/tailbone/views/wutta/people.py index 3158b478..c10020ea 100644 --- a/tailbone/views/wutta/people.py +++ b/tailbone/views/wutta/people.py @@ -45,10 +45,6 @@ class PersonView(wutta.PersonView): model_class = Person Session = Session - # TODO: /grids/complete.mako is too aggressive for the - # limited support we have in wuttaweb thus far - paginated = False - labels = { 'display_name': "Full Name", } @@ -91,12 +87,6 @@ class PersonView(wutta.PersonView): # display_name g.set_link('display_name') - # first_name - g.set_link('first_name') - - # last_name - g.set_link('last_name') - # merge_requested g.set_label('merge_requested', "MR") g.set_renderer('merge_requested', self.render_merge_requested) From ec36df4a341a1e8c7ba5821fa270fdb1125b1848 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Aug 2024 14:05:52 -0500 Subject: [PATCH 049/142] feat: move single-column grid sorting logic to wuttaweb --- tailbone/forms/core.py | 26 +-- tailbone/grids/core.py | 272 ++++++++++++++----------- tailbone/templates/grids/complete.mako | 63 +++--- tailbone/views/master.py | 28 +-- tailbone/views/wutta/people.py | 8 +- tests/grids/test_core.py | 245 +++++++++++++++++++++- tests/views/test_master.py | 33 ++- 7 files changed, 475 insertions(+), 200 deletions(-) diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index eeae4537..704d3b54 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -47,7 +47,7 @@ from pyramid_deform import SessionFileUploadTempStore from pyramid.renderers import render from webhelpers2.html import tags, HTML -from wuttaweb.util import get_form_data, make_json_safe +from wuttaweb.util import FieldList, get_form_data, make_json_safe from tailbone.db import Session from tailbone.util import raw_datetime, render_markdown @@ -1418,30 +1418,6 @@ class Form(object): return False -class FieldList(list): - """ - Convenience wrapper for a form's field list. - """ - - def insert_before(self, field, newfield): - if field in self: - i = self.index(field) - self.insert(i, newfield) - else: - log.warning("field '%s' not found, will append new field: %s", - field, newfield) - self.append(newfield) - - def insert_after(self, field, newfield): - if field in self: - i = self.index(field) - self.insert(i + 1, newfield) - else: - log.warning("field '%s' not found, will append new field: %s", - field, newfield) - self.append(newfield) - - @colander.deferred def upload_widget(node, kw): request = kw['request'] diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 0b23fb78..cc1888fb 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -39,7 +39,8 @@ from pyramid.renderers import render from webhelpers2.html import HTML, tags from paginate_sqlalchemy import SqlalchemyOrmPage -from wuttaweb.grids import Grid as WuttaGrid, GridAction as WuttaGridAction +from wuttaweb.grids import Grid as WuttaGrid, GridAction as WuttaGridAction, SortInfo +from wuttaweb.util import FieldList from . import filters as gridfilters from tailbone.db import Session from tailbone.util import raw_datetime @@ -48,23 +49,17 @@ from tailbone.util import raw_datetime log = logging.getLogger(__name__) -class FieldList(list): - """ - Convenience wrapper for a field list. - """ - - def insert_before(self, field, newfield): - i = self.index(field) - self.insert(i, newfield) - - def insert_after(self, field, newfield): - i = self.index(field) - self.insert(i + 1, newfield) - - class Grid(WuttaGrid): """ - Core grid class. In sore need of documentation. + Base class for all grids. + + This is now a subclass of + :class:`wuttaweb:wuttaweb.grids.base.Grid`, and exists to add + customizations which have traditionally been part of Tailbone. + + Some of these customizations are still undocumented. Some will + eventually be moved to the upstream/parent class, and possibly + some will be removed outright. What docs we have, are shown here. .. _Buefy docs: https://buefy.org/documentation/table/ @@ -206,10 +201,6 @@ class Grid(WuttaGrid): filters={}, use_byte_string_filters=False, searchable={}, - sortable=False, - sorters={}, - default_sortkey=None, - default_sortdir='asc', checkboxes=False, checked=None, check_handler=None, @@ -231,6 +222,20 @@ class Grid(WuttaGrid): DeprecationWarning, stacklevel=2) kwargs.setdefault('vue_tagname', kwargs.pop('component')) + if kwargs.get('default_sortkey'): + warnings.warn("default_sortkey param is deprecated for Grid(); " + "please use sort_defaults param instead", + DeprecationWarning, stacklevel=2) + if kwargs.get('default_sortdir'): + 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'): + sortkey = kwargs.pop('default_sortkey', None) + sortdir = kwargs.pop('default_sortdir', 'asc') + if sortkey: + kwargs.setdefault('sort_defaults', [(sortkey, sortdir)]) + if kwargs.get('pageable'): warnings.warn("component param is deprecated for Grid(); " "please use vue_tagname param instead", @@ -284,11 +289,6 @@ class Grid(WuttaGrid): self.searchable = searchable or {} - self.sortable = sortable - self.sorters = self.make_sorters(sorters) - self.default_sortkey = default_sortkey - self.default_sortdir = default_sortdir - self.checkboxes = checkboxes self.checked = checked if self.checked is None: @@ -328,9 +328,7 @@ class Grid(WuttaGrid): @property def component(self): - """ - DEPRECATED - use :attr:`vue_tagname` instead. - """ + """ """ warnings.warn("Grid.component is deprecated; " "please use vue_tagname instead", DeprecationWarning, stacklevel=2) @@ -338,20 +336,66 @@ class Grid(WuttaGrid): @property def component_studly(self): - """ - DEPRECATED - use :attr:`vue_component` instead. - """ + """ """ warnings.warn("Grid.component_studly is deprecated; " "please use vue_component instead", DeprecationWarning, stacklevel=2) return self.vue_component + def get_default_sortkey(self): + """ """ + warnings.warn("Grid.default_sortkey is deprecated; " + "please use Grid.sort_defaults instead", + DeprecationWarning, stacklevel=2) + if self.sort_defaults: + return self.sort_defaults[0].sortkey + + def set_default_sortkey(self, value): + """ """ + warnings.warn("Grid.default_sortkey is deprecated; " + "please use Grid.sort_defaults instead", + DeprecationWarning, stacklevel=2) + if self.sort_defaults: + info = self.sort_defaults[0] + self.sort_defaults[0] = SortInfo(value, info.sortdir) + else: + self.sort_defaults = [SortInfo(value, 'asc')] + + default_sortkey = property(get_default_sortkey, set_default_sortkey) + + def get_default_sortdir(self): + """ """ + warnings.warn("Grid.default_sortdir is deprecated; " + "please use Grid.sort_defaults instead", + DeprecationWarning, stacklevel=2) + if self.sort_defaults: + return self.sort_defaults[0].sortdir + + def set_default_sortdir(self, value): + """ """ + warnings.warn("Grid.default_sortdir is deprecated; " + "please use Grid.sort_defaults instead", + DeprecationWarning, stacklevel=2) + if self.sort_defaults: + info = self.sort_defaults[0] + self.sort_defaults[0] = SortInfo(info.sortkey, value) + else: + raise ValueError("cannot set default_sortdir without default_sortkey") + + default_sortdir = property(get_default_sortdir, set_default_sortdir) + def get_pageable(self): """ """ + warnings.warn("Grid.pageable is deprecated; " + "please use Grid.paginated instead", + DeprecationWarning, stacklevel=2) return self.paginated def set_pageable(self, value): """ """ + warnings.warn("Grid.pageable is deprecated; " + "please use Grid.paginated instead", + DeprecationWarning, stacklevel=2) self.paginated = value pageable = property(get_pageable, set_pageable) @@ -405,18 +449,30 @@ class Grid(WuttaGrid): self.joiners[key] = joiner def set_sorter(self, key, *args, **kwargs): - if len(args) == 1 and args[0] is None: - self.remove_sorter(key) + """ """ + + if len(args) == 1: + if kwargs: + warnings.warn("kwargs are ignored for Grid.set_sorter(); " + "please refactor your code accordingly", + DeprecationWarning, stacklevel=2) + if args[0] is None: + warnings.warn("specifying None is deprecated for Grid.set_sorter(); " + "please use Grid.remove_sorter() instead", + DeprecationWarning, stacklevel=2) + self.remove_sorter(key) + else: + super().set_sorter(key, args[0]) + + elif len(args) == 0: + super().set_sorter(key) + else: + warnings.warn("multiple args are deprecated for Grid.set_sorter(); " + "please refactor your code accordingly", + DeprecationWarning, stacklevel=2) self.sorters[key] = self.make_sorter(*args, **kwargs) - def remove_sorter(self, key): - self.sorters.pop(key, None) - - def set_sort_defaults(self, sortkey, sortdir='asc'): - self.default_sortkey = sortkey - self.default_sortdir = sortdir - def set_filter(self, key, *args, **kwargs): if len(args) == 1 and args[0] is None: self.remove_filter(key) @@ -731,53 +787,12 @@ class Grid(WuttaGrid): if filtr.active: yield filtr - def make_sorters(self, sorters=None): - """ - Returns an initial set of sorters which will be available to the grid. - The grid itself may or may not provide some default sorters, and the - ``sorters`` kwarg may contain additions and/or overrides. - """ - sorters, updates = {}, sorters - if self.model_class: - mapper = orm.class_mapper(self.model_class) - for prop in mapper.iterate_properties: - if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'): - sorters[prop.key] = self.make_sorter(prop) - if updates: - sorters.update(updates) - return sorters - - def make_sorter(self, model_property): - """ - Returns a function suitable for a sort map callable, with typical logic - built in for sorting applied to ``field``. - """ - class_ = getattr(model_property, 'class_', self.model_class) - column = getattr(class_, model_property.key) - - def sorter(query, direction): - # TODO: this seems hacky..normally we expect a true query - # of course, but in some cases it may be a list instead. - # if so then we can't actually sort - if isinstance(query, list): - return query - return query.order_by(getattr(column, direction)()) - - sorter._class = class_ - sorter._column = column - - return sorter - def make_simple_sorter(self, key, foldcase=False): - """ - Returns a function suitable for a sort map callable, with typical logic - built in for sorting a data set comprised of dicts, on the given key. - """ - if foldcase: - keyfunc = lambda v: v[key].lower() - else: - keyfunc = lambda v: v[key] - return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc') + """ """ + warnings.warn("Grid.make_simple_sorter() is deprecated; " + "please use Grid.make_sorter() instead", + DeprecationWarning, stacklevel=2) + return self.make_sorter(key, foldcase=foldcase) def get_pagesize_options(self, default=None): """ """ @@ -849,10 +864,17 @@ class Grid(WuttaGrid): # initial default settings settings = {} if self.sortable: - if self.default_sortkey: + if self.sort_defaults: + sort_defaults = self.sort_defaults + if len(sort_defaults) > 1: + log.warning("multiple sort defaults are not yet supported; " + "list will be pruned to first element for '%s' grid: %s", + self.key, sort_defaults) + sort_defaults = [sort_defaults[0]] + sortinfo = sort_defaults[0] settings['sorters.length'] = 1 - settings['sorters.1.key'] = self.default_sortkey - settings['sorters.1.dir'] = self.default_sortdir + settings['sorters.1.key'] = sortinfo.sortkey + settings['sorters.1.dir'] = sortinfo.sortdir else: settings['sorters.length'] = 0 if self.paginated: @@ -927,11 +949,12 @@ class Grid(WuttaGrid): filtr.verb = settings['filter.{}.verb'.format(filtr.key)] filtr.value = settings['filter.{}.value'.format(filtr.key)] if self.sortable: + # and self.sort_on_backend: self.active_sorters = [] for i in range(1, settings['sorters.length'] + 1): self.active_sorters.append({ - 'field': settings[f'sorters.{i}.key'], - 'order': settings[f'sorters.{i}.dir'], + 'key': settings[f'sorters.{i}.key'], + 'dir': settings[f'sorters.{i}.dir'], }) if self.paginated: self.pagesize = settings['pagesize'] @@ -1321,21 +1344,24 @@ class Grid(WuttaGrid): return data - def sort_data(self, data): - """ - Sort the given query according to current settings, and return the result. - """ - # bail if no sort settings - if not self.active_sorters: + def sort_data(self, data, sorters=None): + """ """ + if sorters is None: + sorters = self.active_sorters + if not sorters: return data - # TODO: is there a better way to check for SA sorting? - if self.model_class: + # sqlalchemy queries require special handling, in case of + # multi-column sorting + if isinstance(data, orm.Query): # collect actual column sorters for order_by clause - sorters = [] - for sorter in self.active_sorters: - sortkey = sorter['field'] + query_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: log.warning("unknown sorter: %s", sorter) @@ -1347,34 +1373,36 @@ class Grid(WuttaGrid): self.joined.add(sortkey) # add column/dir to collection - sortdir = sorter['order'] - sorters.append(getattr(sortfunc._column, sortdir)()) + query_sorters.append(getattr(sortfunc._column, sortdir)()) # apply sorting to query - if sorters: - data = data.order_by(*sorters) + if query_sorters: + data = data.order_by(*query_sorters) return data - else: - # not a SQLAlchemy grid, custom sorter + # manual sorting; only one column allowed + if len(sorters) != 1: + raise NotImplementedError("mulit-column manual sorting not yet supported") - assert len(self.active_sorters) < 2 + # our one and only active sorter + sorter = sorters[0] + sortkey = sorter['key'] + sortdir = sorter['dir'] - sortkey = self.active_sorters[0]['field'] - sortdir = self.active_sorters[0]['order'] or 'asc' + # cannot sort unless we have a sorter callable + sortfunc = self.sorters.get(sortkey) + if not sortfunc: + return data - # Cannot sort unless we have a sort function. - sortfunc = self.sorters.get(sortkey) - if not sortfunc: - return data + # apply joins needed for this sorter + # TODO: is this actually relevant for manual sort? + if sortkey in self.joiners and sortkey not in self.joined: + data = self.joiners[sortkey](data) + self.joined.add(sortkey) - # apply joins needed for this sorter - if sortkey in self.joiners and sortkey not in self.joined: - data = self.joiners[sortkey](data) - self.joined.add(sortkey) - - return sortfunc(data, sortdir) + # invoke the sorter + return sortfunc(data, sortdir) def paginate_data(self, data): """ @@ -1671,7 +1699,7 @@ class Grid(WuttaGrid): columns.append({ 'field': name, 'label': self.get_label(name), - 'sortable': self.sortable and name in self.sorters, + 'sortable': self.is_sortable(name), 'visible': name not in self.invisible, }) return columns diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index d3981a16..5a005c2e 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -81,7 +81,11 @@ % endif % endif - % if getattr(grid, 'sortable', False): + ## sorting + % if grid.sortable: + ## nb. buefy only supports *one* default sorter + :default-sort="sorters.length ? [sorters[0].key, sorters[0].dir] : null" + backend-sorting @sort="onSort" @sorting-priority-removed="sortingPriorityRemoved" @@ -93,8 +97,6 @@ ## https://github.com/buefy/buefy/issues/2584 :sort-multiple="allowMultiSort" - ## nb. specify default sort only if single-column - :default-sort="backendSorters.length == 1 ? [backendSorters[0].field, backendSorters[0].order] : null" ## nb. otherwise there may be default multi-column sort :sort-multiple-data="sortingPriority" @@ -272,7 +274,9 @@ % endif % endif - % if getattr(grid, 'sortable', False): + ## sorting + % if grid.sortable: + sorters: ${json.dumps(grid.active_sorters)|n}, ## TODO: there is a bug (?) which prevents the arrow from ## displaying for simple default single-column sort. so to @@ -281,10 +285,7 @@ ## https://github.com/buefy/buefy/issues/2584 allowMultiSort: false, - ## nb. this contains all truly active sorters - backendSorters: ${json.dumps(grid.active_sorters)|n}, - - ## nb. whereas this will only contain multi-column sorters, + ## nb. this will only contain multi-column sorters, ## but will be *empty* for single-column sorting % if len(grid.active_sorters) > 1: sortingPriority: ${json.dumps(grid.active_sorters)|n}, @@ -474,17 +475,18 @@ }, getBasicParams() { - let params = {} - % if getattr(grid, 'sortable', False): - for (let i = 1; i <= this.backendSorters.length; i++) { - params['sort'+i+'key'] = this.backendSorters[i-1].field - params['sort'+i+'dir'] = this.backendSorters[i-1].order + const params = { + % if grid.paginated and grid.paginate_on_backend: + pagesize: this.perPage, + page: this.currentPage, + % endif + } + % if grid.sortable and grid.sort_on_backend: + for (let i = 1; i <= this.sorters.length; i++) { + params['sort'+i+'key'] = this.sorters[i-1].key + params['sort'+i+'dir'] = this.sorters[i-1].dir } % endif - % if grid.paginated: - params.pagesize = this.perPage - params.page = this.currentPage - % endif return params }, @@ -526,15 +528,15 @@ this.loading = true this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(response => { if (!response.data.error) { - ${grid.vue_component}CurrentData = response.data.data.data + ${grid.vue_component}CurrentData = response.data.data this.data = ${grid.vue_component}CurrentData % if grid.paginated and grid.paginate_on_backend: this.pagerStats = response.data.pager_stats % endif - this.rowStatusMap = response.data.data.row_status_map + this.rowStatusMap = response.data.row_status_map || {} this.loading = false this.savingDefaults = false - this.checkedRows = this.locateCheckedRows(response.data.data.checked_rows) + this.checkedRows = this.locateCheckedRows(response.data.checked_rows || []) if (success) { success() } @@ -597,26 +599,26 @@ onSort(field, order, event) { - // nb. buefy passes field name, oruga passes object - if (field.field) { + ## nb. buefy passes field name; oruga passes field object + % if request.use_oruga: field = field.field - } + % endif if (event.ctrlKey) { // engage or enhance multi-column sorting - let sorter = this.backendSorters.filter(i => i.field === field)[0] + const sorter = this.sorters.filter(s => s.key === field)[0] if (sorter) { - sorter.order = sorter.order === 'desc' ? 'asc' : 'desc' + sorter.dir = sorter.dir === 'desc' ? 'asc' : 'desc' } else { - this.backendSorters.push({field, order}) + this.sorters.push({key: field, dir: order}) } - this.sortingPriority = this.backendSorters + this.sortingPriority = this.sorters } else { // sort by single column only - this.backendSorters = [{field, order}] + this.sorters = [{key: field, dir: order}] this.sortingPriority = [] } @@ -629,12 +631,11 @@ sortingPriorityRemoved(field) { // prune field from active sorters - this.backendSorters = this.backendSorters.filter( - (sorter) => sorter.field !== field) + this.sorters = this.sorters.filter(s => s.key !== field) // nb. must keep active sorter list "as-is" even if // there is only one sorter; buefy seems to expect it - this.sortingPriority = this.backendSorters + this.sortingPriority = this.sorters this.loadAsyncData() }, diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 1fa0ae40..53f46020 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -345,8 +345,8 @@ class MasterView(View): self.first_visible_grid_index = grid.pager.first_item # return grid data only, if partial page was requested - if self.request.params.get('partial'): - context = {'data': grid.get_table_data()} + 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) @@ -2565,11 +2565,12 @@ class MasterView(View): so if you like you can return a different help URL depending on which type of CRUD view is in effect, etc. """ + # nb. self.Session may differ, so use tailbone.db.Session + session = Session() model = self.model route_prefix = self.get_route_prefix() - # nb. self.Session may differ, so use tailbone.db.Session - info = Session.query(model.TailbonePageHelp)\ + info = session.query(model.TailbonePageHelp)\ .filter(model.TailbonePageHelp.route_prefix == route_prefix)\ .first() if info and info.help_url: @@ -2587,11 +2588,12 @@ class MasterView(View): """ Return the markdown help text for current page, if defined. """ + # nb. self.Session may differ, so use tailbone.db.Session + session = Session() model = self.model route_prefix = self.get_route_prefix() - # nb. self.Session may differ, so use tailbone.db.Session - info = Session.query(model.TailbonePageHelp)\ + info = session.query(model.TailbonePageHelp)\ .filter(model.TailbonePageHelp.route_prefix == route_prefix)\ .first() if info and info.markdown_text: @@ -2608,6 +2610,8 @@ class MasterView(View): if not self.can_edit_help(): raise self.forbidden() + # nb. self.Session may differ, so use tailbone.db.Session + session = Session() model = self.model route_prefix = self.get_route_prefix() schema = colander.Schema() @@ -2625,13 +2629,12 @@ class MasterView(View): if not form.validate(): return {'error': "Form did not validate"} - # nb. self.Session may differ, so use tailbone.db.Session - info = Session.query(model.TailbonePageHelp)\ + info = session.query(model.TailbonePageHelp)\ .filter(model.TailbonePageHelp.route_prefix == route_prefix)\ .first() if not info: info = model.TailbonePageHelp(route_prefix=route_prefix) - Session.add(info) + session.add(info) info.help_url = form.validated['help_url'] info.markdown_text = form.validated['markdown_text'] @@ -2641,6 +2644,8 @@ class MasterView(View): if not self.can_edit_help(): raise self.forbidden() + # nb. self.Session may differ, so use tailbone.db.Session + session = Session() model = self.model route_prefix = self.get_route_prefix() schema = colander.Schema() @@ -2657,15 +2662,14 @@ class MasterView(View): if not form.validate(): return {'error': "Form did not validate"} - # nb. self.Session may differ, so use tailbone.db.Session - info = Session.query(model.TailboneFieldInfo)\ + info = session.query(model.TailboneFieldInfo)\ .filter(model.TailboneFieldInfo.route_prefix == route_prefix)\ .filter(model.TailboneFieldInfo.field_name == form.validated['field_name'])\ .first() if not info: info = model.TailboneFieldInfo(route_prefix=route_prefix, field_name=form.validated['field_name']) - Session.add(info) + session.add(info) info.markdown_text = form.validated['markdown_text'] return {'ok': True} diff --git a/tailbone/views/wutta/people.py b/tailbone/views/wutta/people.py index c10020ea..968eaf3d 100644 --- a/tailbone/views/wutta/people.py +++ b/tailbone/views/wutta/people.py @@ -44,6 +44,7 @@ class PersonView(wutta.PersonView): """ model_class = Person Session = Session + sort_defaults = 'display_name' labels = { 'display_name': "Full Name", @@ -73,13 +74,6 @@ class PersonView(wutta.PersonView): # CRUD methods ############################## - def get_query(self, session=None): - """ """ - model = self.app.model - session = session or self.Session() - return session.query(model.Person)\ - .order_by(model.Person.display_name) - def configure_grid(self, g): """ """ super().configure_grid(g) diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index 7cba917a..9f9b816f 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -1,6 +1,8 @@ # -*- coding: utf-8; -*- -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch + +from sqlalchemy import orm from tailbone.grids import core as mod from tests.util import WebTestCase @@ -27,6 +29,16 @@ class TestGrid(WebTestCase): grid = self.make_grid(component='blarg') self.assertEqual(grid.vue_tagname, 'blarg') + # default_sortkey, default_sortdir + grid = self.make_grid() + self.assertEqual(grid.sort_defaults, []) + grid = self.make_grid(default_sortkey='name') + self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')]) + grid = self.make_grid(default_sortdir='desc') + self.assertEqual(grid.sort_defaults, []) + grid = self.make_grid(default_sortkey='name', default_sortdir='desc') + self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')]) + # pageable grid = self.make_grid() self.assertFalse(grid.paginated) @@ -159,6 +171,27 @@ class TestGrid(WebTestCase): grid.set_action_urls(setting, setting, 0) self.assertEqual(setting['_action_url_view'], '/blarg') + def test_default_sortkey(self): + grid = self.make_grid() + self.assertEqual(grid.sort_defaults, []) + self.assertIsNone(grid.default_sortkey) + grid.default_sortkey = 'name' + self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'asc')]) + self.assertEqual(grid.default_sortkey, 'name') + grid.default_sortkey = 'value' + self.assertEqual(grid.sort_defaults, [mod.SortInfo('value', 'asc')]) + self.assertEqual(grid.default_sortkey, 'value') + + def test_default_sortdir(self): + grid = self.make_grid() + self.assertEqual(grid.sort_defaults, []) + self.assertIsNone(grid.default_sortdir) + self.assertRaises(ValueError, setattr, grid, 'default_sortdir', 'asc') + grid.sort_defaults = [mod.SortInfo('name', 'asc')] + grid.default_sortdir = 'desc' + self.assertEqual(grid.sort_defaults, [mod.SortInfo('name', 'desc')]) + self.assertEqual(grid.default_sortdir, 'desc') + def test_pageable(self): grid = self.make_grid() self.assertFalse(grid.paginated) @@ -219,6 +252,212 @@ class TestGrid(WebTestCase): size = grid.get_pagesize() self.assertEqual(size, 15) + def test_set_sorter(self): + model = self.app.model + grid = self.make_grid(model_class=model.Setting, + sortable=True, sort_on_backend=True) + + # passing None will remove sorter + self.assertIn('name', grid.sorters) + grid.set_sorter('name', None) + self.assertNotIn('name', grid.sorters) + + # can recreate sorter with just column name + grid.set_sorter('name') + self.assertIn('name', grid.sorters) + grid.remove_sorter('name') + self.assertNotIn('name', grid.sorters) + grid.set_sorter('name', 'name') + self.assertIn('name', grid.sorters) + + # can recreate sorter with model property + grid.remove_sorter('name') + self.assertNotIn('name', grid.sorters) + grid.set_sorter('name', model.Setting.name) + self.assertIn('name', grid.sorters) + + # extra kwargs are ignored + grid.remove_sorter('name') + self.assertNotIn('name', grid.sorters) + grid.set_sorter('name', model.Setting.name, foo='bar') + self.assertIn('name', grid.sorters) + + # passing multiple args will invoke make_filter() directly + grid.remove_sorter('name') + self.assertNotIn('name', grid.sorters) + with patch.object(grid, 'make_sorter') as make_sorter: + make_sorter.return_value = 42 + grid.set_sorter('name', 'foo', 'bar') + make_sorter.assert_called_once_with('foo', 'bar') + self.assertEqual(grid.sorters['name'], 42) + + def test_make_simple_sorter(self): + model = self.app.model + grid = self.make_grid(model_class=model.Setting, + sortable=True, sort_on_backend=True) + + # delegates to grid.make_sorter() + with patch.object(grid, 'make_sorter') as make_sorter: + make_sorter.return_value = 42 + sorter = grid.make_simple_sorter('name', foldcase=True) + make_sorter.assert_called_once_with('name', foldcase=True) + self.assertEqual(sorter, 42) + + def test_load_settings(self): + model = self.app.model + + # nb. first use a paging grid + grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True, + pagesize=20, page=1) + + # settings are loaded, applied, saved + self.assertEqual(grid.page, 1) + self.assertNotIn('grid.foo.page', self.request.session) + self.request.GET = {'pagesize': '10', 'page': '2'} + grid.load_settings() + self.assertEqual(grid.page, 2) + self.assertEqual(self.request.session['grid.foo.page'], 2) + + # can skip the saving step + self.request.GET = {'pagesize': '10', 'page': '3'} + grid.load_settings(store=False) + self.assertEqual(grid.page, 3) + self.assertEqual(self.request.session['grid.foo.page'], 2) + + # no error for non-paginated grid + grid = self.make_grid(key='foo', paginated=False) + grid.load_settings() + self.assertFalse(grid.paginated) + + # nb. next use a sorting grid + grid = self.make_grid(key='settings', model_class=model.Setting, + sortable=True, sort_on_backend=True) + + # settings are loaded, applied, saved + self.assertEqual(grid.sort_defaults, []) + self.assertFalse(hasattr(grid, 'active_sorters')) + self.request.GET = {'sort1key': 'name', 'sort1dir': 'desc'} + grid.load_settings() + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}]) + self.assertEqual(self.request.session['grid.settings.sorters.length'], 1) + self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name') + self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc') + + # can skip the saving step + self.request.GET = {'sort1key': 'name', 'sort1dir': 'asc'} + grid.load_settings(store=False) + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}]) + self.assertEqual(self.request.session['grid.settings.sorters.length'], 1) + self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name') + self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc') + + # no error for non-sortable grid + grid = self.make_grid(key='foo', sortable=False) + grid.load_settings() + self.assertFalse(grid.sortable) + + # with sort defaults + grid = self.make_grid(model_class=model.Setting, sortable=True, + sort_on_backend=True, sort_defaults='name') + self.assertFalse(hasattr(grid, 'active_sorters')) + grid.load_settings() + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}]) + + # with multi-column sort defaults + grid = self.make_grid(model_class=model.Setting, sortable=True, + sort_on_backend=True) + grid.sort_defaults = [ + mod.SortInfo('name', 'asc'), + mod.SortInfo('value', 'desc'), + ] + self.assertFalse(hasattr(grid, 'active_sorters')) + grid.load_settings() + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'asc'}]) + + # load settings from session when nothing is in request + self.request.GET = {} + self.request.session.invalidate() + self.assertNotIn('grid.settings.sorters.length', self.request.session) + self.request.session['grid.settings.sorters.length'] = 1 + self.request.session['grid.settings.sorters.1.key'] = 'name' + self.request.session['grid.settings.sorters.1.dir'] = 'desc' + grid = self.make_grid(key='settings', model_class=model.Setting, + sortable=True, sort_on_backend=True, + paginated=True, paginate_on_backend=True) + self.assertFalse(hasattr(grid, 'active_sorters')) + grid.load_settings() + self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}]) + + def test_sort_data(self): + model = self.app.model + sample_data = [ + {'name': 'foo1', 'value': 'ONE'}, + {'name': 'foo2', 'value': 'two'}, + {'name': 'foo3', 'value': 'three'}, + {'name': 'foo4', 'value': 'four'}, + {'name': 'foo5', 'value': 'five'}, + {'name': 'foo6', 'value': 'six'}, + {'name': 'foo7', 'value': 'seven'}, + {'name': 'foo8', 'value': 'eight'}, + {'name': 'foo9', 'value': 'nine'}, + ] + for setting in sample_data: + self.app.save_setting(self.session, setting['name'], setting['value']) + self.session.commit() + sample_query = self.session.query(model.Setting) + + grid = self.make_grid(model_class=model.Setting, + sortable=True, sort_on_backend=True, + sort_defaults=('name', 'desc')) + grid.load_settings() + + # can sort a simple list of data + sorted_data = grid.sort_data(sample_data) + self.assertIsInstance(sorted_data, list) + self.assertEqual(len(sorted_data), 9) + self.assertEqual(sorted_data[0]['name'], 'foo9') + self.assertEqual(sorted_data[-1]['name'], 'foo1') + + # can also sort a data query + sorted_query = grid.sort_data(sample_query) + self.assertIsInstance(sorted_query, orm.Query) + sorted_data = sorted_query.all() + self.assertEqual(len(sorted_data), 9) + self.assertEqual(sorted_data[0]['name'], 'foo9') + self.assertEqual(sorted_data[-1]['name'], 'foo1') + + # cannot sort data if sorter missing in overrides + sorted_data = grid.sort_data(sample_data, sorters=[]) + # nb. sorted data is in same order as original sample (not sorted) + self.assertEqual(sorted_data[0]['name'], 'foo1') + self.assertEqual(sorted_data[-1]['name'], 'foo9') + + # error if mult-column sort attempted + self.assertRaises(NotImplementedError, grid.sort_data, sample_data, sorters=[ + {'key': 'name', 'dir': 'desc'}, + {'key': 'value', 'dir': 'asc'}, + ]) + + # cannot sort data if sortfunc is missing for column + grid.remove_sorter('name') + sorted_data = grid.sort_data(sample_data) + # nb. sorted data is in same order as original sample (not sorted) + self.assertEqual(sorted_data[0]['name'], 'foo1') + self.assertEqual(sorted_data[-1]['name'], 'foo9') + + # cannot sort data if sortfunc is missing for column + grid.remove_sorter('name') + # nb. attempting multi-column sort, but only one sorter exists + self.assertEqual(list(grid.sorters), ['value']) + grid.active_sorters = [{'key': 'name', 'dir': 'asc'}, + {'key': 'value', 'dir': 'asc'}] + with patch.object(sample_query, 'order_by') as order_by: + order_by.return_value = 42 + sorted_query = grid.sort_data(sample_query) + order_by.assert_called_once() + self.assertEqual(len(order_by.call_args.args), 1) + self.assertEqual(sorted_query, 42) + def test_render_vue_tag(self): model = self.app.model @@ -249,11 +488,13 @@ class TestGrid(WebTestCase): model = self.app.model # sanity check - grid = self.make_grid('settings', model_class=model.Setting) + grid = self.make_grid('settings', model_class=model.Setting, sortable=True) columns = grid.get_vue_columns() self.assertEqual(len(columns), 2) self.assertEqual(columns[0]['field'], 'name') + self.assertTrue(columns[0]['sortable']) self.assertEqual(columns[1]['field'], 'value') + self.assertTrue(columns[1]['sortable']) def test_get_vue_data(self): model = self.app.model diff --git a/tests/views/test_master.py b/tests/views/test_master.py index 572875a0..0e459e7d 100644 --- a/tests/views/test_master.py +++ b/tests/views/test_master.py @@ -1,6 +1,6 @@ # -*- coding: utf-8; -*- -from unittest.mock import patch +from unittest.mock import patch, MagicMock from tailbone.views import master as mod from wuttaweb.grids import GridAction @@ -33,3 +33,34 @@ class TestMasterView(WebTestCase): view = self.make_view() action = view.make_action('view') self.assertIsInstance(action, GridAction) + + def test_index(self): + self.pyramid_config.include('tailbone.views.common') + self.pyramid_config.include('tailbone.views.auth') + model = self.app.model + + # mimic view for /settings + with patch.object(mod, 'Session', return_value=self.session): + with patch.multiple(mod.MasterView, create=True, + model_class=model.Setting, + Session=MagicMock(return_value=self.session), + get_index_url=MagicMock(return_value='/settings/'), + get_help_url=MagicMock(return_value=None)): + + # basic + view = self.make_view() + response = view.index() + self.assertEqual(response.status_code, 200) + + # then again with data, to include view action url + data = [{'name': 'foo', 'value': 'bar'}] + with patch.object(view, 'get_data', return_value=data): + response = view.index() + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content_type, 'text/html') + + # then once more as 'partial' - aka. data only + self.request.GET = {'partial': '1'} + response = view.index() + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content_type, 'application/json') From 290f8fd51eddca9e2f3778a23f44bfe356e94ad7 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Aug 2024 19:22:04 -0500 Subject: [PATCH 050/142] feat: move multi-column grid sorting logic to wuttaweb tailbone grid template still duplicates much for Vue, and will until we can port the filters and anything else remaining.. --- tailbone/grids/core.py | 251 +++++++------------------ tailbone/templates/base.mako | 18 +- tailbone/templates/grids/complete.mako | 181 ++++++++++-------- tailbone/views/master.py | 3 +- tests/grids/test_core.py | 91 ++++++--- 5 files changed, 252 insertions(+), 292 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index cc1888fb..9c445fec 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -850,28 +850,23 @@ class Grid(WuttaGrid): return self.get_pagesize() - def load_settings(self, store=True): - """ - Load current/effective settings for the grid, from the request query - string and/or session storage. If ``store`` is true, then once - settings have been fully read, they are stored in current session for - next time. Finally, various instance attributes of the grid and its - filters are updated in-place to reflect the settings; this is so code - needn't access the settings dict directly, but the more Pythonic - instance attributes. - """ + def load_settings(self, **kwargs): + """ """ + if 'store' in kwargs: + warnings.warn("the 'store' param is deprecated for load_settings(); " + "please use the 'persist' param instead", + DeprecationWarning, stacklevel=2) + kwargs.setdefault('persist', kwargs.pop('store')) + + persist = kwargs.get('persist', True) # initial default settings settings = {} if self.sortable: if self.sort_defaults: - sort_defaults = self.sort_defaults - if len(sort_defaults) > 1: - log.warning("multiple sort defaults are not yet supported; " - "list will be pruned to first element for '%s' grid: %s", - self.key, sort_defaults) - sort_defaults = [sort_defaults[0]] - sortinfo = sort_defaults[0] + # nb. as of writing neither Buefy nor Oruga support a + # multi-column *default* sort; so just use first sorter + sortinfo = self.sort_defaults[0] settings['sorters.length'] = 1 settings['sorters.1.key'] = sortinfo.sortkey settings['sorters.1.dir'] = sortinfo.sortdir @@ -900,16 +895,16 @@ class Grid(WuttaGrid): elif self.filterable and self.request_has_settings('filter'): self.update_filter_settings(settings, 'request') if self.request_has_settings('sort'): - self.update_sort_settings(settings, 'request') + self.update_sort_settings(settings, src='request') else: - self.update_sort_settings(settings, 'session') + self.update_sort_settings(settings, src='session') self.update_page_settings(settings) # If request has no filter settings but does have sort settings, grab # those, then grab filter settings from session, then grab pager # settings from request or session. elif self.request_has_settings('sort'): - self.update_sort_settings(settings, 'request') + self.update_sort_settings(settings, src='request') self.update_filter_settings(settings, 'session') self.update_page_settings(settings) @@ -921,26 +916,26 @@ class Grid(WuttaGrid): elif self.request_has_settings('page'): self.update_page_settings(settings) self.update_filter_settings(settings, 'session') - self.update_sort_settings(settings, '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_sort_settings(settings, 'session') + self.update_sort_settings(settings, src='session') self.update_page_settings(settings) # If no settings were found in request or session, don't store result. else: - store = False + persist = False # Maybe store settings for next time. - if store: - self.persist_settings(settings, 'session') + if persist: + self.persist_settings(settings, dest='session') # If request contained instruction to save current settings as defaults # for the current user, then do that. if self.request.GET.get('save-current-filters-as-defaults') == 'true': - self.persist_settings(settings, 'defaults') + self.persist_settings(settings, dest='defaults') # update ourself to reflect settings if self.filterable: @@ -1107,44 +1102,6 @@ class Grid(WuttaGrid): return any([key.startswith(f'{prefix}.filter') for key in self.request.session]) - def get_setting(self, source, settings, key, normalize=lambda v: v, default=None): - """ - Get the effective value for a particular setting, preferring ``source`` - but falling back to existing ``settings`` and finally the ``default``. - """ - if source not in ('request', 'session'): - raise ValueError("Invalid source identifier: {}".format(source)) - - # If source is query string, try that first. - if source == 'request': - value = self.request.GET.get(key) - if value is not None: - try: - value = normalize(value) - except ValueError: - pass - else: - return value - - # Or, if source is session, try that first. - else: - value = self.request.session.get('grid.{}.{}'.format(self.key, key)) - if value is not None: - return normalize(value) - - # If source had nothing, try default/existing settings. - value = settings.get(key) - if value is not None: - try: - value = normalize(value) - except ValueError: - pass - else: - return value - - # Okay then, default it is. - return default - def update_filter_settings(self, settings, source): """ Updates a settings dictionary according to filter settings data found @@ -1165,71 +1122,18 @@ class Grid(WuttaGrid): # 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( - source, settings, '{}.verb'.format(filtr.key), default='') + settings, f'{filtr.key}.verb', src='request', default='') settings['{}.value'.format(prefix)] = self.get_setting( - source, settings, filtr.key, default='') + settings, filtr.key, src='request', default='') else: # source = session settings['{}.active'.format(prefix)] = self.get_setting( - source, settings, '{}.active'.format(prefix), + settings, f'{prefix}.active', src='session', normalize=lambda v: str(v).lower() == 'true', default=False) settings['{}.verb'.format(prefix)] = self.get_setting( - source, settings, '{}.verb'.format(prefix), default='') + settings, f'{prefix}.verb', src='session', default='') settings['{}.value'.format(prefix)] = self.get_setting( - source, settings, '{}.value'.format(prefix), default='') - - def update_sort_settings(self, settings, source): - """ - Updates a settings dictionary according to sort 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.sortable: - return - - if source == 'request': - - # TODO: remove this eventually, but some links in the wild - # may still include these params, so leave it for now - if 'sortkey' in self.request.GET: - settings['sorters.length'] = 1 - settings['sorters.1.key'] = self.get_setting(source, settings, 'sortkey') - settings['sorters.1.dir'] = self.get_setting(source, settings, 'sortdir') - - else: # the future - i = 1 - while True: - skey = f'sort{i}key' - if skey in self.request.GET: - settings[f'sorters.{i}.key'] = self.get_setting(source, settings, skey) - settings[f'sorters.{i}.dir'] = self.get_setting(source, settings, f'sort{i}dir') - else: - break - i += 1 - settings['sorters.length'] = i - 1 - - else: # session - - # TODO: definitely will remove this, but leave it for now - # so it doesn't monkey with current user sessions when - # next upgrade happens. so, remove after all are upgraded - sortkey = self.get_setting(source, settings, 'sortkey') - if sortkey: - settings['sorters.length'] = 1 - settings['sorters.1.key'] = sortkey - settings['sorters.1.dir'] = self.get_setting(source, settings, 'sortdir') - - else: # the future - settings['sorters.length'] = self.get_setting(source, settings, - 'sorters.length', int) - for i in range(1, settings['sorters.length'] + 1): - for key in ('key', 'dir'): - skey = f'sorters.{i}.{key}' - settings[skey] = self.get_setting(source, settings, skey) + settings, f'{prefix}.value', src='session', default='') def update_page_settings(self, settings): """ @@ -1264,18 +1168,19 @@ class Grid(WuttaGrid): if page is not None: settings['page'] = int(page) - def persist_settings(self, settings, to='session'): - """ - Persist the given settings in some way, as defined by ``func``. - """ + def persist_settings(self, settings, dest='session'): + """ """ + if dest not in ('defaults', 'session'): + raise ValueError(f"invalid dest identifier: {dest}") + app = self.request.rattail_config.get_app() model = app.model - def persist(key, value=lambda k: settings[k]): - if to == 'defaults': + def persist(key, value=lambda k: settings.get(k)): + if dest == 'defaults': skey = 'tailbone.{}.grid.{}.{}'.format(self.request.user.uuid, self.key, key) app.save_setting(Session(), skey, value(key)) - else: # to == session + else: # dest == session skey = 'grid.{}.{}'.format(self.key, key) self.request.session[skey] = value(key) @@ -1287,9 +1192,11 @@ class Grid(WuttaGrid): if self.sortable: - # first clear existing settings for *sorting* only - # nb. this is because number of sort settings will vary - if to == 'defaults': + # first must clear all sort settings from dest. this is + # because number of sort settings will vary, so we delete + # all and then write all + + if dest == 'defaults': prefix = f'tailbone.{self.request.user.uuid}.grid.{self.key}' query = Session.query(model.Setting)\ .filter(sa.or_( @@ -1303,7 +1210,9 @@ class Grid(WuttaGrid): for setting in query.all(): Session.delete(setting) Session.flush() + else: # session + # remove sort settings from user session prefix = f'grid.{self.key}' for key in list(self.request.session): if key.startswith(f'{prefix}.sorters.'): @@ -1315,10 +1224,12 @@ class Grid(WuttaGrid): self.request.session.pop(f'{prefix}.sortkey', None) self.request.session.pop(f'{prefix}.sortdir', None) - persist('sorters.length') - for i in range(1, settings['sorters.length'] + 1): - persist(f'sorters.{i}.key') - persist(f'sorters.{i}.dir') + # now save sort settings to dest + if 'sorters.length' in settings: + persist('sorters.length') + for i in range(1, settings['sorters.length'] + 1): + persist(f'sorters.{i}.key') + persist(f'sorters.{i}.dir') if self.paginated: persist('pagesize') @@ -1351,58 +1262,32 @@ class Grid(WuttaGrid): if not sorters: return data - # sqlalchemy queries require special handling, in case of - # multi-column sorting - if isinstance(data, orm.Query): + # 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) - # collect actual column sorters for order_by clause - query_sorters = [] - for sorter in sorters: - sortkey = sorter['key'] - sortdir = sorter['dir'] + 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: - log.warning("unknown sorter: %s", sorter) - continue + # 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) + # 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) - # add column/dir to collection - query_sorters.append(getattr(sortfunc._column, sortdir)()) + # invoke the sorter + data = sortfunc(data, sortdir) - # apply sorting to query - if query_sorters: - data = data.order_by(*query_sorters) - - return data - - # manual sorting; only one column allowed - if len(sorters) != 1: - raise NotImplementedError("mulit-column manual sorting not yet supported") - - # our one and only active sorter - sorter = sorters[0] - sortkey = sorter['key'] - sortdir = sorter['dir'] - - # cannot sort unless we have a sorter callable - sortfunc = self.sorters.get(sortkey) - if not sortfunc: - return data - - # apply joins needed for this sorter - # TODO: is this actually relevant for manual sort? - if sortkey in self.joiners and sortkey not in self.joined: - data = self.joiners[sortkey](data) - self.joined.add(sortkey) - - # invoke the sorter - return sortfunc(data, sortdir) + return data def paginate_data(self, data): """ diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index 3a12859e..8e3b7785 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -658,19 +658,19 @@ ## TODO: is there a better way to check if viewing parent? % if parent_instance is Undefined: % if master.editable and instance_editable and master.has_perm('edit'): - % endif - % if master.cloneable and master.has_perm('clone'): - % endif % if master.deletable and instance_deletable and master.has_perm('delete'): - @@ -679,7 +679,7 @@ % else: ## viewing row % if instance_deletable and master.has_perm('delete_row'): - @@ -688,13 +688,13 @@ % endif % elif master and master.editing: % if master.viewable and master.has_perm('view'): - % endif % if master.deletable and instance_deletable and master.has_perm('delete'): - @@ -702,13 +702,13 @@ % endif % elif master and master.deleting: % if master.viewable and master.has_perm('view'): - % endif % if master.editable and instance_editable and master.has_perm('edit'): - diff --git a/tailbone/templates/grids/complete.mako b/tailbone/templates/grids/complete.mako index 5a005c2e..8dc2d6dc 100644 --- a/tailbone/templates/grids/complete.mako +++ b/tailbone/templates/grids/complete.mako @@ -83,26 +83,29 @@ ## sorting % if grid.sortable: - ## nb. buefy only supports *one* default sorter - :default-sort="sorters.length ? [sorters[0].key, sorters[0].dir] : null" - - backend-sorting - @sort="onSort" - @sorting-priority-removed="sortingPriorityRemoved" - - ## TODO: there is a bug (?) which prevents the arrow from - ## displaying for simple default single-column sort. so to - ## work around that, we *disable* multi-sort until the - ## component is mounted. seems to work for now..see also - ## https://github.com/buefy/buefy/issues/2584 - :sort-multiple="allowMultiSort" - - - ## nb. otherwise there may be default multi-column sort - :sort-multiple-data="sortingPriority" - - ## user must ctrl-click column header to do multi-sort - sort-multiple-key="ctrlKey" + ## nb. buefy/oruga only support *one* default sorter + :default-sort="sorters.length ? [sorters[0].field, sorters[0].order] : null" + % if grid.sort_on_backend: + backend-sorting + @sort="onSort" + % endif + % if grid.sort_multiple: + % if grid.sort_on_backend: + ## TODO: there is a bug (?) which prevents the arrow + ## from displaying for simple default single-column sort, + ## when multi-column sort is allowed for the table. for + ## now we work around that by waiting until mount to + ## enable the multi-column support. see also + ## https://github.com/buefy/buefy/issues/2584 + :sort-multiple="allowMultiSort" + :sort-multiple-data="sortingPriority" + @sorting-priority-removed="sortingPriorityRemoved" + % else: + sort-multiple + % endif + ## nb. user must ctrl-click column header for multi-sort + sort-multiple-key="ctrlKey" + % endif % endif % if getattr(grid, 'click_handlers', None): @@ -276,23 +279,24 @@ ## sorting % if grid.sortable: - sorters: ${json.dumps(grid.active_sorters)|n}, - - ## TODO: there is a bug (?) which prevents the arrow from - ## displaying for simple default single-column sort. so to - ## work around that, we *disable* multi-sort until the - ## component is mounted. seems to work for now..see also - ## https://github.com/buefy/buefy/issues/2584 - allowMultiSort: false, - - ## nb. this will only contain multi-column sorters, - ## but will be *empty* for single-column sorting - % if len(grid.active_sorters) > 1: - sortingPriority: ${json.dumps(grid.active_sorters)|n}, - % else: - sortingPriority: [], + sorters: ${json.dumps(grid.get_vue_active_sorters())|n}, + % if grid.sort_multiple: + % if grid.sort_on_backend: + ## TODO: there is a bug (?) which prevents the arrow + ## from displaying for simple default single-column sort, + ## when multi-column sort is allowed for the table. for + ## now we work around that by waiting until mount to + ## enable the multi-column support. see also + ## https://github.com/buefy/buefy/issues/2584 + allowMultiSort: false, + ## nb. this should be empty when current sort is single-column + % if len(grid.active_sorters) > 1: + sortingPriority: ${json.dumps(grid.get_vue_active_sorters())|n}, + % else: + sortingPriority: [], + % endif + % endif % endif - % endif ## filterable: ${json.dumps(grid.filterable)|n}, @@ -395,14 +399,19 @@ }, }, - mounted() { - ## TODO: there is a bug (?) which prevents the arrow from - ## displaying for simple default single-column sort. so to - ## work around that, we *disable* multi-sort until the - ## component is mounted. seems to work for now..see also - ## https://github.com/buefy/buefy/issues/2584 - this.allowMultiSort = true - }, + % if grid.sortable and grid.sort_multiple and grid.sort_on_backend: + + ## TODO: there is a bug (?) which prevents the arrow + ## from displaying for simple default single-column sort, + ## when multi-column sort is allowed for the table. for + ## now we work around that by waiting until mount to + ## enable the multi-column support. see also + ## https://github.com/buefy/buefy/issues/2584 + mounted() { + this.allowMultiSort = true + }, + + % endif methods: { @@ -483,8 +492,8 @@ } % if grid.sortable and grid.sort_on_backend: for (let i = 1; i <= this.sorters.length; i++) { - params['sort'+i+'key'] = this.sorters[i-1].key - params['sort'+i+'dir'] = this.sorters[i-1].dir + params['sort'+i+'key'] = this.sorters[i-1].field + params['sort'+i+'dir'] = this.sorters[i-1].order } % endif return params @@ -597,48 +606,66 @@ }) }, - onSort(field, order, event) { + % if grid.sortable and grid.sort_on_backend: - ## nb. buefy passes field name; oruga passes field object - % if request.use_oruga: - field = field.field - % endif + onSort(field, order, event) { - if (event.ctrlKey) { + ## nb. buefy passes field name; oruga passes field object + % if request.use_oruga: + field = field.field + % endif - // engage or enhance multi-column sorting - const sorter = this.sorters.filter(s => s.key === field)[0] - if (sorter) { - sorter.dir = sorter.dir === 'desc' ? 'asc' : 'desc' - } else { - this.sorters.push({key: field, dir: order}) - } - this.sortingPriority = this.sorters + % if grid.sort_multiple: - } else { + // did user ctrl-click the column header? + if (event.ctrlKey) { + + // toggle direction for existing, or add new sorter + const sorter = this.sorters.filter(s => s.field === field)[0] + if (sorter) { + sorter.order = sorter.order === 'desc' ? 'asc' : 'desc' + } else { + this.sorters.push({field, order}) + } + + // apply multi-column sorting + this.sortingPriority = this.sorters + + } else { + + % endif // sort by single column only - this.sorters = [{key: field, dir: order}] - this.sortingPriority = [] - } + this.sorters = [{field, order}] - // always reset to first page when changing sort options - // TODO: i mean..right? would we ever not want that? - this.currentPage = 1 - this.loadAsyncData() - }, + % if grid.sort_multiple: + // multi-column sort not engaged + this.sortingPriority = [] + } + % endif - sortingPriorityRemoved(field) { + // nb. always reset to first page when sorting changes + this.currentPage = 1 + this.loadAsyncData() + }, - // prune field from active sorters - this.sorters = this.sorters.filter(s => s.key !== field) + % if grid.sort_multiple: - // nb. must keep active sorter list "as-is" even if - // there is only one sorter; buefy seems to expect it - this.sortingPriority = this.sorters + sortingPriorityRemoved(field) { - this.loadAsyncData() - }, + // prune from active sorters + this.sorters = this.sorters.filter(s => s.field !== field) + + // nb. even though we might have just one sorter + // now, we are still technically in multi-sort mode + this.sortingPriority = this.sorters + + this.loadAsyncData() + }, + + % endif + + % endif resetView() { this.loading = true diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 53f46020..dde72106 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -341,7 +341,7 @@ class MasterView(View): return self.redirect(self.request.current_route_url(**kw)) # Stash some grid stats, for possible use when generating URLs. - if grid.pageable and hasattr(grid, 'pager'): + if grid.paginated and hasattr(grid, 'pager'): self.first_visible_grid_index = grid.pager.first_item # return grid data only, if partial page was requested @@ -442,6 +442,7 @@ class MasterView(View): 'filterable': self.filterable, 'use_byte_string_filters': self.use_byte_string_filters, 'sortable': self.sortable, + 'sort_multiple': not self.request.use_oruga, 'paginated': self.pageable, 'extra_row_class': self.grid_extra_class, 'url': lambda obj: self.get_action_url('view', obj), diff --git a/tests/grids/test_core.py b/tests/grids/test_core.py index 9f9b816f..c621627a 100644 --- a/tests/grids/test_core.py +++ b/tests/grids/test_core.py @@ -388,14 +388,63 @@ class TestGrid(WebTestCase): grid.load_settings() self.assertEqual(grid.active_sorters, [{'key': 'name', 'dir': 'desc'}]) + def test_persist_settings(self): + model = self.app.model + + # nb. start out with paginated-only grid + grid = self.make_grid(key='foo', paginated=True, paginate_on_backend=True) + + # invalid dest + self.assertRaises(ValueError, grid.persist_settings, {}, dest='doesnotexist') + + # nb. no error if empty settings, but it saves null values + grid.persist_settings({}, dest='session') + self.assertIsNone(self.request.session['grid.foo.page']) + + # provided values are saved + grid.persist_settings({'pagesize': 15, 'page': 3}, dest='session') + self.assertEqual(self.request.session['grid.foo.page'], 3) + + # nb. now switch to sortable-only grid + grid = self.make_grid(key='settings', model_class=model.Setting, + sortable=True, sort_on_backend=True) + + # no error if empty settings; does not save values + grid.persist_settings({}, dest='session') + self.assertNotIn('grid.settings.sorters.length', self.request.session) + + # provided values are saved + grid.persist_settings({'sorters.length': 2, + 'sorters.1.key': 'name', + 'sorters.1.dir': 'desc', + 'sorters.2.key': 'value', + 'sorters.2.dir': 'asc'}, + dest='session') + self.assertEqual(self.request.session['grid.settings.sorters.length'], 2) + self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name') + self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc') + self.assertEqual(self.request.session['grid.settings.sorters.2.key'], 'value') + self.assertEqual(self.request.session['grid.settings.sorters.2.dir'], 'asc') + + # old values removed when new are saved + grid.persist_settings({'sorters.length': 1, + 'sorters.1.key': 'name', + 'sorters.1.dir': 'desc'}, + dest='session') + self.assertEqual(self.request.session['grid.settings.sorters.length'], 1) + self.assertEqual(self.request.session['grid.settings.sorters.1.key'], 'name') + self.assertEqual(self.request.session['grid.settings.sorters.1.dir'], 'desc') + self.assertNotIn('grid.settings.sorters.2.key', self.request.session) + self.assertNotIn('grid.settings.sorters.2.dir', self.request.session) + def test_sort_data(self): model = self.app.model sample_data = [ {'name': 'foo1', 'value': 'ONE'}, {'name': 'foo2', 'value': 'two'}, - {'name': 'foo3', 'value': 'three'}, - {'name': 'foo4', 'value': 'four'}, - {'name': 'foo5', 'value': 'five'}, + {'name': 'foo3', 'value': 'ggg'}, + {'name': 'foo4', 'value': 'ggg'}, + {'name': 'foo5', 'value': 'ggg'}, {'name': 'foo6', 'value': 'six'}, {'name': 'foo7', 'value': 'seven'}, {'name': 'foo8', 'value': 'eight'}, @@ -432,32 +481,30 @@ class TestGrid(WebTestCase): self.assertEqual(sorted_data[0]['name'], 'foo1') self.assertEqual(sorted_data[-1]['name'], 'foo9') - # error if mult-column sort attempted - self.assertRaises(NotImplementedError, grid.sort_data, sample_data, sorters=[ - {'key': 'name', 'dir': 'desc'}, - {'key': 'value', 'dir': 'asc'}, - ]) + # multi-column sorting for list data + sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'}, + {'key': 'name', 'dir': 'asc'}]) + self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'}) + self.assertEqual(dict(sorted_data[1]), {'name': 'foo3', 'value': 'ggg'}) + self.assertEqual(dict(sorted_data[3]), {'name': 'foo5', 'value': 'ggg'}) + self.assertEqual(dict(sorted_data[-1]), {'name': 'foo2', 'value': 'two'}) + + # multi-column sorting for query + sorted_query = grid.sort_data(sample_query, sorters=[{'key': 'value', 'dir': 'asc'}, + {'key': 'name', 'dir': 'asc'}]) + self.assertEqual(dict(sorted_data[0]), {'name': 'foo8', 'value': 'eight'}) + self.assertEqual(dict(sorted_data[1]), {'name': 'foo3', 'value': 'ggg'}) + self.assertEqual(dict(sorted_data[3]), {'name': 'foo5', 'value': 'ggg'}) + self.assertEqual(dict(sorted_data[-1]), {'name': 'foo2', 'value': 'two'}) # cannot sort data if sortfunc is missing for column grid.remove_sorter('name') - sorted_data = grid.sort_data(sample_data) + sorted_data = grid.sort_data(sample_data, sorters=[{'key': 'value', 'dir': 'asc'}, + {'key': 'name', 'dir': 'asc'}]) # nb. sorted data is in same order as original sample (not sorted) self.assertEqual(sorted_data[0]['name'], 'foo1') self.assertEqual(sorted_data[-1]['name'], 'foo9') - # cannot sort data if sortfunc is missing for column - grid.remove_sorter('name') - # nb. attempting multi-column sort, but only one sorter exists - self.assertEqual(list(grid.sorters), ['value']) - grid.active_sorters = [{'key': 'name', 'dir': 'asc'}, - {'key': 'value', 'dir': 'asc'}] - with patch.object(sample_query, 'order_by') as order_by: - order_by.return_value = 42 - sorted_query = grid.sort_data(sample_query) - order_by.assert_called_once() - self.assertEqual(len(order_by.call_args.args), 1) - self.assertEqual(sorted_query, 42) - def test_render_vue_tag(self): model = self.app.model From b7955a587179e4c7819a9d0a67a60be280e9c386 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 18 Aug 2024 19:58:50 -0500 Subject: [PATCH 051/142] =?UTF-8?q?bump:=20version=200.18.0=20=E2=86=92=20?= =?UTF-8?q?0.19.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 12 ++++++++++++ pyproject.toml | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0671e03b..72798b30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ 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.0 (2024-08-18) + +### Feat + +- move multi-column grid sorting logic to wuttaweb +- move single-column grid sorting logic to wuttaweb + +### Fix + +- fix misc. errors in grid template per wuttaweb +- fix broken permission directives in web api startup + ## v0.18.0 (2024-08-16) ### Feat diff --git a/pyproject.toml b/pyproject.toml index bd4882c6..1840de77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.18.0" +version = "0.19.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.9.0", + "WuttaWeb>=0.10.0", "zope.sqlalchemy>=1.5", ] From 0fb3c0f3d2dde74157b77d0313756151c1373317 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 19 Aug 2024 09:23:31 -0500 Subject: [PATCH 052/142] fix: fix broken user auth for web API app --- tailbone/api/auth.py | 12 +++++------ tailbone/app.py | 4 ---- tailbone/auth.py | 50 ++++++++++---------------------------------- 3 files changed, 16 insertions(+), 50 deletions(-) diff --git a/tailbone/api/auth.py b/tailbone/api/auth.py index 1b347b21..a710e30d 100644 --- a/tailbone/api/auth.py +++ b/tailbone/api/auth.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,8 +24,6 @@ Tailbone Web API - Auth Views """ -from rattail.db.auth import set_user_password - from cornice import Service from tailbone.api import APIView, api @@ -42,11 +40,10 @@ class AuthenticationView(APIView): This will establish a server-side web session for the user if none exists. Note that this also resets the user's session timer. """ - data = {'ok': True} + data = {'ok': True, 'permissions': []} if self.request.user: data['user'] = self.get_user_info(self.request.user) - - data['permissions'] = list(self.request.tailbone_cached_permissions) + data['permissions'] = list(self.request.user_permissions) # background color may be set per-request, by some apps if hasattr(self.request, 'background_color') and self.request.background_color: @@ -176,7 +173,8 @@ class AuthenticationView(APIView): return {'error': "The current/old password you provided is incorrect"} # okay then, set new password - set_user_password(self.request.user, data['new_password']) + auth = self.app.get_auth_handler() + auth.set_user_password(self.request.user, data['new_password']) return { 'ok': True, 'user': self.get_user_info(self.request.user), diff --git a/tailbone/app.py b/tailbone/app.py index 5e8e49d9..626c9206 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -25,19 +25,15 @@ Application Entry Point """ import os -import warnings -import sqlalchemy as sa from sqlalchemy.orm import sessionmaker, scoped_session from wuttjamaican.util import parse_list from rattail.config import make_config from rattail.exceptions import ConfigurationError -from rattail.db.types import GPCType from pyramid.config import Configurator -from pyramid.authentication import SessionAuthenticationPolicy from zope.sqlalchemy import register import tailbone.db diff --git a/tailbone/auth.py b/tailbone/auth.py index fbe6bf2f..95bf90ba 100644 --- a/tailbone/auth.py +++ b/tailbone/auth.py @@ -27,20 +27,18 @@ Authentication & Authorization import logging import re -from rattail.util import NOTSET +from wuttjamaican.util import UNSPECIFIED -from zope.interface import implementer -from pyramid.authentication import SessionAuthenticationHelper -from pyramid.request import RequestLocalCache from pyramid.security import remember, forget +from wuttaweb.auth import WuttaSecurityPolicy from tailbone.db import Session log = logging.getLogger(__name__) -def login_user(request, user, timeout=NOTSET): +def login_user(request, user, timeout=UNSPECIFIED): """ Perform the steps necessary to login the given user. Note that this returns a ``headers`` dict which you should pass to the redirect. @@ -49,7 +47,7 @@ def login_user(request, user, timeout=NOTSET): app = config.get_app() user.record_event(app.enum.USER_EVENT_LOGIN) headers = remember(request, user.uuid) - if timeout is NOTSET: + if timeout is UNSPECIFIED: timeout = session_timeout_for_user(config, user) log.debug("setting session timeout for '{}' to {}".format(user.username, timeout)) set_session_timeout(request, timeout) @@ -94,12 +92,12 @@ def set_session_timeout(request, timeout): request.session['_timeout'] = timeout or None -class TailboneSecurityPolicy: +class TailboneSecurityPolicy(WuttaSecurityPolicy): - def __init__(self, api_mode=False): + def __init__(self, db_session=None, api_mode=False, **kwargs): + kwargs['db_session'] = db_session or Session() + super().__init__(**kwargs) self.api_mode = api_mode - self.session_helper = SessionAuthenticationHelper() - self.identity_cache = RequestLocalCache(self.load_identity) def load_identity(self, request): config = request.registry.settings.get('rattail_config') @@ -115,7 +113,7 @@ class TailboneSecurityPolicy: if match: token = match.group(1) auth = app.get_auth_handler() - user = auth.authenticate_user_token(Session(), token) + user = auth.authenticate_user_token(self.db_session, token) if not user: @@ -126,36 +124,10 @@ class TailboneSecurityPolicy: # fetch user object from db model = app.model - user = Session.get(model.User, uuid) + user = self.db_session.get(model.User, uuid) if not user: return # this user is responsible for data changes in current request - Session().set_continuum_user(user) + self.db_session.set_continuum_user(user) return user - - def identity(self, request): - return self.identity_cache.get_or_create(request) - - def authenticated_userid(self, request): - user = self.identity(request) - if user is not None: - return user.uuid - - def remember(self, request, userid, **kw): - return self.session_helper.remember(request, userid, **kw) - - def forget(self, request, **kw): - return self.session_helper.forget(request, **kw) - - def permits(self, request, context, permission): - # nb. root user can do anything - if request.is_root: - return True - - config = request.registry.settings.get('rattail_config') - app = config.get_app() - auth = app.get_auth_handler() - - user = self.identity(request) - return auth.has_permission(Session(), user, permission) From b642c98d4091729ef8f957abb213d70c2c2e8fb8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 19 Aug 2024 09:23:55 -0500 Subject: [PATCH 053/142] =?UTF-8?q?bump:=20version=200.19.0=20=E2=86=92=20?= =?UTF-8?q?0.19.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 72798b30..ce64ec60 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.1 (2024-08-19) + +### Fix + +- fix broken user auth for web API app + ## v0.19.0 (2024-08-18) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 1840de77..fa33a2df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "Tailbone" -version = "0.19.0" +version = "0.19.1" description = "Backoffice Web Application for Rattail" readme = "README.rst" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] From 1d56a4c0d09d857f3d9276ac010743eceb8e2eac Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 19 Aug 2024 09:53:10 -0500 Subject: [PATCH 054/142] fix: replace all occurrences of `component_studly` => `vue_component` --- tailbone/grids/core.py | 2 +- tailbone/templates/batch/index.mako | 6 +++--- .../batch/inventory/desktop_form.mako | 4 ++-- tailbone/templates/batch/pos/view.mako | 2 +- .../templates/batch/vendorcatalog/create.mako | 12 +++++------ tailbone/templates/batch/view.mako | 18 ++++++++--------- tailbone/templates/customers/view.mako | 4 ++-- tailbone/templates/custorders/items/view.mako | 6 +++--- tailbone/templates/departments/view.mako | 2 +- tailbone/templates/importing/runjob.mako | 14 ++++++------- tailbone/templates/login.mako | 8 ++++---- tailbone/templates/master/form.mako | 2 +- tailbone/templates/people/index.mako | 16 +++++++-------- tailbone/templates/poser/reports/view.mako | 6 +++--- tailbone/templates/products/batch.mako | 20 +++++++++---------- tailbone/templates/products/index.mako | 8 ++++---- .../templates/purchases/credits/index.mako | 12 +++++------ tailbone/templates/receiving/view.mako | 8 ++++---- .../templates/reports/generated/delete.mako | 2 +- .../templates/reports/generated/view.mako | 2 +- tailbone/templates/reports/problems/view.mako | 2 +- tailbone/templates/roles/view.mako | 2 +- tailbone/templates/settings/email/index.mako | 8 ++++---- .../templates/tempmon/appliances/view.mako | 2 +- tailbone/templates/tempmon/clients/view.mako | 2 +- .../trainwreck/transactions/view.mako | 2 +- .../trainwreck/transactions/view_row.mako | 2 +- tailbone/templates/users/view.mako | 2 +- 28 files changed, 88 insertions(+), 88 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 9c445fec..d00a85ae 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -1697,7 +1697,7 @@ class Grid(WuttaGrid): results['checked_rows'] = checked # TODO: this seems a bit hacky, but is required for now to # initialize things on the client side... - var = '{}CurrentData'.format(self.component_studly) + var = '{}CurrentData'.format(self.vue_component) results['checked_rows_code'] = '[{}]'.format( ', '.join(['{}[{}]'.format(var, i) for i in checked])) diff --git a/tailbone/templates/batch/index.mako b/tailbone/templates/batch/index.mako index 209fbb0c..a7808590 100644 --- a/tailbone/templates/batch/index.mako +++ b/tailbone/templates/batch/index.mako @@ -83,7 +83,7 @@ % if master.results_executable and master.has_perm('execute_multiple'): % endif diff --git a/tailbone/templates/batch/inventory/desktop_form.mako b/tailbone/templates/batch/inventory/desktop_form.mako index 7e4795a8..8ca32ce0 100644 --- a/tailbone/templates/batch/inventory/desktop_form.mako +++ b/tailbone/templates/batch/inventory/desktop_form.mako @@ -147,7 +147,7 @@ diff --git a/tailbone/templates/batch/vendorcatalog/create.mako b/tailbone/templates/batch/vendorcatalog/create.mako index d25c8f16..63865bd5 100644 --- a/tailbone/templates/batch/vendorcatalog/create.mako +++ b/tailbone/templates/batch/vendorcatalog/create.mako @@ -5,12 +5,12 @@ ${parent.modify_this_page_vars()} % endif @@ -356,8 +356,8 @@ % endif diff --git a/tailbone/templates/customers/view.mako b/tailbone/templates/customers/view.mako index 8b07bdb3..bbca9580 100644 --- a/tailbone/templates/customers/view.mako +++ b/tailbone/templates/customers/view.mako @@ -21,10 +21,10 @@ diff --git a/tailbone/templates/importing/runjob.mako b/tailbone/templates/importing/runjob.mako index 2bc2a4e9..23526ed2 100644 --- a/tailbone/templates/importing/runjob.mako +++ b/tailbone/templates/importing/runjob.mako @@ -67,21 +67,21 @@ ${parent.modify_this_page_vars()} diff --git a/tailbone/templates/login.mako b/tailbone/templates/login.mako index d18323b5..f898660f 100644 --- a/tailbone/templates/login.mako +++ b/tailbone/templates/login.mako @@ -60,19 +60,19 @@ <%def name="modify_this_page_vars()"> % endif diff --git a/tailbone/templates/products/batch.mako b/tailbone/templates/products/batch.mako index a4a4d503..66e38028 100644 --- a/tailbone/templates/products/batch.mako +++ b/tailbone/templates/products/batch.mako @@ -22,7 +22,7 @@ <%def name="render_form_innards()"> - ${h.form(request.current_route_url(), **{'@submit': 'submit{}'.format(form.component_studly)})} + ${h.form(request.current_route_url(), **{'@submit': 'submit{}'.format(form.vue_component)})} ${h.csrf_token(request)}
@@ -43,8 +43,8 @@
- {{ ${form.component_studly}ButtonText }} + :disabled="${form.vue_component}Submitting"> + {{ ${form.vue_component}ButtonText }} Cancel @@ -66,21 +66,21 @@ ## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako) - let ${form.component_studly} = { + let ${form.vue_component} = { template: '#${form.component}-template', methods: { ## TODO: deprecate / remove the latter option here % if form.auto_disable_save or form.auto_disable: - submit${form.component_studly}() { - this.${form.component_studly}Submitting = true - this.${form.component_studly}ButtonText = "Working, please wait..." + submit${form.vue_component}() { + this.${form.vue_component}Submitting = true + this.${form.vue_component}ButtonText = "Working, please wait..." } % endif } } - let ${form.component_studly}Data = { + let ${form.vue_component}Data = { ## TODO: ugh, this seems pretty hacky. need to declare some data models ## for various field components to bind to... @@ -95,8 +95,8 @@ ## TODO: deprecate / remove the latter option here % if form.auto_disable_save or form.auto_disable: - ${form.component_studly}Submitting: false, - ${form.component_studly}ButtonText: ${json.dumps(getattr(form, 'submit_label', getattr(form, 'save_label', "Submit")))|n}, + ${form.vue_component}Submitting: false, + ${form.vue_component}ButtonText: ${json.dumps(getattr(form, 'submit_label', getattr(form, 'save_label', "Submit")))|n}, % endif ## TODO: more hackiness, this is for the sake of batch params diff --git a/tailbone/templates/products/index.mako b/tailbone/templates/products/index.mako index 0d4bc410..b4731dee 100644 --- a/tailbone/templates/products/index.mako +++ b/tailbone/templates/products/index.mako @@ -41,11 +41,11 @@ % if label_profiles and master.has_perm('print_labels'): diff --git a/tailbone/templates/reports/generated/view.mako b/tailbone/templates/reports/generated/view.mako index 6260efba..e5bcc9e4 100644 --- a/tailbone/templates/reports/generated/view.mako +++ b/tailbone/templates/reports/generated/view.mako @@ -28,7 +28,7 @@ diff --git a/tailbone/templates/reports/problems/view.mako b/tailbone/templates/reports/problems/view.mako index 026c73dc..1d5cb14f 100644 --- a/tailbone/templates/reports/problems/view.mako +++ b/tailbone/templates/reports/problems/view.mako @@ -67,7 +67,7 @@ diff --git a/tailbone/templates/tempmon/clients/view.mako b/tailbone/templates/tempmon/clients/view.mako index cff22fed..b1db423b 100644 --- a/tailbone/templates/tempmon/clients/view.mako +++ b/tailbone/templates/tempmon/clients/view.mako @@ -26,7 +26,7 @@ ${parent.modify_this_page_vars()} diff --git a/tailbone/templates/trainwreck/transactions/view.mako b/tailbone/templates/trainwreck/transactions/view.mako index 2be51c7d..02950941 100644 --- a/tailbone/templates/trainwreck/transactions/view.mako +++ b/tailbone/templates/trainwreck/transactions/view.mako @@ -6,7 +6,7 @@ diff --git a/tailbone/templates/trainwreck/transactions/view_row.mako b/tailbone/templates/trainwreck/transactions/view_row.mako index 9abcb8ba..9c76f7bd 100644 --- a/tailbone/templates/trainwreck/transactions/view_row.mako +++ b/tailbone/templates/trainwreck/transactions/view_row.mako @@ -6,7 +6,7 @@ diff --git a/tailbone/templates/users/view.mako b/tailbone/templates/users/view.mako index ed2b5f16..06087927 100644 --- a/tailbone/templates/users/view.mako +++ b/tailbone/templates/users/view.mako @@ -81,7 +81,7 @@ % if master.has_perm('manage_api_tokens'): + % if getattr(master, 'has_rows', False): + ${rows_grid.render_vue_finalize()} + % endif + % if expose_versions: + ${versions_grid.render_vue_finalize()} + % endif 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 Date: Mon, 19 Aug 2024 13:57:36 -0500 Subject: [PATCH 059/142] =?UTF-8?q?bump:=20version=200.19.2=20=E2=86=92=20?= =?UTF-8?q?0.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 Date: Mon, 19 Aug 2024 14:38:41 -0500 Subject: [PATCH 060/142] 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 Date: Mon, 19 Aug 2024 21:30:58 -0500 Subject: [PATCH 061/142] 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 @@
-<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - - - -${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 @@ - ${declare_formposter_mixin()} - - ${self.body()} - -
+
- ${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()} @@ -181,7 +185,7 @@ <%def name="head_tags()"> -<%def name="render_whole_page_template()"> +<%def name="render_vue_template_whole_page()"> -<%def name="modify_whole_page_vars()"> - - - -<%def name="finalize_whole_page_vars()"> - ## NOTE: if you override this, must use - - -<%def name="make_whole_page_app()"> - - - <%def name="wtfield(form, name, **kwargs)">
@@ -961,3 +911,87 @@
+ +############################## +## 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()} + + +## DEPRECATED; remains for back-compat +<%def name="render_whole_page_template()"> + ${self.render_vue_template_whole_page()} + ${self.declare_whole_page_vars()} + + +## DEPRECATED; remains for back-compat +<%def name="declare_whole_page_vars()"> + ${self.render_vue_script_whole_page()} + + +<%def name="modify_vue_vars()"> + ## DEPRECATED; called for back-compat + ${self.modify_whole_page_vars()} + + +## DEPRECATED; remains for back-compat +<%def name="modify_whole_page_vars()"> + + + +<%def name="make_vue_components()"> + ${make_grid_filter_components()} + ${page_help.make_component()} + ${multi_file_upload.make_component()} + + + ## DEPRECATED; called for back-compat + ${self.finalize_whole_page_vars()} + ${self.make_whole_page_component()} + + +## DEPRECATED; remains for back-compat +<%def name="make_whole_page_component()"> + + + +<%def name="make_vue_app()"> + ## DEPRECATED; called for back-compat + ${self.make_whole_page_app()} + + +## DEPRECATED; remains for back-compat +<%def name="make_whole_page_app()"> + + + +############################## +## DEPRECATED +############################## + +<%def name="finalize_whole_page_vars()"> 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 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 name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.results_refreshable and master.has_perm('refresh'): - % endif % if master.results_executable and master.has_perm('execute_multiple'): - % endif - -<%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 - - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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()} - - -${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 @@
-<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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()} - - - -${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 @@ -<%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 name="render_this_page()"> ${parent.render_this_page()} @@ -197,16 +191,6 @@ -<%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 name="render_form()">
<${form.component} @show-upload="showUploadDialog = true"> @@ -267,9 +251,27 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - -<%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'): - % endif - % if execute_enabled and master.has_perm('execute'): - % endif - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${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 @@
-<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${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 @@
-<%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()} - - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - ${product_lookup.tailbone_product_lookup_component()} - - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + ${product_lookup.tailbone_product_lookup_component()} + 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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%def name="modify_this_page_vars()"> - - - -${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()} - - -${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 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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if main_form_collapsible: % endif - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@
-<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${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 @@
-<%def name="modify_this_page_vars()"> - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@
-<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} - - -${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()} - -<%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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + 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 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + 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 name="make_this_page_component()"> - ${parent.make_this_page_component()} - - - -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - - ## TODO: stop using |n filter - ${grid.render_complete()|n} - - <%def name="page_content()"> - - + ${grid.render_vue_tag(**{':csrftoken': 'csrftoken'})} -${parent.body()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + ${grid.render_vue_template()} + + +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + ${grid.render_vue_finalize()} + 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 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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - % if expose_versions: - - % endif - - -<%def name="modify_whole_page_vars()"> - ${parent.modify_whole_page_vars()} - -<%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 - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} ${message_recipients_template()} -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 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'): - % endif - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 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'): % endif - - -${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 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} - +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - - -############################## -## page body -############################## - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + 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: -
  • ${item}
  • - % endfor - % endif +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} + ${self.render_vue_template_this_page()} -<%def name="page_content()"> - -<%def name="render_this_page()"> -
    - -
    - ${self.page_content()} -
    - -
      - ${self.context_menu_items()} -
    - -
    +<%def name="render_vue_template_this_page()"> + ## DEPRECATED; called for back-compat + ${self.render_this_page_template()} <%def name="render_this_page_template()"> - + -<%def name="modify_this_page_vars()"> - ## NOTE: if you override this, must use +############################## +## DEPRECATED +############################## -${self.render_this_page_template()} -${self.make_this_page_component()} +<%def name="declare_this_page_vars()"> + +<%def name="modify_this_page_vars()"> + +<%def name="finalize_this_page_vars()"> 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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${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 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'): - % endif - -${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 name="object_helpers()"> ${parent.object_helpers()} ${view_profiles_helper([instance])} @@ -13,9 +23,9 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -<%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 - - - -${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 @@ - + <%def name="declare_personal_tab_vars()"> @@ -3022,114 +3089,46 @@ -<%def name="declare_profile_info_vars()"> - - - <%def name="make_profile_info_component()"> - ${self.declare_profile_info_vars()} - -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -<%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: - - ${self.make_transactions_tab_component()} - % endif - - ${self.make_user_tab_component()} - ${self.make_profile_info_component()} - - -<%def name="modify_whole_page_vars()"> - ${parent.modify_whole_page_vars()} - - % if request.has_perm('people_profile.view_versions'): - - % endif + % endif + +<%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: + + ${self.make_transactions_tab_component()} + % endif + + ${self.make_user_tab_component()} + ${self.make_profile_info_component()} + + +############################## +## DEPRECATED +############################## + +<%def name="declare_profile_info_vars()"> 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 @@
    -<%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'): - + % endif - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="principal_table()"> +
    + ${grid.render_table_element(data_prop='principalsData')|n} +
    + + +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} - - -<%def name="principal_table()"> -
    - ${grid.render_table_element(data_prop='principalsData')|n} -
    - - -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} + + -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + 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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%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'): - % endif - - -${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 name="page_content()"> ${parent.page_content()} @@ -67,9 +62,14 @@ % endif -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - -<%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()} - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 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 name="render_vue_templates()"> + ${parent.render_vue_templates()} % if allow_edit_catalog_unit_cost or allow_edit_invoice_unit_cost: - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} +<%def name="modify_vue_vars()"> + ${parent.modify_vue_vars()} % if master.has_perm('configure'): - % endif - -${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 @@ -<%def name="render_this_page_template()"> - ${parent.render_this_page_template()} +<%def name="render_vue_templates()"> + ${parent.render_vue_templates()} - - -<%def name="make_this_page_component()"> - ${parent.make_this_page_component()} - -${parent.body()} +<%def name="make_vue_components()"> + ${parent.make_vue_components()} + + 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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -
    +
    ## 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()} @@ -596,7 +579,7 @@ -<%def name="render_whole_page_template()"> +<%def name="render_vue_template_whole_page()"> - -## ${multi_file_upload.render_template()} <%def name="render_this_page_component()"> @@ -1068,9 +1049,7 @@ % endif -<%def name="declare_whole_page_vars()"> -## ${multi_file_upload.declare_vars()} - +<%def name="render_vue_script_whole_page()"> -<%def name="modify_whole_page_vars()"> +############################## +## vue components + app +############################## -## TODO: do we really need this? -## <%def name="finalize_whole_page_vars()"> +<%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()} + + +## DEPRECATED; remains for back-compat +<%def name="render_whole_page_template()"> + ${self.render_vue_template_whole_page()} + ${self.render_vue_script_whole_page()} + + +<%def name="modify_vue_vars()"> + ## DEPRECATED; called for back-compat ${self.modify_whole_page_vars()} -## ${self.finalize_whole_page_vars()} + +<%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()} + + +## DEPRECATED; remains for back-compat +<%def name="make_whole_page_component()"> <% request.register_component('whole-page', 'WholePage') %> +<%def name="make_vue_app()"> + ## DEPRECATED; called for back-compat + ${self.make_whole_page_app()} + + +## DEPRECATED; remains for back-compat <%def name="make_whole_page_app()"> + +############################## +## DEPRECATED +############################## + +<%def name="declare_whole_page_vars()"> + +<%def name="modify_whole_page_vars()"> 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 @@
    -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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()} - - -${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()} - - - -${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 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'): - + % endif - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 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'): - % endif - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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): -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${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 @@ -<%def name="modify_this_page_vars()"> - ${parent.modify_this_page_vars()} - - - -${parent.body()} From 59bd58aca768f9e18a1e3db7447a576c48d29191 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 20 Aug 2024 13:46:40 -0500 Subject: [PATCH 062/142] 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()">
    @@ -108,7 +108,7 @@
    - ${parent.render_grid_component()} + ${grid.render_vue_tag()}
    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 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 @@
    - <${execute_form.component} ref="executeResultsForm"> + ${execute_form.render_vue_tag(ref='executeResultsForm')}
    @@ -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 @@ -128,9 +128,6 @@ <%def name="make_vue_components()"> ${parent.make_vue_components()} % if master.results_executable and master.has_perm('execute_multiple'): - + ${execute_form.render_vue_finalize()} % endif 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 @@
    ${execution_described|n}
    - <${execute_form.component} ref="executeBatchForm"> - + ${execute_form.render_vue_tag(ref='executeBatchForm')}