Compare commits

...

138 commits

Author SHA1 Message Date
e150453801 fix: add startup hack for tempmon DB model 2025-03-05 10:34:52 -06:00
e2582ffec5 bump: version 0.22.6 → 0.22.7 2025-02-19 10:33:39 -06:00
a6508154cb docs: update intersphinx doc links per server migration 2025-02-18 12:13:28 -06:00
7348eec671 fix: stop using old config for logo image url on login page 2025-02-18 11:16:23 -06:00
4221fa50dd fix: fix warning msg for deprecated Grid param 2025-02-14 11:37:21 -06:00
e0ebd43e7a bump: version 0.22.5 → 0.22.6 2025-02-01 15:18:12 -06:00
c7ee9de9eb fix: register vue3 form component for products -> make batch 2024-12-28 16:43:22 -06:00
950db697a0 bump: version 0.22.4 → 0.22.5 2024-12-16 12:46:45 -06:00
358b3b75a5 fix: whoops this is latest rattail 2024-12-10 13:05:32 -06:00
7e559a01b3 fix: require newer rattail lib 2024-12-10 12:52:49 -06:00
23bdde245a fix: require newer wuttaweb 2024-12-10 12:34:34 -06:00
2c269b640b fix: let caller request safe HTML literal for rendered grid table
mostly just for convenience
2024-12-01 18:12:30 -06:00
Lance Edgar
f1c8ffedda bump: version 0.22.3 → 0.22.4 2024-11-22 12:57:04 -06:00
Lance Edgar
aace6033c5 fix: avoid error in product search for duplicated key 2024-11-20 20:17:21 -06:00
Lance Edgar
7171c7fb06 fix: use vmodel for confirm password widget input
since previously this did not work at all for butterball (vue3 +
oruga) - although it was never clear why per se..

Refs: #1
2024-11-19 20:53:23 -06:00
Lance Edgar
993f066f2c bump: version 0.22.2 → 0.22.3 2024-11-19 15:45:37 -06:00
Lance Edgar
980031f524 fix: avoid error for trainwreck query when not a customer
when viewing a person's profile, who does not have a customer record,
the trainwreck query can't really return anything since it normally
should be matching on the customer ID
2024-11-18 14:59:50 -06:00
Lance Edgar
bcaf0d08bc bump: version 0.22.1 → 0.22.2 2024-11-18 14:08:10 -06:00
Lance Edgar
ac439c949b fix: use local/custom enum for continuum operations
since we can't rely on that existing in rattail proper, due to it not
always having sqlalchemy
2024-11-12 19:45:24 -06:00
Lance Edgar
20b3f87dbe fix: add basic master view for Product Costs 2024-11-12 18:30:50 -06:00
Lance Edgar
9e55717041 fix: show continuum operation type when viewing version history 2024-11-12 18:28:41 -06:00
Lance Edgar
772b6610cb fix: always define app attr for ViewSupplement 2024-11-12 18:26:36 -06:00
Lance Edgar
3f27f626df fix: avoid deprecated import 2024-11-10 19:16:45 -06:00
Lance Edgar
29743e70b7 bump: version 0.22.0 → 0.22.1 2024-11-02 16:56:28 -05:00
Lance Edgar
54220601ed fix: fix submit button for running problem report
esp. on Chrome(-based) browsers
2024-11-01 17:47:46 -05:00
Lance Edgar
9a6f8970ae fix: avoid deprecated grid method 2024-10-23 09:46:14 -05:00
Lance Edgar
28f90ad6b5 bump: version 0.21.11 → 0.22.0 2024-10-22 17:09:29 -05:00
Lance Edgar
535317e4f7 fix: avoid deprecated method to suggest username 2024-10-22 15:04:40 -05:00
Lance Edgar
072db39233 feat: add support for new ordering batch from parsed file 2024-10-22 14:26:10 -05:00
Lance Edgar
c6365f2631 bump: version 0.21.10 → 0.21.11 2024-10-03 09:05:46 -05:00
Lance Edgar
d520f64fee fix: custom method for adding grid action
since for now, we are using custom grid action class
2024-10-03 08:56:52 -05:00
Lance Edgar
2308d2e240 fix: become/stop root should redirect to previous url
for default theme; butterball already did that
2024-09-16 12:55:58 -05:00
Lance Edgar
0b4efae392 bump: version 0.21.9 → 0.21.10 2024-09-15 10:56:01 -05:00
Lance Edgar
0b646d2d18 fix: update project repo links, kallithea -> forgejo 2024-09-14 12:49:37 -05:00
Lance Edgar
a4d81a6e3c docs: use markdown for readme file 2024-09-13 18:16:07 -05:00
Lance Edgar
5e742eab17 fix: use better icon for submit button on login page 2024-09-09 08:32:28 -05:00
Lance Edgar
b9b8bbd2ea fix: wrap notes text for batch view 2024-08-29 17:18:32 -05:00
Lance Edgar
8df52bf2a2 fix: expose datasync consumer batch size via configure page 2024-08-29 17:01:49 -05:00
Lance Edgar
55f45ae8a0 bump: version 0.21.8 → 0.21.9 2024-08-28 17:38:33 -05:00
Lance Edgar
2219cf8198 fix: render custom attrs in form component tag 2024-08-28 17:38:05 -05:00
Lance Edgar
9be2f63475 bump: version 0.21.7 → 0.21.8 2024-08-28 14:37:40 -05:00
Lance Edgar
812d8d2349 fix: ignore session kwarg for MasterView.make_row_grid() 2024-08-28 14:37:18 -05:00
Lance Edgar
20dcdd8b86 bump: version 0.21.6 → 0.21.7 2024-08-28 14:20:51 -05:00
Lance Edgar
bc399182ba fix: avoid error when form value cannot be obtained 2024-08-28 14:20:17 -05:00
Lance Edgar
71d63f6b93 bump: version 0.21.5 → 0.21.6 2024-08-28 09:53:37 -05:00
Lance Edgar
0b6cfaa9c5 fix: avoid error when grid value cannot be obtained 2024-08-28 09:53:14 -05:00
Lance Edgar
b81914fbf5 test: fix broken test 2024-08-28 00:35:15 -05:00
Lance Edgar
b30f066c41 bump: version 0.21.4 → 0.21.5 2024-08-28 00:30:15 -05:00
Lance Edgar
2e20fc5b75 fix: set empty string for "-new-" file configure option
otherwise the "-new-" option is not properly auto-selected
2024-08-27 13:50:30 -05:00
Lance Edgar
ca05e68890 bump: version 0.21.3 → 0.21.4 2024-08-26 16:12:14 -05:00
Lance Edgar
7a9d5772db fix: handle differing email profile keys for appinfo/configure
hopefully this all can improve some day soon..
2024-08-26 16:11:32 -05:00
Lance Edgar
dffd951369 bump: version 0.21.2 → 0.21.3 2024-08-26 15:25:56 -05:00
Lance Edgar
d67eb2f1cc fix: show non-standard config values for app info configure email
this page is currently showing some basic email sender/recips etc. but
the config keys traditionally used by rattail are different than
wuttjamaican..so for now we must "translate"
2024-08-26 15:24:40 -05:00
Lance Edgar
3a9bf69aa7 bump: version 0.21.1 → 0.21.2 2024-08-26 14:56:15 -05:00
Lance Edgar
d1f4c0f150 fix: refactor waterpark base template to use wutta feedback component
although for now we still provide the template and add reply-to
2024-08-26 14:54:45 -05:00
Lance Edgar
b7991b5dc6 fix: fix input/output file upload feature for configure pages, per oruga 2024-08-23 16:18:17 -05:00
Lance Edgar
c1a2c9cc70 fix: tweak how grid data translates to Vue template context
per wuttaweb changes
2024-08-23 14:14:17 -05:00
Lance Edgar
37f760959d fix: merge filters into main grid template
to better match wuttaweb
2024-08-22 19:58:27 -05:00
Lance Edgar
cea3e4b927 fix: add basic wutta view for users
just proving concepts still at this point..nothing reliable
2024-08-22 19:40:21 -05:00
Lance Edgar
29531c83c4 fix: some fixes for wutta people view 2024-08-22 19:21:48 -05:00
Lance Edgar
4c3e3aeb6a fix: various fixes for waterpark theme 2024-08-22 17:09:58 -05:00
Lance Edgar
c176d97870 fix: avoid deprecated component form kwarg 2024-08-22 15:54:15 -05:00
Lance Edgar
7d6f75bb05 bump: version 0.21.0 → 0.21.1 2024-08-22 15:33:28 -05:00
Lance Edgar
7b40c527c8 fix: misc. bugfixes per recent changes 2024-08-22 15:31:09 -05:00
Lance Edgar
f292850d05 test: fix some tests 2024-08-22 14:59:18 -05:00
Lance Edgar
8d5427e92f bump: version 0.20.1 → 0.21.0 2024-08-22 14:53:59 -05:00
Lance Edgar
b8131c8393 fix: change grid reset-view param name to match wuttaweb 2024-08-22 13:49:57 -05:00
Lance Edgar
e52a83751e feat: move "most" filtering logic for grid class to wuttaweb
we still define all filters, and the "most important" grid methods for
filtering
2024-08-21 20:16:03 -05:00
Lance Edgar
ffa724ef37 fix: move "searchable columns" grid feature to wuttaweb 2024-08-21 15:52:30 -05:00
Lance Edgar
1d00fe994a fix: use wuttaweb to get/render csrf token 2024-08-21 09:44:32 -05:00
Lance Edgar
71abbe06da feat: inherit from wuttaweb templates for home, login pages 2024-08-21 00:49:26 -05:00
Lance Edgar
f755460242 feat: inherit from wuttaweb for AppInfoView, appinfo/configure template 2024-08-21 00:49:23 -05:00
Lance Edgar
2ffc067097 fix: inherit from wuttaweb for appinfo/index template
although for now, still must override for some link buttons
2024-08-20 22:27:46 -05:00
Lance Edgar
b6a8e508bf fix: prefer wuttaweb config for "home redirect to login" feature 2024-08-20 22:16:01 -05:00
Lance Edgar
1def26a35b feat: add "has output file templates" config option for master view
this is a bit hacky, a quick copy/paste job from the equivalent
feature for input file templates.

i assume this will get cleaned up when moved to wuttaweb..
2024-08-20 19:09:56 -05:00
Lance Edgar
07871188aa fix: fix master/index template rendering for waterpark theme 2024-08-20 17:03:57 -05:00
Lance Edgar
c8dc60cb68 fix: fix spacing for navbar logo/title in waterpark theme 2024-08-20 16:49:34 -05:00
Lance Edgar
526c84dfa6 bump: version 0.20.0 → 0.20.1 2024-08-20 16:05:52 -05:00
Lance Edgar
21f90f3f32 fix: fix default filter verbs logic for workorder status 2024-08-20 16:02:35 -05:00
Lance Edgar
83586ef90f bump: version 0.19.3 → 0.20.0 2024-08-20 15:06:09 -05:00
Lance Edgar
59bd58aca7 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
2024-08-20 15:03:25 -05:00
Lance Edgar
1ec1eba496 feat: refactor templates to simplify base/page/form structure
to mimic what has been done in wuttaweb
2024-08-19 23:20:59 -05:00
Lance Edgar
d29b840343 fix: avoid deprecated reference to app db engine 2024-08-19 14:38:41 -05:00
Lance Edgar
b762a0782a bump: version 0.19.2 → 0.19.3 2024-08-19 13:57:36 -05:00
Lance Edgar
15ab0c9592 fix: add pager stats to all grid vue data (fixes view history)
also various other tweaks to modernize
2024-08-19 13:48:18 -05:00
Lance Edgar
41945c5e37 bump: version 0.19.1 → 0.19.2 2024-08-19 12:01:42 -05:00
Lance Edgar
f5661fe349 fix: sort on frontend for appinfo package listing grid 2024-08-19 11:56:46 -05:00
Lance Edgar
0eeeb4bd35 fix: prefer attr over key lookup when getting model values
applies to both forms and grids.

the base model class can still handle `obj[key]` but now it is limited
to the column fields only, no association proxies.

so, better to just try `getattr(obj, key)` first and only fall back to
the other if it fails.

unless the obj is clearly a dict in which case try `obj[key]` only
2024-08-19 11:49:52 -05:00
Lance Edgar
1d56a4c0d0 fix: replace all occurrences of component_studly => vue_component 2024-08-19 09:53:10 -05:00
Lance Edgar
b642c98d40 bump: version 0.19.0 → 0.19.1 2024-08-19 09:23:55 -05:00
Lance Edgar
0fb3c0f3d2 fix: fix broken user auth for web API app 2024-08-19 09:23:31 -05:00
Lance Edgar
b7955a5871 bump: version 0.18.0 → 0.19.0 2024-08-18 19:58:50 -05:00
Lance Edgar
290f8fd51e 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..
2024-08-18 19:52:21 -05:00
Lance Edgar
ec36df4a34 feat: move single-column grid sorting logic to wuttaweb 2024-08-18 14:05:52 -05:00
Lance Edgar
c95e42bf82 fix: fix misc. errors in grid template per wuttaweb 2024-08-18 10:20:48 -05:00
Lance Edgar
5e82fe3946 fix: fix broken permission directives in web api startup 2024-08-18 10:20:48 -05:00
Lance Edgar
f4c8176d83 bump: version 0.17.0 → 0.18.0 2024-08-16 22:54:22 -05:00
Lance Edgar
9da2a148c6 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.
2024-08-16 18:45:04 -05:00
Lance Edgar
2a0b6da2f9 feat: inherit from wutta base class for Grid 2024-08-16 14:34:50 -05:00
Lance Edgar
f7641218cb fix: avoid route error in user view, when using wutta people view
kind of a temporary edge case here, can eventually change it back
2024-08-16 11:56:54 -05:00
Lance Edgar
1b78bd617c feat: inherit most logic from wuttaweb, for GridAction 2024-08-16 11:56:12 -05:00
Lance Edgar
09612b1921 fix: fix some more wutta compat for base template
missed those earlier
2024-08-15 23:46:58 -05:00
Lance Edgar
bbd98e7b2f bump: version 0.16.1 → 0.17.0 2024-08-15 23:15:25 -05:00
Lance Edgar
da0f6bd5e1 feat: use wuttaweb for get_liburl() logic
thankfully this is already handled and we can remove from tailbone.
although this adds some new cruft as well, to handle auto-migrating
any existing liburl config for apps.

eventually once all apps have migrated to new settings we can remove
the prefix from our calls here but also in wuttaweb signature
2024-08-15 23:12:02 -05:00
Lance Edgar
bbc2c584ec bump: version 0.16.0 → 0.16.1 2024-08-15 21:16:53 -05:00
Lance Edgar
7f0c571a44 fix: improve wutta People view a bit
try to behave more like traditional tailbone, for the few things
supported so far.  taking a conservative approach here for now since
probably other things are more pressing.
2024-08-15 21:12:34 -05:00
Lance Edgar
53040dc6be fix: update references to get_class_hierarchy()
per upstream changes
2024-08-15 20:29:36 -05:00
Lance Edgar
1cacfab2a6 fix: tweak template for people/view_profile per wutta compat
wutta has the view defined but it returns minimal context
2024-08-15 18:44:14 -05:00
Lance Edgar
bab09e3fe7 bump: version 0.15.6 → 0.16.0 2024-08-15 16:22:35 -05:00
Lance Edgar
dd176a5e9e feat: add first wutta-based master, for PersonView
still opt-in-only at this point, the traditional tailbone-native
master is used by default.

new wutta master is not feature complete yet.  but at least things
seem to render and form posts work okay..

when enabled, this uses a "completely" wutta-based stack for the view,
grid and forms.  but the underlying DB is of course rattail, and the
templates are still traditional/tailbone.
2024-08-15 16:05:53 -05:00
Lance Edgar
a6ce5eb21d 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.
2024-08-15 15:49:54 -05:00
Lance Edgar
b53479f8e4 bump: version 0.15.5 → 0.15.6 2024-08-13 11:21:38 -05:00
Lance Edgar
1f752530d2 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
2024-08-10 13:49:41 -05:00
Lance Edgar
2c46fde742 fix: simplify verbiage for batch execution panel 2024-08-10 08:43:54 -05:00
Lance Edgar
d57efba381 bump: version 0.15.4 → 0.15.5 2024-08-09 19:48:51 -05:00
Lance Edgar
f2fce2e305 fix: assign convenience attrs for all views (config, app, enum, model) 2024-08-09 19:22:26 -05:00
Lance Edgar
b5f0ecb165 bump: version 0.15.3 → 0.15.4 2024-08-09 10:13:00 -05:00
Lance Edgar
7e683dfc4a 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
2024-08-09 10:11:38 -05:00
Lance Edgar
0b8315fc78 bump: version 0.15.2 → 0.15.3 2024-08-08 19:39:36 -05:00
Lance Edgar
ffd694e7b7 fix: fix timepicker parseTime() when value is null 2024-08-08 19:39:01 -05:00
Lance Edgar
80dc4eb7a9 bump: version 0.15.1 → 0.15.2 2024-08-06 23:19:14 -05:00
Lance Edgar
518c108c88 fix: use auth handler, avoid legacy calls for role/perm checks 2024-08-06 10:36:20 -05:00
Lance Edgar
bd1993f440 bump: version 0.15.0 → 0.15.1 2024-08-05 22:57:02 -05:00
Lance Edgar
91ea9021d7 fix: move magic b template context var to wuttaweb 2024-08-05 21:50:22 -05:00
Lance Edgar
2903b376b5 bump: version 0.14.5 → 0.15.0 2024-08-05 15:35:06 -05:00
Lance Edgar
9d2684046f feat: move more subscriber logic to wuttaweb 2024-08-05 15:00:11 -05:00
Lance Edgar
3b92bb3a9e fix: use wuttaweb logic for util.get_form_data() 2024-08-05 09:11:19 -05:00
Lance Edgar
5ec899cf08 bump: version 0.14.4 → 0.14.5 2024-08-03 17:43:46 -05:00
Lance Edgar
458c95696a fix: use auth handler instead of deprecated auth functions 2024-08-03 14:13:16 -05:00
Lance Edgar
08a89c490a fix: avoid duplicate partial param when grid reloads data 2024-07-21 20:20:43 -05:00
Lance Edgar
a9495b6a70 bump: version 0.14.3 → 0.14.4 2024-07-18 17:59:55 -05:00
Lance Edgar
1bba6d9947 fix: fix more settings persistence bug(s) for datasync/configure
esp. for the profile consumers info
2024-07-18 17:58:59 -05:00
Lance Edgar
f4f79f170a fix: fix modals for luigi tasks page, per oruga 2024-07-17 19:45:47 -05:00
Lance Edgar
9c466796da bump: version 0.14.2 → 0.14.3 2024-07-17 18:24:21 -05:00
Lance Edgar
e88b8fc9bc fix: fix auto-collapse title for viewing trainwreck txn 2024-07-16 21:21:43 -05:00
Lance Edgar
3aafe578f0 fix: allow auto-collapse of header when viewing trainwreck txn 2024-07-16 18:59:35 -05:00
Lance Edgar
af0f84762c bump: version 0.14.1 → 0.14.2 2024-07-15 21:52:05 -05:00
Lance Edgar
be6eb5f815 fix: add null menu handler, for use with API apps 2024-07-15 21:51:45 -05:00
185 changed files with 6728 additions and 4003 deletions

View file

@ -5,6 +5,321 @@ All notable changes to Tailbone will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## v0.22.7 (2025-02-19)
### Fix
- stop using old config for logo image url on login page
- fix warning msg for deprecated Grid param
## v0.22.6 (2025-02-01)
### Fix
- register vue3 form component for products -> make batch
## v0.22.5 (2024-12-16)
### Fix
- whoops this is latest rattail
- require newer rattail lib
- require newer wuttaweb
- let caller request safe HTML literal for rendered grid table
## v0.22.4 (2024-11-22)
### Fix
- avoid error in product search for duplicated key
- use vmodel for confirm password widget input
## v0.22.3 (2024-11-19)
### Fix
- avoid error for trainwreck query when not a customer
## v0.22.2 (2024-11-18)
### Fix
- use local/custom enum for continuum operations
- add basic master view for Product Costs
- show continuum operation type when viewing version history
- always define `app` attr for ViewSupplement
- avoid deprecated import
## v0.22.1 (2024-11-02)
### Fix
- fix submit button for running problem report
- avoid deprecated grid method
## v0.22.0 (2024-10-22)
### Feat
- add support for new ordering batch from parsed file
### Fix
- avoid deprecated method to suggest username
## v0.21.11 (2024-10-03)
### Fix
- custom method for adding grid action
- become/stop root should redirect to previous url
## v0.21.10 (2024-09-15)
### Fix
- update project repo links, kallithea -> forgejo
- use better icon for submit button on login page
- wrap notes text for batch view
- expose datasync consumer batch size via configure page
## v0.21.9 (2024-08-28)
### Fix
- render custom attrs in form component tag
## v0.21.8 (2024-08-28)
### Fix
- ignore session kwarg for `MasterView.make_row_grid()`
## v0.21.7 (2024-08-28)
### Fix
- avoid error when form value cannot be obtained
## v0.21.6 (2024-08-28)
### Fix
- avoid error when grid value cannot be obtained
## v0.21.5 (2024-08-28)
### Fix
- set empty string for "-new-" file configure option
## v0.21.4 (2024-08-26)
### Fix
- handle differing email profile keys for appinfo/configure
## v0.21.3 (2024-08-26)
### Fix
- show non-standard config values for app info configure email
## v0.21.2 (2024-08-26)
### Fix
- refactor waterpark base template to use wutta feedback component
- fix input/output file upload feature for configure pages, per oruga
- tweak how grid data translates to Vue template context
- merge filters into main grid template
- add basic wutta view for users
- some fixes for wutta people view
- various fixes for waterpark theme
- avoid deprecated `component` form kwarg
## v0.21.1 (2024-08-22)
### Fix
- misc. bugfixes per recent changes
## v0.21.0 (2024-08-22)
### Feat
- move "most" filtering logic for grid class to wuttaweb
- inherit from wuttaweb templates for home, login pages
- inherit from wuttaweb for AppInfoView, appinfo/configure template
- add "has output file templates" config option for master view
### Fix
- change grid reset-view param name to match wuttaweb
- move "searchable columns" grid feature to wuttaweb
- use wuttaweb to get/render csrf token
- inherit from wuttaweb for appinfo/index template
- prefer wuttaweb config for "home redirect to login" feature
- fix master/index template rendering for waterpark theme
- fix spacing for navbar logo/title in waterpark theme
## v0.20.1 (2024-08-20)
### Fix
- fix default filter verbs logic for workorder status
## v0.20.0 (2024-08-20)
### Feat
- add new 'waterpark' theme, based on wuttaweb w/ vue2 + buefy
- refactor templates to simplify base/page/form structure
### Fix
- avoid deprecated reference to app db engine
## v0.19.3 (2024-08-19)
### Fix
- add pager stats to all grid vue data (fixes view history)
## v0.19.2 (2024-08-19)
### Fix
- sort on frontend for appinfo package listing grid
- prefer attr over key lookup when getting model values
- replace all occurrences of `component_studly` => `vue_component`
## v0.19.1 (2024-08-19)
### Fix
- fix broken user auth for web API app
## 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
- 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
- use wuttaweb for `get_liburl()` logic
## v0.16.1 (2024-08-15)
### Fix
- improve wutta People view a bit
- update references to `get_class_hierarchy()`
- tweak template for `people/view_profile` per wutta compat
## v0.16.0 (2024-08-15)
### Feat
- add first wutta-based master, for PersonView
- refactor forms/grids/views/templates per wuttaweb compat
## 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
- assign convenience attrs for all views (config, app, enum, model)
## v0.15.4 (2024-08-09)
### Fix
- avoid bug when checking current theme
## v0.15.3 (2024-08-08)
### Fix
- fix timepicker `parseTime()` when value is null
## v0.15.2 (2024-08-06)
### Fix
- use auth handler, avoid legacy calls for role/perm checks
## v0.15.1 (2024-08-05)
### Fix
- move magic `b` template context var to wuttaweb
## 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
- use auth handler instead of deprecated auth functions
- avoid duplicate `partial` param when grid reloads data
## 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
- fix auto-collapse title for viewing trainwreck txn
- allow auto-collapse of header when viewing trainwreck txn
## v0.14.2 (2024-07-15)
### Fix
- add null menu handler, for use with API apps
## v0.14.1 (2024-07-14)
### Fix

View file

@ -1,10 +1,8 @@
Tailbone
========
# Tailbone
Tailbone is an extensible web application based on Rattail. It provides a
"back-office network environment" (BONE) for use in managing retail data.
Please see Rattail's `home page`_ for more information.
.. _home page: http://rattailproject.org/
Please see Rattail's [home page](http://rattailproject.org/) for more
information.

6
docs/api/util.rst Normal file
View file

@ -0,0 +1,6 @@
``tailbone.util``
=================
.. automodule:: tailbone.util
:members:

View file

@ -27,10 +27,10 @@ templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
intersphinx_mapping = {
'rattail': ('https://rattailproject.org/docs/rattail/', None),
'rattail': ('https://docs.wuttaproject.org/rattail/', None),
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None),
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None),
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
}
# allow todo entries to show up

View file

@ -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

View file

@ -6,9 +6,9 @@ build-backend = "hatchling.build"
[project]
name = "Tailbone"
version = "0.14.1"
version = "0.22.7"
description = "Backoffice Web Application for Rattail"
readme = "README.rst"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
license = {text = "GNU GPL v3+"}
classifiers = [
@ -53,13 +53,13 @@ dependencies = [
"pyramid_mako",
"pyramid_retry",
"pyramid_tm",
"rattail[db,bouncer]>=0.17.0",
"rattail[db,bouncer]>=0.20.1",
"sa-filters",
"simplejson",
"transaction",
"waitress",
"WebHelpers2",
"WuttaWeb>=0.2.0",
"WuttaWeb>=0.21.0",
"zope.sqlalchemy>=1.5",
]
@ -84,9 +84,9 @@ tailbone = "tailbone.config:ConfigExtension"
[project.urls]
Homepage = "https://rattailproject.org"
Repository = "https://kallithea.rattailproject.org/rattail-project/tailbone"
Issues = "https://redmine.rattailproject.org/projects/tailbone/issues"
Changelog = "https://kallithea.rattailproject.org/rattail-project/tailbone/files/master/CHANGELOG.md"
Repository = "https://forgejo.wuttaproject.org/rattail/tailbone"
Issues = "https://forgejo.wuttaproject.org/rattail/tailbone/issues"
Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone/src/branch/master/CHANGELOG.md"
[tool.commitizen]

View file

@ -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),

View file

@ -29,8 +29,7 @@ import logging
import humanize
import sqlalchemy as sa
from rattail.db import model
from rattail.util import pretty_quantity
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
from cornice import Service
from deform import widget as dfwidget
@ -45,7 +44,7 @@ log = logging.getLogger(__name__)
class ReceivingBatchViews(APIBatchView):
model_class = model.PurchaseBatch
model_class = PurchaseBatch
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'receivingbatchviews'
permission_prefix = 'receiving'
@ -55,7 +54,8 @@ class ReceivingBatchViews(APIBatchView):
supports_execute = True
def base_query(self):
query = super(ReceivingBatchViews, self).base_query()
model = self.app.model
query = super().base_query()
query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING)
return query
@ -85,7 +85,7 @@ class ReceivingBatchViews(APIBatchView):
# assume "receive from PO" if given a PO key
if data.get('purchase_key'):
data['receiving_workflow'] = 'from_po'
data['workflow'] = 'from_po'
return super().create_object(data)
@ -120,6 +120,7 @@ class ReceivingBatchViews(APIBatchView):
return self._get(obj=batch)
def eligible_purchases(self):
model = self.app.model
uuid = self.request.params.get('vendor_uuid')
vendor = self.Session.get(model.Vendor, uuid) if uuid else None
if not vendor:
@ -176,7 +177,7 @@ class ReceivingBatchViews(APIBatchView):
class ReceivingBatchRowViews(APIBatchRowView):
model_class = model.PurchaseBatchRow
model_class = PurchaseBatchRow
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'receiving.rows'
permission_prefix = 'receiving'
@ -185,7 +186,8 @@ class ReceivingBatchRowViews(APIBatchRowView):
supports_quick_entry = True
def make_filter_spec(self):
filters = super(ReceivingBatchRowViews, self).make_filter_spec()
model = self.app.model
filters = super().make_filter_spec()
if filters:
# must translate certain convenience filters
@ -296,11 +298,11 @@ class ReceivingBatchRowViews(APIBatchRowView):
return filters
def normalize(self, row):
data = super(ReceivingBatchRowViews, self).normalize(row)
data = super().normalize(row)
model = self.app.model
batch = row.batch
app = self.get_rattail_app()
prodder = app.get_products_handler()
prodder = self.app.get_products_handler()
data['product_uuid'] = row.product_uuid
data['item_id'] = row.item_id
@ -375,7 +377,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
if accounted_for:
# some product accounted for; button should receive "remainder" only
if remainder:
remainder = pretty_quantity(remainder)
remainder = self.app.render_quantity(remainder)
data['quick_receive_quantity'] = remainder
data['quick_receive_text'] = "Receive Remainder ({} {})".format(
remainder, data['unit_uom'])
@ -386,7 +388,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
else: # nothing yet accounted for, button should receive "all"
if not remainder:
log.warning("quick receive remainder is empty for row %s", row.uuid)
remainder = pretty_quantity(remainder)
remainder = self.app.render_quantity(remainder)
data['quick_receive_quantity'] = remainder
data['quick_receive_text'] = "Receive ALL ({} {})".format(
remainder, data['unit_uom'])
@ -414,7 +416,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
data['received_alert'] = None
if self.batch_handler.get_units_confirmed(row):
msg = "You have already received some of this product; last update was {}.".format(
humanize.naturaltime(app.make_utc() - row.modified))
humanize.naturaltime(self.app.make_utc() - row.modified))
data['received_alert'] = msg
return data
@ -423,6 +425,8 @@ class ReceivingBatchRowViews(APIBatchRowView):
"""
View which handles "receiving" against a particular batch row.
"""
model = self.app.model
# first do basic input validation
schema = ReceiveRow().bind(session=self.Session())
form = forms.Form(schema=schema, request=self.request)

View file

@ -26,7 +26,6 @@ Tailbone Web API - Master View
import json
from rattail.config import parse_bool
from rattail.db.util import get_fieldnames
from cornice import resource, Service
@ -185,7 +184,7 @@ class APIMasterView(APIView):
if sortcol:
spec = {
'field': sortcol.field_name,
'direction': 'asc' if parse_bool(self.request.params['ascending']) else 'desc',
'direction': 'asc' if self.config.parse_bool(self.request.params['ascending']) else 'desc',
}
if sortcol.model_name:
spec['model'] = sortcol.model_name

View file

@ -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
@ -66,9 +62,20 @@ def make_rattail_config(settings):
# nb. this is for compaibility with wuttaweb
settings['wutta_config'] = rattail_config
# must import all sqlalchemy models before things get rolling,
# otherwise can have errors about continuum TransactionMeta class
# not yet mapped, when relevant pages are first requested...
# cf. https://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/database/sqlalchemy.html#importing-all-sqlalchemy-models
# hat tip to https://stackoverflow.com/a/59241485
if getattr(rattail_config, 'tempmon_engine', None):
from rattail_tempmon.db import model as tempmon_model, Session as TempmonSession
tempmon_session = TempmonSession()
tempmon_session.query(tempmon_model.Appliance).first()
tempmon_session.close()
# configure database sessions
if hasattr(rattail_config, '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'):
@ -189,9 +196,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')
@ -318,7 +332,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')

View file

@ -27,20 +27,18 @@ Authentication & Authorization
import logging
import re
from rattail.util import prettify, 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,63 +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)
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)

View file

@ -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:

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@ -270,9 +270,21 @@ class VersionDiff(Diff):
for field in self.fields:
values[field] = {'before': self.render_old_value(field),
'after': self.render_new_value(field)}
operation = None
if self.version.operation_type == continuum.Operation.INSERT:
operation = 'INSERT'
elif self.version.operation_type == continuum.Operation.UPDATE:
operation = 'UPDATE'
elif self.version.operation_type == continuum.Operation.DELETE:
operation = 'DELETE'
else:
operation = self.version.operation_type
return {
'key': id(self.version),
'model_title': self.title,
'operation': operation,
'diff_class': self.nature,
'fields': self.fields,
'values': values,

View file

@ -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 FieldList, get_form_data, make_json_safe
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,
@ -326,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
@ -337,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:
@ -378,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 {}
@ -387,14 +401,60 @@ class Form(object):
self.edit_help_url = edit_help_url
self.route_prefix = route_prefix
self.button_icon_submit = kwargs.get('button_icon_submit', 'save')
def __iter__(self):
return iter(self.fields)
@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
@ -570,7 +630,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:
@ -801,6 +863,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'):
@ -839,6 +905,11 @@ class Form(object):
return self.deform_form
def render_vue_template(self, template='/forms/deform.mako', **context):
""" """
output = self.render_deform(template=template, **context)
return HTML.literal(output)
def render_deform(self, dform=None, template=None, **kwargs):
if not template:
template = '/forms/deform.mako'
@ -861,8 +932,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
@ -874,12 +945,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)
@ -887,6 +959,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
@ -953,7 +1037,11 @@ class Form(object):
def set_vuejs_component_kwargs(self, **kwargs):
self.vuejs_component_kwargs.update(kwargs)
def render_vuejs_component(self):
def render_vue_tag(self, **kwargs):
""" """
return self.render_vuejs_component(**kwargs)
def render_vuejs_component(self, **kwargs):
"""
Render the Vue.js component HTML for the form.
@ -964,10 +1052,11 @@ class Form(object):
<tailbone-form :configure-fields-help="configureFieldsHelp">
</tailbone-form>
"""
kwargs = dict(self.vuejs_component_kwargs)
kw = dict(self.vuejs_component_kwargs)
kw.update(kwargs)
if self.can_edit_help:
kwargs.setdefault(':configure-fields-help', 'configureFieldsHelp')
return HTML.tag(self.component, **kwargs)
kw.setdefault(':configure-fields-help', 'configureFieldsHelp')
return HTML.tag(self.vue_tagname, **kw)
def set_json_data(self, key, value):
"""
@ -993,7 +1082,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 ``<b-field>``
wrapper. Note that this is meant to render *editable* fields,
@ -1011,7 +1105,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 <b-field> (*not* the widget)
attrs = {
@ -1130,6 +1224,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.
@ -1269,12 +1375,19 @@ class Form(object):
def obtain_value(self, record, field_name):
if record:
if isinstance(record, dict):
return record[field_name]
try:
return getattr(record, field_name)
except AttributeError:
pass
try:
return record[field_name]
except KeyError:
return None
except TypeError:
return getattr(record, field_name, None)
pass
# TODO: is this always safe to do?
elif self.defaults and field_name in self.defaults:
@ -1328,30 +1441,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']

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@ -24,6 +24,9 @@
Template Context Helpers
"""
# start off with all from wuttaweb
from wuttaweb.helpers import *
import os
import datetime
from decimal import Decimal
@ -33,14 +36,9 @@ from rattail.time import localtime, make_utc
from rattail.util import pretty_quantity, pretty_hours, hours_as_decimal
from rattail.db.util import maxlen
from webhelpers2.html import *
from webhelpers2.html.tags import *
from tailbone.util import (csrf_token, get_csrf_token,
pretty_datetime, raw_datetime,
from tailbone.util import (pretty_datetime, raw_datetime,
render_markdown,
route_exists,
get_liburl)
route_exists)
def pretty_date(date):

View file

@ -394,6 +394,11 @@ class TailboneMenuHandler(WuttaMenuHandler):
'route': 'products',
'perm': 'products.list',
},
{
'title': "Product Costs",
'route': 'product_costs',
'perm': 'product_costs.list',
},
{
'title': "Departments",
'route': 'departments',
@ -451,6 +456,11 @@ class TailboneMenuHandler(WuttaMenuHandler):
'route': 'vendors',
'perm': 'vendors.list',
},
{
'title': "Product Costs",
'route': 'product_costs',
'perm': 'product_costs.list',
},
{'type': 'sep'},
{
'title': "Ordering",
@ -703,7 +713,7 @@ class TailboneMenuHandler(WuttaMenuHandler):
},
{'type': 'sep'},
{
'title': "App Details",
'title': "App Info",
'route': 'appinfo',
'perm': 'appinfo.list',
},
@ -745,3 +755,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 []

View file

@ -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')

View file

@ -48,43 +48,21 @@ 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.
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``.
.. 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.
will be the same as :attr:`wuttaweb:request.wutta_config`.
.. method:: request.register_component(tagname, classname)
@ -94,79 +72,55 @@ 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)
config = request.wutta_config
app = config.get_app()
auth = app.get_auth_handler()
session = session or Session()
# compatibility
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
if hasattr(request, 'client_addr'):
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)
# 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()
# TODO: why would this ever be null?
if rattail_config:
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)
app = rattail_config.get_app()
auth = app.get_auth_handler()
request.tailbone_cached_permissions = auth.get_permissions(
Session(), request.user)
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):
@ -205,7 +159,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',
@ -286,27 +239,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

View file

@ -1,250 +1,2 @@
## -*- coding: utf-8; -*-
<%inherit file="/configure.mako" />
<%def name="form_content()">
<h3 class="block is-size-3">Basics</h3>
<div class="block" style="padding-left: 2rem;">
<b-field grouped>
<b-field label="App Title">
<b-input name="rattail.app_title"
v-model="simpleSettings['rattail.app_title']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
<b-field label="Node Type">
## TODO: should be a dropdown, app handler defines choices
<b-input name="rattail.node_type"
v-model="simpleSettings['rattail.node_type']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
<b-field label="Node Title">
<b-input name="rattail.node_title"
v-model="simpleSettings['rattail.node_title']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
</b-field>
<b-field>
<b-checkbox name="rattail.production"
v-model="simpleSettings['rattail.production']"
native-value="true"
@input="settingsNeedSaved = true">
Production Mode
</b-checkbox>
</b-field>
<div class="level-left">
<div class="level-item">
<b-field>
<b-checkbox name="rattail.running_from_source"
v-model="simpleSettings['rattail.running_from_source']"
native-value="true"
@input="settingsNeedSaved = true">
Running from Source
</b-checkbox>
</b-field>
</div>
<div class="level-item">
<b-field label="Top-Level Package" horizontal
v-if="simpleSettings['rattail.running_from_source']">
<b-input name="rattail.running_from_source.rootpkg"
v-model="simpleSettings['rattail.running_from_source.rootpkg']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
</div>
</div>
</div>
<h3 class="block is-size-3">Display</h3>
<div class="block" style="padding-left: 2rem;">
<b-field grouped>
<b-field label="Background Color">
<b-input name="tailbone.background_color"
v-model="simpleSettings['tailbone.background_color']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
</b-field>
</div>
<h3 class="block is-size-3">Grids</h3>
<div class="block" style="padding-left: 2rem;">
<b-field grouped>
<b-field label="Default Page Size">
<b-input name="tailbone.grid.default_pagesize"
v-model="simpleSettings['tailbone.grid.default_pagesize']"
@input="settingsNeedSaved = true">
</b-input>
</b-field>
</b-field>
</div>
<h3 class="block is-size-3">Web Libraries</h3>
<div class="block" style="padding-left: 2rem;">
<${b}-table :data="weblibs">
<${b}-table-column field="title"
label="Name"
v-slot="props">
{{ props.row.title }}
</${b}-table-column>
<${b}-table-column field="configured_version"
label="Version"
v-slot="props">
{{ props.row.configured_version || props.row.default_version }}
</${b}-table-column>
<${b}-table-column field="configured_url"
label="URL Override"
v-slot="props">
{{ props.row.configured_url }}
</${b}-table-column>
<${b}-table-column field="live_url"
label="Effective (Live) URL"
v-slot="props">
<span v-if="props.row.modified"
class="has-text-warning">
save settings and refresh page to see new URL
</span>
<span v-if="!props.row.modified">
{{ props.row.live_url }}
</span>
</${b}-table-column>
<${b}-table-column field="actions"
label="Actions"
v-slot="props">
<a href="#"
@click.prevent="editWebLibraryInit(props.row)">
% if request.use_oruga:
<o-icon icon="edit" />
% else:
<i class="fas fa-edit"></i>
% endif
Edit
</a>
</${b}-table-column>
</${b}-table>
% for weblib in weblibs:
${h.hidden('tailbone.libver.{}'.format(weblib['key']), **{':value': "simpleSettings['tailbone.libver.{}']".format(weblib['key'])})}
${h.hidden('tailbone.liburl.{}'.format(weblib['key']), **{':value': "simpleSettings['tailbone.liburl.{}']".format(weblib['key'])})}
% endfor
<${b}-modal has-modal-card
% if request.use_oruga:
v-model:active="editWebLibraryShowDialog"
% else:
:active.sync="editWebLibraryShowDialog"
% endif
>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Web Library: {{ editWebLibraryRecord.title }}</p>
</header>
<section class="modal-card-body">
<b-field grouped>
<b-field label="Default Version">
<b-input v-model="editWebLibraryRecord.default_version"
disabled>
</b-input>
</b-field>
<b-field label="Override Version">
<b-input v-model="editWebLibraryVersion">
</b-input>
</b-field>
</b-field>
<b-field label="Override URL">
<b-input v-model="editWebLibraryURL"
expanded />
</b-field>
<b-field label="Effective URL (as of last page load)">
<b-input v-model="editWebLibraryRecord.live_url"
disabled
expanded />
</b-field>
</section>
<footer class="modal-card-foot">
<b-button type="is-primary"
@click="editWebLibrarySave()"
icon-pack="fas"
icon-left="save">
Save
</b-button>
<b-button @click="editWebLibraryShowDialog = false">
Cancel
</b-button>
</footer>
</div>
</${b}-modal>
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
ThisPageData.weblibs = ${json.dumps(weblibs)|n}
ThisPageData.editWebLibraryShowDialog = false
ThisPageData.editWebLibraryRecord = {}
ThisPageData.editWebLibraryVersion = null
ThisPageData.editWebLibraryURL = null
ThisPage.methods.editWebLibraryInit = function(row) {
this.editWebLibraryRecord = row
this.editWebLibraryVersion = row.configured_version
this.editWebLibraryURL = row.configured_url
this.editWebLibraryShowDialog = true
}
ThisPage.methods.editWebLibrarySave = function() {
this.editWebLibraryRecord.configured_version = this.editWebLibraryVersion
this.editWebLibraryRecord.configured_url = this.editWebLibraryURL
this.editWebLibraryRecord.modified = true
this.simpleSettings[`tailbone.libver.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryVersion
this.simpleSettings[`tailbone.liburl.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryURL
this.settingsNeedSaved = true
this.editWebLibraryShowDialog = false
}
</script>
</%def>
${parent.body()}
<%inherit file="wuttaweb:templates/appinfo/configure.mako" />

View file

@ -1,8 +1,7 @@
## -*- coding: utf-8; -*-
<%inherit file="/master/index.mako" />
<%def name="render_grid_component()">
<%inherit file="wuttaweb:templates/appinfo/index.mako" />
<%def name="page_content()">
<div class="buttons">
<once-button type="is-primary"
@ -28,100 +27,5 @@
</div>
<${b}-collapse class="panel" open>
<template #trigger="props">
<div class="panel-heading"
style="cursor: pointer;"
role="button">
## TODO: for some reason buefy will "reuse" the icon
## element in such a way that its display does not
## refresh. so to work around that, we use different
## structure for the two icons, so buefy is forced to
## re-draw
<b-icon v-if="props.open"
pack="fas"
icon="angle-down">
</b-icon>
<span v-if="!props.open">
<b-icon pack="fas"
icon="angle-right">
</b-icon>
</span>
<span>Configuration Files</span>
</div>
</template>
<div class="panel-block">
<div style="width: 100%;">
<${b}-table :data="configFiles">
<${b}-table-column field="priority"
label="Priority"
v-slot="props">
{{ props.row.priority }}
</${b}-table-column>
<${b}-table-column field="path"
label="File Path"
v-slot="props">
{{ props.row.path }}
</${b}-table-column>
</${b}-table>
</div>
</div>
</${b}-collapse>
<${b}-collapse class="panel"
:open="false">
<template #trigger="props">
<div class="panel-heading"
style="cursor: pointer;"
role="button">
## TODO: for some reason buefy will "reuse" the icon
## element in such a way that its display does not
## refresh. so to work around that, we use different
## structure for the two icons, so buefy is forced to
## re-draw
<b-icon v-if="props.open"
pack="fas"
icon="angle-down">
</b-icon>
<span v-if="!props.open">
<b-icon pack="fas"
icon="angle-right">
</b-icon>
</span>
<strong>Installed Packages</strong>
</div>
</template>
<div class="panel-block">
<div style="width: 100%;">
${parent.render_grid_component()}
</div>
</div>
</${b}-collapse>
${parent.page_content()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(request.rattail_config.prioritized_files, 1)])|n}
</script>
</%def>
${parent.body()}

View file

@ -15,8 +15,8 @@
<app-settings :groups="groups" :showing-group="showingGroup"></app-settings>
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
<script type="text/x-template" id="app-settings-template">
<div class="form">
@ -150,19 +150,18 @@
</script>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.groups = ${json.dumps(settings_data)|n}
ThisPageData.showingGroup = ${json.dumps(current_group or '')|n}
</script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript">
<%def name="make_vue_components()">
${parent.make_vue_components()}
<script>
Vue.component('app-settings', {
template: '#app-settings-template',
@ -193,6 +192,3 @@
</script>
</%def>
${parent.body()}

View file

@ -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" />
@ -34,17 +35,21 @@
</head>
<body>
${declare_formposter_mixin()}
${self.body()}
<div id="whole-page-app">
<div id="app" style="height: 100%;">
<whole-page></whole-page>
</div>
${self.render_whole_page_template()}
${self.make_whole_page_component()}
${self.make_whole_page_app()}
## TODO: this must come before the self.body() call..but why?
${declare_formposter_mixin()}
## content body from derived/child template
${self.body()}
## Vue app
${self.render_vue_templates()}
${self.modify_vue_vars()}
${self.make_vue_components()}
${self.make_vue_app()}
</body>
</html>
@ -122,16 +127,16 @@
</%def>
<%def name="vuejs()">
${h.javascript_link(h.get_liburl(request, 'vue'))}
${h.javascript_link(h.get_liburl(request, 'vue_resource'))}
${h.javascript_link(h.get_liburl(request, 'vue', prefix='tailbone'))}
${h.javascript_link(h.get_liburl(request, 'vue_resource', prefix='tailbone'))}
</%def>
<%def name="buefy()">
${h.javascript_link(h.get_liburl(request, 'buefy'))}
${h.javascript_link(h.get_liburl(request, 'buefy', prefix='tailbone'))}
</%def>
<%def name="fontawesome()">
<script defer src="${h.get_liburl(request, 'fontawesome')}"></script>
<script defer src="${h.get_liburl(request, 'fontawesome', prefix='tailbone')}"></script>
</%def>
<%def name="extra_javascript()"></%def>
@ -153,12 +158,16 @@
<style type="text/css">
.filters .filter-fieldname,
.filters .filter-fieldname .button {
% if filter_fieldname_width is not Undefined:
min-width: ${filter_fieldname_width};
% endif
justify-content: left;
}
% if filter_fieldname_width is not Undefined:
.filters .filter-verb {
min-width: ${filter_verb_width};
}
% endif
</style>
</%def>
@ -167,7 +176,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
</%def>
@ -177,7 +186,7 @@
<%def name="head_tags()"></%def>
<%def name="render_whole_page_template()">
<%def name="render_vue_template_whole_page()">
<script type="text/x-template" id="whole-page-template">
<div>
<header>
@ -276,7 +285,7 @@
<span class="header-text">
${index_title}
</span>
% 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'):
<once-button type="is-primary"
tag="a" href="${url('{}.create'.format(route_prefix))}"
icon-left="plus"
@ -302,7 +311,7 @@
<span class="header-text">
${h.link_to(instance_title, instance_url)}
</span>
% 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'):
<once-button type="is-primary"
tag="a" href="${url('{}.create'.format(route_prefix))}"
@ -623,9 +632,23 @@
% endif
<div class="navbar-dropdown">
% if request.is_root:
${h.link_to("Stop being root", url('stop_root'), class_='navbar-item root-user')}
${h.form(url('stop_root'), ref='stopBeingRootForm')}
${h.csrf_token(request)}
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
<a @click="$refs.stopBeingRootForm.submit()"
class="navbar-item root-user">
Stop being root
</a>
${h.end_form()}
% elif request.is_admin:
${h.link_to("Become root", url('become_root'), class_='navbar-item root-user')}
${h.form(url('become_root'), ref='startBeingRootForm')}
${h.csrf_token(request)}
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
<a @click="$refs.startBeingRootForm.submit()"
class="navbar-item root-user">
Become root
</a>
${h.end_form()}
% endif
% if messaging_enabled:
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
@ -633,7 +656,11 @@
% if request.is_root or not request.user.prevent_password_change:
${h.link_to("Change Password", url('change_password'), class_='navbar-item')}
% endif
${h.link_to("Edit Preferences", url('my.preferences'), class_='navbar-item')}
% try:
## nb. does not exist yet for wuttaweb
${h.link_to("Edit Preferences", url('my.preferences'), class_='navbar-item')}
% except:
% endtry
${h.link_to("Logout", url('logout'), class_='navbar-item')}
</div>
</div>
@ -654,19 +681,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'):
<once-button tag="a" href="${action_url('edit', instance)}"
<once-button tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit"
text="Edit This">
</once-button>
% endif
% if master.cloneable and master.has_perm('clone'):
<once-button tag="a" href="${action_url('clone', instance)}"
% if getattr(master, 'cloneable', False) and not master.cloning and master.has_perm('clone'):
<once-button tag="a" href="${master.get_action_url('clone', instance)}"
icon-left="object-ungroup"
text="Clone This">
</once-button>
% endif
% if master.deletable and instance_deletable and master.has_perm('delete'):
<once-button tag="a" href="${action_url('delete', instance)}"
<once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger"
icon-left="trash"
text="Delete This">
@ -675,7 +702,7 @@
% else:
## viewing row
% if instance_deletable and master.has_perm('delete_row'):
<once-button tag="a" href="${action_url('delete', instance)}"
<once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger"
icon-left="trash"
text="Delete This">
@ -684,13 +711,13 @@
% endif
% elif master and master.editing:
% if master.viewable and master.has_perm('view'):
<once-button tag="a" href="${action_url('view', instance)}"
<once-button tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
text="View This">
</once-button>
% endif
% if master.deletable and instance_deletable and master.has_perm('delete'):
<once-button tag="a" href="${action_url('delete', instance)}"
<once-button tag="a" href="${master.get_action_url('delete', instance)}"
type="is-danger"
icon-left="trash"
text="Delete This">
@ -698,13 +725,13 @@
% endif
% elif master and master.deleting:
% if master.viewable and master.has_perm('view'):
<once-button tag="a" href="${action_url('view', instance)}"
<once-button tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
text="View This">
</once-button>
% endif
% if master.editable and instance_editable and master.has_perm('edit'):
<once-button tag="a" href="${action_url('edit', instance)}"
<once-button tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit"
text="Edit This">
</once-button>
@ -745,11 +772,8 @@
% endif
</%def>
<%def name="declare_whole_page_vars()">
${page_help.declare_vars()}
${multi_file_upload.declare_vars()}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
<script type="text/javascript">
<%def name="render_vue_script_whole_page()">
<script>
let WholePage = {
template: '#whole-page-template',
@ -856,7 +880,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 +890,7 @@
globalSearchActive: false,
globalSearchTerm: '',
globalSearchData: ${json.dumps(global_search_data)|n},
globalSearchData: ${json.dumps(global_search_data or [])|n},
mountedHooks: [],
}
@ -885,57 +909,6 @@
</script>
</%def>
<%def name="modify_whole_page_vars()">
<script type="text/javascript">
% if request.user:
FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n}
FeedbackFormData.userName = ${json.dumps(str(request.user))|n}
% endif
</script>
</%def>
<%def name="finalize_whole_page_vars()">
## NOTE: if you override this, must use <script> tags
</%def>
<%def name="make_whole_page_component()">
${make_grid_filter_components()}
${self.declare_whole_page_vars()}
${self.modify_whole_page_vars()}
${self.finalize_whole_page_vars()}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))}
${page_help.make_component()}
${multi_file_upload.make_component()}
<script type="text/javascript">
FeedbackForm.data = function() { return FeedbackFormData }
Vue.component('feedback-form', FeedbackForm)
WholePage.data = function() { return WholePageData }
Vue.component('whole-page', WholePage)
</script>
</%def>
<%def name="make_whole_page_app()">
<script type="text/javascript">
new Vue({
el: '#whole-page-app'
})
</script>
</%def>
<%def name="wtfield(form, name, **kwargs)">
<div class="field-wrapper${' error' if form[name].errors else ''}">
<label for="${name}">${form[name].label}</label>
@ -957,3 +930,88 @@
</div>
</div>
</%def>
##############################
## vue components + app
##############################
<%def name="render_vue_templates()">
${page_help.declare_vars()}
${multi_file_upload.declare_vars()}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))}
## DEPRECATED; called for back-compat
${self.render_whole_page_template()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="render_whole_page_template()">
${self.render_vue_template_whole_page()}
${self.declare_whole_page_vars()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="declare_whole_page_vars()">
${self.render_vue_script_whole_page()}
</%def>
<%def name="modify_vue_vars()">
## DEPRECATED; called for back-compat
${self.modify_whole_page_vars()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="modify_whole_page_vars()">
<script>
% if request.user:
FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n}
FeedbackFormData.userName = ${json.dumps(str(request.user))|n}
% endif
</script>
</%def>
<%def name="make_vue_components()">
${make_wutta_components()}
${make_grid_filter_components()}
${page_help.make_component()}
${multi_file_upload.make_component()}
<script>
FeedbackForm.data = function() { return FeedbackFormData }
Vue.component('feedback-form', FeedbackForm)
</script>
## DEPRECATED; called for back-compat
${self.finalize_whole_page_vars()}
${self.make_whole_page_component()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="make_whole_page_component()">
<script>
WholePage.data = function() { return WholePageData }
Vue.component('whole-page', WholePage)
</script>
</%def>
<%def name="make_vue_app()">
## DEPRECATED; called for back-compat
${self.make_whole_page_app()}
</%def>
## DEPRECATED; remains for back-compat
<%def name="make_whole_page_app()">
<script>
new Vue({
el: '#app'
})
</script>
</%def>
##############################
## DEPRECATED
##############################
<%def name="finalize_whole_page_vars()"></%def>

View file

@ -1,10 +1,7 @@
## -*- coding: utf-8; -*-
<%inherit file="wuttaweb:templates/base_meta.mako" />
<%def name="app_title()">${rattail_app.get_node_title()}</%def>
<%def name="global_title()">${"[STAGE] " if not request.rattail_config.production() else ''}${self.app_title()}</%def>
<%def name="extra_styles()"></%def>
<%def name="app_title()">${app.get_node_title()}</%def>
<%def name="favicon()">
<link rel="icon" type="image/x-icon" href="${request.rattail_config.get('tailbone', 'favicon_url', default=request.static_url('tailbone:static/img/rattail.ico'))}" />
@ -13,9 +10,3 @@
<%def name="header_logo()">
${h.image(request.rattail_config.get('tailbone', 'header_image_url', default=request.static_url('tailbone:static/img/rattail.ico')), "Header Logo", style="height: 49px;")}
</%def>
<%def name="footer()">
<p class="has-text-centered">
powered by ${h.link_to("Rattail", url('about'))}
</p>
</%def>

View file

@ -43,7 +43,7 @@
<br />
<div class="form-wrapper">
<div class="form">
<${execute_form.component} ref="executeResultsForm"></${execute_form.component}>
${execute_form.render_vue_tag(ref='executeResultsForm')}
</div>
</div>
</section>
@ -64,10 +64,17 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
% if master.results_executable and master.has_perm('execute_multiple'):
${execute_form.render_vue_template(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)}
% endif
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
% if master.results_refreshable and master.has_perm('refresh'):
<script type="text/javascript">
<script>
TailboneGridData.refreshResultsButtonText = "Refresh Results"
TailboneGridData.refreshResultsButtonDisabled = false
@ -81,9 +88,9 @@
</script>
% endif
% if master.results_executable and master.has_perm('execute_multiple'):
<script type="text/javascript">
<script>
${execute_form.component_studly}.methods.submit = function() {
${execute_form.vue_component}.methods.submit = function() {
this.$refs.actualExecuteForm.submit()
}
@ -118,25 +125,9 @@
% endif
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<%def name="make_vue_components()">
${parent.make_vue_components()}
% if master.results_executable and master.has_perm('execute_multiple'):
<script type="text/javascript">
${execute_form.component_studly}.data = function() { return ${execute_form.component_studly}Data }
Vue.component('${execute_form.component}', ${execute_form.component_studly})
</script>
${execute_form.render_vue_finalize()}
% endif
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
% if master.results_executable and master.has_perm('execute_multiple'):
${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n}
% endif
</%def>
${parent.body()}

View file

@ -147,7 +147,7 @@
<script type="text/javascript">
let ${form.component_studly} = {
let ${form.vue_component} = {
template: '#${form.component}-template',
mixins: [SimpleRequestMixin],
@ -278,7 +278,7 @@
},
}
let ${form.component_studly}Data = {
let ${form.vue_component}Data = {
submitting: false,
productUPC: null,
@ -297,14 +297,9 @@
</script>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.toggleCompleteSubmitting = false
</script>
</%def>
${parent.body()}

View file

@ -1,13 +1,9 @@
## -*- coding: utf-8; -*-
<%inherit file="/batch/view.mako" />
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
${form.component_studly}Data.taxesData = ${json.dumps(taxes_data)|n}
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
${form.vue_component}Data.taxesData = ${json.dumps(taxes_data)|n}
</script>
</%def>
${parent.body()}

View file

@ -39,14 +39,9 @@
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.catalogParsers = ${json.dumps(catalog_parsers_data)|n}
</script>
</%def>
${parent.body()}

View file

@ -1,16 +1,16 @@
## -*- coding: utf-8; -*-
<%inherit file="/batch/create.mako" />
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
${form.component_studly}Data.parsers = ${json.dumps(parsers_data)|n}
${form.vue_component}Data.parsers = ${json.dumps(parsers_data)|n}
${form.component_studly}Data.vendorName = null
${form.component_studly}Data.vendorNameReplacement = null
${form.vue_component}Data.vendorName = null
${form.vue_component}Data.vendorNameReplacement = null
${form.component_studly}.watch.field_model_parser_key = function(val) {
${form.vue_component}.watch.field_model_parser_key = function(val) {
let parser = this.parsers[val]
if (parser.vendor_uuid) {
if (this.field_model_vendor_uuid != parser.vendor_uuid) {
@ -24,11 +24,11 @@
}
}
${form.component_studly}.methods.vendorLabelChanging = function(label) {
${form.vue_component}.methods.vendorLabelChanging = function(label) {
this.vendorNameReplacement = label
}
${form.component_studly}.methods.vendorChanged = function(uuid) {
${form.vue_component}.methods.vendorChanged = function(uuid) {
if (uuid) {
this.vendorName = this.vendorNameReplacement
this.vendorNameReplacement = null
@ -37,6 +37,3 @@
</script>
</%def>
${parent.body()}

View file

@ -85,13 +85,11 @@
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
% if batch.executed:
<p>
Batch was executed
${h.pretty_datetime(request.rattail_config, batch.executed)}
by ${batch.executed_by}
</p>
% elif master.handler.executable(batch):
% if master.has_perm('execute'):
<p>Batch has not yet been executed.</p>
<b-button type="is-primary"
% if not execute_enabled:
disabled
@ -121,8 +119,7 @@
<div class="markdown">
${execution_described|n}
</div>
<${execute_form.component} ref="executeBatchForm">
</${execute_form.component}>
${execute_form.render_vue_tag(ref='executeBatchForm')}
</section>
<footer class="modal-card-foot">
@ -151,12 +148,6 @@
</nav>
</%def>
<%def name="render_form_template()">
## TODO: should use self.render_form_buttons()
## ${form.render_deform(form_id='batch-form', buttons=capture(self.render_form_buttons))|n}
${form.render_deform(form_id='batch-form', buttons=capture(buttons))|n}
</%def>
<%def name="render_this_page()">
${parent.render_this_page()}
@ -176,8 +167,7 @@
Please be certain to use the right one!
</p>
<br />
<${upload_worksheet_form.component} ref="uploadForm">
</${upload_worksheet_form.component}>
${upload_worksheet_form.render_vue_tag(ref='uploadForm')}
</section>
<footer class="modal-card-foot">
@ -199,16 +189,6 @@
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'):
${upload_worksheet_form.render_deform(buttons=False, form_kwargs={'ref': 'actualUploadForm'})|n}
% endif
% if master.handler.executable(batch) and master.has_perm('execute'):
${execute_form.render_deform(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)|n}
% endif
</%def>
<%def name="render_form()">
<div class="form">
<${form.component} @show-upload="showUploadDialog = true">
@ -269,9 +249,27 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'):
${upload_worksheet_form.render_vue_template(buttons=False, form_kwargs={'ref': 'actualUploadForm'})}
% endif
% if master.handler.executable(batch) and master.has_perm('execute'):
${execute_form.render_vue_template(form_kwargs={'ref': 'actualExecuteForm'}, buttons=False)}
% endif
</%def>
## DEPRECATED; remains for back-compat
## nb. this is called by parent template, /form.mako
<%def name="render_form_template()">
## TODO: should use self.render_form_buttons()
## ${form.render_deform(form_id='batch-form', buttons=capture(self.render_form_buttons))|n}
${form.render_deform(form_id='batch-form', buttons=capture(buttons))|n}
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.statusBreakdownData = ${json.dumps(status_breakdown_data)|n}
@ -287,7 +285,7 @@
}
% if not batch.executed and master.has_perm('edit'):
${form.component_studly}Data.togglingBatchComplete = false
${form.vue_component}Data.togglingBatchComplete = false
% endif
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'):
@ -308,7 +306,7 @@
form.submit()
}
${upload_worksheet_form.component_studly}.methods.submit = function() {
${upload_worksheet_form.vue_component}.methods.submit = function() {
this.$refs.actualUploadForm.submit()
}
@ -323,7 +321,7 @@
this.$refs.executeBatchForm.submit()
}
${execute_form.component_studly}.methods.submit = function() {
${execute_form.vue_component}.methods.submit = function() {
this.$refs.actualExecuteForm.submit()
}
@ -331,9 +329,9 @@
% if master.rows_bulk_deletable and not batch.executed and master.has_perm('delete_rows'):
${rows_grid.component_studly}Data.deleteResultsShowDialog = false
${rows_grid.vue_component}Data.deleteResultsShowDialog = false
${rows_grid.component_studly}.methods.deleteResultsInit = function() {
${rows_grid.vue_component}.methods.deleteResultsInit = function() {
this.deleteResultsShowDialog = true
}
@ -342,28 +340,12 @@
</script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<%def name="make_vue_components()">
${parent.make_vue_components()}
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'):
<script type="text/javascript">
## UploadForm
${upload_worksheet_form.component_studly}.data = function() { return ${upload_worksheet_form.component_studly}Data }
Vue.component('${upload_worksheet_form.component}', ${upload_worksheet_form.component_studly})
</script>
${upload_worksheet_form.render_vue_finalize()}
% endif
% if execute_enabled and master.has_perm('execute'):
<script type="text/javascript">
## ExecuteForm
${execute_form.component_studly}.data = function() { return ${execute_form.component_studly}Data }
Vue.component('${execute_form.component}', ${execute_form.component_studly})
</script>
${execute_form.render_vue_finalize()}
% endif
</%def>
${parent.body()}

View file

@ -208,9 +208,9 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.menuSequence = ${json.dumps([m['key'] for m in menus])|n}
@ -443,6 +443,3 @@
</script>
</%def>
${parent.body()}

View file

@ -92,7 +92,7 @@
<b-select name="${tmpl['setting_file']}"
v-model="inputFileTemplateSettings['${tmpl['setting_file']}']"
@input="settingsNeedSaved = true">
<option :value="null">-new-</option>
<option value="">-new-</option>
<option v-for="option in inputFileTemplateFileOptions['${tmpl['key']}']"
:key="option"
:value="option">
@ -104,22 +104,40 @@
<b-field label="Upload"
v-show="inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !inputFileTemplateSettings['${tmpl['setting_file']}']">
<b-field class="file is-primary"
:class="{'has-name': !!inputFileTemplateSettings['${tmpl['setting_file']}']}">
<b-upload name="${tmpl['setting_file']}.upload"
v-model="inputFileTemplateUploads['${tmpl['key']}']"
class="file-label"
@input="settingsNeedSaved = true">
<span class="file-cta">
<b-icon class="file-icon" pack="fas" icon="upload"></b-icon>
<span class="file-label">Click to upload</span>
</span>
</b-upload>
<span v-if="inputFileTemplateUploads['${tmpl['key']}']"
class="file-name">
{{ inputFileTemplateUploads['${tmpl['key']}'].name }}
</span>
</b-field>
% if request.use_oruga:
<o-field class="file">
<o-upload name="${tmpl['setting_file']}.upload"
v-model="inputFileTemplateUploads['${tmpl['key']}']"
v-slot="{ onclick }"
@input="settingsNeedSaved = true">
<o-button variant="primary"
@click="onclick">
<o-icon icon="upload" />
<span>Click to upload</span>
</o-button>
<span class="file-name" v-if="inputFileTemplateUploads['${tmpl['key']}']">
{{ inputFileTemplateUploads['${tmpl['key']}'].name }}
</span>
</o-upload>
</o-field>
% else:
<b-field class="file is-primary"
:class="{'has-name': !!inputFileTemplateSettings['${tmpl['setting_file']}']}">
<b-upload name="${tmpl['setting_file']}.upload"
v-model="inputFileTemplateUploads['${tmpl['key']}']"
class="file-label"
@input="settingsNeedSaved = true">
<span class="file-cta">
<b-icon class="file-icon" pack="fas" icon="upload"></b-icon>
<span class="file-label">Click to upload</span>
</span>
</b-upload>
<span v-if="inputFileTemplateUploads['${tmpl['key']}']"
class="file-name">
{{ inputFileTemplateUploads['${tmpl['key']}'].name }}
</span>
</b-field>
% endif
</b-field>
@ -143,6 +161,85 @@
</div>
</%def>
<%def name="output_file_template_field(key)">
<% tmpl = output_file_templates[key] %>
<b-field grouped>
<b-field label="${tmpl['label']}">
<b-select name="${tmpl['setting_mode']}"
v-model="outputFileTemplateSettings['${tmpl['setting_mode']}']"
@input="settingsNeedSaved = true">
<option value="default">use default</option>
<option value="hosted">use uploaded file</option>
</b-select>
</b-field>
<b-field label="File"
v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted'"
:message="outputFileTemplateSettings['${tmpl['setting_file']}'] ? 'This file lives on disk at: ${output_file_option_dirs[tmpl['key']]}' : null">
<b-select name="${tmpl['setting_file']}"
v-model="outputFileTemplateSettings['${tmpl['setting_file']}']"
@input="settingsNeedSaved = true">
<option value="">-new-</option>
<option v-for="option in outputFileTemplateFileOptions['${tmpl['key']}']"
:key="option"
:value="option">
{{ option }}
</option>
</b-select>
</b-field>
<b-field label="Upload"
v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !outputFileTemplateSettings['${tmpl['setting_file']}']">
% if request.use_oruga:
<o-field class="file">
<o-upload name="${tmpl['setting_file']}.upload"
v-model="outputFileTemplateUploads['${tmpl['key']}']"
v-slot="{ onclick }"
@input="settingsNeedSaved = true">
<o-button variant="primary"
@click="onclick">
<o-icon icon="upload" />
<span>Click to upload</span>
</o-button>
<span class="file-name" v-if="outputFileTemplateUploads['${tmpl['key']}']">
{{ outputFileTemplateUploads['${tmpl['key']}'].name }}
</span>
</o-upload>
</o-field>
% else:
<b-field class="file is-primary"
:class="{'has-name': !!outputFileTemplateSettings['${tmpl['setting_file']}']}">
<b-upload name="${tmpl['setting_file']}.upload"
v-model="outputFileTemplateUploads['${tmpl['key']}']"
class="file-label"
@input="settingsNeedSaved = true">
<span class="file-cta">
<b-icon class="file-icon" pack="fas" icon="upload"></b-icon>
<span class="file-label">Click to upload</span>
</span>
</b-upload>
<span v-if="outputFileTemplateUploads['${tmpl['key']}']"
class="file-name">
{{ outputFileTemplateUploads['${tmpl['key']}'].name }}
</span>
</b-field>
% endif
</b-field>
</b-field>
</%def>
<%def name="output_file_templates_section()">
<h3 class="block is-size-3">Output File Templates</h3>
<div class="block" style="padding-left: 2rem;">
% for key in output_file_templates:
${self.output_file_template_field(key)}
% endfor
</div>
</%def>
<%def name="form_content()"></%def>
<%def name="page_content()">
@ -183,15 +280,14 @@
<b-button @click="purgeSettingsShowDialog = false">
Cancel
</b-button>
${h.form(request.current_route_url())}
${h.form(request.current_route_url(), **{'@submit': 'purgingSettings = true'})}
${h.csrf_token(request)}
${h.hidden('remove_settings', 'true')}
<b-button type="is-danger"
native-type="submit"
:disabled="purgingSettings"
icon-pack="fas"
icon-left="trash"
@click="purgingSettings = true">
icon-left="trash">
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }}
</b-button>
${h.end_form()}
@ -205,62 +301,42 @@
${h.end_form()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if simple_settings is not Undefined:
ThisPageData.simpleSettings = ${json.dumps(simple_settings)|n}
% endif
% if input_file_template_settings is not Undefined:
ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n}
ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n}
ThisPageData.inputFileTemplateUploads = {
% for key in input_file_templates:
'${key}': null,
% endfor
}
% endif
ThisPageData.purgeSettingsShowDialog = false
ThisPageData.purgingSettings = false
ThisPageData.settingsNeedSaved = false
ThisPageData.undoChanges = false
ThisPageData.savingSettings = false
ThisPageData.validators = []
ThisPage.methods.purgeSettingsInit = function() {
this.purgeSettingsShowDialog = true
}
% if input_file_template_settings is not Undefined:
ThisPage.methods.validateInputFileTemplateSettings = function() {
% for tmpl in input_file_templates.values():
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
return "You must provide a file to upload for the ${tmpl['label']} template."
}
}
}
% endfor
}
% endif
ThisPage.methods.validateSettings = function() {
let msg
% if input_file_template_settings is not Undefined:
msg = this.validateInputFileTemplateSettings()
if (msg) {
return msg
}
% endif
}
ThisPage.methods.validateSettings = function() {}
ThisPage.methods.saveSettings = function() {
let msg = this.validateSettings()
let msg
// nb. this is the future
for (let validator of this.validators) {
msg = validator.call(this)
if (msg) {
alert(msg)
return
}
}
// nb. legacy method
msg = this.validateSettings()
if (msg) {
alert(msg)
return
@ -291,8 +367,65 @@
window.addEventListener('beforeunload', this.beforeWindowUnload)
}
##############################
## input file templates
##############################
% if input_file_template_settings is not Undefined:
ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n}
ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n}
ThisPageData.inputFileTemplateUploads = {
% for key in input_file_templates:
'${key}': null,
% endfor
}
ThisPage.methods.validateInputFileTemplateSettings = function() {
% for tmpl in input_file_templates.values():
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
return "You must provide a file to upload for the ${tmpl['label']} template."
}
}
}
% endfor
}
ThisPageData.validators.push(ThisPage.methods.validateInputFileTemplateSettings)
% endif
##############################
## output file templates
##############################
% if output_file_template_settings is not Undefined:
ThisPageData.outputFileTemplateSettings = ${json.dumps(output_file_template_settings)|n}
ThisPageData.outputFileTemplateFileOptions = ${json.dumps(output_file_options)|n}
ThisPageData.outputFileTemplateUploads = {
% for key in output_file_templates:
'${key}': null,
% endfor
}
ThisPage.methods.validateOutputFileTemplateSettings = function() {
% for tmpl in output_file_templates.values():
if (this.outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
if (!this.outputFileTemplateSettings['${tmpl['setting_file']}']) {
if (!this.outputFileTemplateUploads['${tmpl['key']}']) {
return "You must provide a file to upload for the ${tmpl['label']} template."
}
}
}
% endfor
}
ThisPageData.validators.push(ThisPage.methods.validateOutputFileTemplateSettings)
% endif
</script>
</%def>
${parent.body()}

View file

@ -88,9 +88,9 @@
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPage.methods.getLabelForKey = function(key) {
switch (key) {
@ -111,6 +111,3 @@
</script>
</%def>
${parent.body()}

View file

@ -106,9 +106,9 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.resolvePersonShowDialog = false
ThisPageData.resolvePersonUUID = null
@ -139,5 +139,3 @@
</script>
</%def>
${parent.body()}

View file

@ -16,15 +16,15 @@
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if expose_shoppers:
${form.component_studly}Data.shoppers = ${json.dumps(shoppers_data)|n}
${form.vue_component}Data.shoppers = ${json.dumps(shoppers_data)|n}
% endif
% if expose_people:
${form.component_studly}Data.peopleData = ${json.dumps(people_data)|n}
${form.vue_component}Data.peopleData = ${json.dumps(people_data)|n}
% endif
ThisPage.methods.detachPerson = function(url) {
@ -36,5 +36,3 @@
</script>
</%def>
${parent.body()}

View file

@ -47,10 +47,9 @@
</div>
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${product_lookup.tailbone_product_lookup_template()}
<script type="text/x-template" id="customer-order-creator-template">
<div>
@ -1265,12 +1264,7 @@
</div>
</script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
${product_lookup.tailbone_product_lookup_component()}
<script type="text/javascript">
<script>
const CustomerOrderCreator = {
template: '#customer-order-creator-template',
@ -2406,5 +2400,7 @@
</script>
</%def>
${parent.body()}
<%def name="make_vue_components()">
${parent.make_vue_components()}
${product_lookup.tailbone_product_lookup_component()}
</%def>

View file

@ -291,11 +291,11 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
${form.component_studly}Data.eventsData = ${json.dumps(events_data)|n}
${form.vue_component}Data.eventsData = ${json.dumps(events_data)|n}
% if master.has_perm('confirm_price'):
@ -392,9 +392,9 @@
this.$refs.changeStatusForm.submit()
}
${form.component_studly}Data.changeFlaggedSubmitting = false
${form.vue_component}Data.changeFlaggedSubmitting = false
${form.component_studly}.methods.changeFlaggedSubmit = function() {
${form.vue_component}.methods.changeFlaggedSubmit = function() {
this.changeFlaggedSubmitting = true
}
@ -448,5 +448,3 @@
</script>
</%def>
${parent.body()}

View file

@ -26,9 +26,9 @@
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if request.has_perm('datasync.restart'):
TailboneGridData.restartDatasyncFormSubmitting = false
@ -50,6 +50,3 @@
</script>
</%def>
${parent.body()}

View file

@ -83,8 +83,8 @@
</b-notification>
<b-field>
<b-checkbox name="use_profile_settings"
v-model="useProfileSettings"
<b-checkbox name="rattail.datasync.use_profile_settings"
v-model="simpleSettings['rattail.datasync.use_profile_settings']"
native-value="true"
@input="settingsNeedSaved = true">
Use these Settings to configure watchers and consumers
@ -99,7 +99,7 @@
</div>
<div class="level-right">
<div class="level-item"
v-show="useProfileSettings">
v-show="simpleSettings['rattail.datasync.use_profile_settings']">
<b-button type="is-primary"
@click="newProfile()"
icon-pack="fas"
@ -162,7 +162,7 @@
</${b}-table-column>
<${b}-table-column label="Actions"
v-slot="props"
v-if="useProfileSettings">
v-if="simpleSettings['rattail.datasync.use_profile_settings']">
<a href="#"
class="grid-action"
@click.prevent="editProfile(props.row)">
@ -580,18 +580,27 @@
<b-field label="Supervisor Process Name"
message="This should be the complete name, including group - e.g. poser:poser_datasync"
expanded>
<b-input name="supervisor_process_name"
v-model="supervisorProcessName"
<b-input name="rattail.datasync.supervisor_process_name"
v-model="simpleSettings['rattail.datasync.supervisor_process_name']"
@input="settingsNeedSaved = true"
expanded>
</b-input>
</b-field>
<b-field label="Consumer Batch Size"
message="Max number of changes to be consumed at once."
expanded>
<numeric-input name="rattail.datasync.batch_size_limit"
v-model="simpleSettings['rattail.datasync.batch_size_limit']"
@input="settingsNeedSaved = true" />
</b-field>
<h3 class="is-size-3">Legacy</h3>
<b-field label="Restart Command"
message="This will run as '${system_user}' system user - please configure sudoers as needed. Typical command is like: sudo supervisorctl restart poser:poser_datasync"
expanded>
<b-input name="restart_command"
v-model="restartCommand"
<b-input name="tailbone.datasync.restart"
v-model="simpleSettings['tailbone.datasync.restart']"
@input="settingsNeedSaved = true"
expanded>
</b-input>
@ -599,14 +608,13 @@
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.showConfigFilesNote = false
ThisPageData.profilesData = ${json.dumps(profiles_data)|n}
ThisPageData.showDisabledProfiles = false
ThisPageData.useProfileSettings = ${json.dumps(use_profile_settings)|n}
ThisPageData.editProfileShowDialog = false
ThisPageData.editingProfile = null
@ -631,9 +639,6 @@
ThisPageData.editingConsumerRunas = null
ThisPageData.editingConsumerEnabled = true
ThisPageData.supervisorProcessName = ${json.dumps(supervisor_process_name)|n}
ThisPageData.restartCommand = ${json.dumps(restart_command)|n}
ThisPage.computed.updateConsumerDisabled = function() {
if (!this.editingConsumerKey) {
return true
@ -734,16 +739,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 +789,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 +801,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 +854,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 +943,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
@ -983,6 +987,3 @@
</script>
</%def>
${parent.body()}

View file

@ -115,8 +115,9 @@
</${b}-table>
</%def>
<%def name="modify_this_page_vars()">
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.processInfo = ${json.dumps(process_info)|n}
@ -171,6 +172,3 @@
</script>
</%def>
${parent.body()}

View file

@ -1,6 +1,7 @@
<div i18n:domain="deform" tal:omit-tag=""
tal:define="oid oid|field.oid;
name name|field.name;
vmodel vmodel|'field_model_' + name;
css_class css_class|field.widget.css_class;
style style|field.widget.style;">
@ -8,7 +9,7 @@
${field.start_mapping()}
<b-input type="password"
name="${name}"
value="${field.widget.redisplay and cstruct or ''}"
v-model="${vmodel}"
tal:attributes="class string: form-control ${css_class or ''};
style style;
attributes|field.widget.attributes|{};"
@ -18,7 +19,6 @@
</b-input>
<b-input type="password"
name="${name}-confirm"
value="${field.widget.redisplay and confirm or ''}"
tal:attributes="class string: form-control ${css_class or ''};
style style;
confirm_attributes|field.widget.confirm_attributes|{};"

View file

@ -1,13 +1,9 @@
## -*- coding: utf-8; -*-
<%inherit file="/master/view.mako" />
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
${form.component_studly}Data.employeesData = ${json.dumps(employees_data)|n}
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
${form.vue_component}Data.employeesData = ${json.dumps(employees_data)|n}
</script>
</%def>
${parent.body()}

View file

@ -6,20 +6,63 @@
<%def name="render_form_buttons()"></%def>
<%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>
<%def name="render_form()">
<div class="form">
${form.render_vuejs_component()}
${form.render_vue_tag()}
</div>
</%def>
<%def name="page_content()">
<div class="form-wrapper">
<br />
${self.render_form()}
</div>
% if main_form_collapsible:
<${b}-collapse class="panel"
% if request.use_oruga:
v-model:open="mainFormPanelOpen"
% else:
:open.sync="mainFormPanelOpen"
% endif
>
<template #trigger="props">
<div class="panel-heading"
role="button"
style="cursor: pointer;">
## TODO: for some reason buefy will "reuse" the icon
## element in such a way that its display does not
## refresh. so to work around that, we use different
## structure for the two icons, so buefy is forced to
## re-draw
<b-icon v-if="props.open"
pack="fas"
icon="caret-down">
</b-icon>
<span v-if="!props.open">
<b-icon pack="fas"
icon="caret-right">
</b-icon>
</span>
&nbsp;
<strong>${main_form_title}</strong>
</div>
</template>
<div class="panel-block">
<div class="form-wrapper">
<br />
${self.render_form()}
</div>
</div>
</${b}-collapse>
% else:
<div class="form-wrapper">
<br />
${self.render_form()}
</div>
% endif
</%def>
<%def name="render_this_page()">
@ -47,25 +90,25 @@
<%def name="before_object_helpers()"></%def>
<%def name="render_this_page_template()">
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
% if form is not Undefined:
${self.render_form_template()}
% endif
${parent.render_this_page_template()}
</%def>
<%def name="finalize_this_page_vars()">
${parent.finalize_this_page_vars()}
% if form is not Undefined:
<script type="text/javascript">
${form.component_studly}.data = function() { return ${form.component_studly}Data }
Vue.component('${form.component}', ${form.component_studly})
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
% if main_form_collapsible:
<script>
ThisPageData.mainFormPanelOpen = ${'false' if main_form_autocollapse else 'true'}
</script>
% endif
</%def>
${parent.body()}
<%def name="make_vue_components()">
${parent.make_vue_components()}
% if form is not Undefined:
${form.render_vue_finalize()}
% endif
</%def>

View file

@ -39,7 +39,7 @@
simplePOST(action, params, success, failure) {
let csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}
let csrftoken = ${json.dumps(h.get_csrf_token(request))|n}
let headers = {
'${csrf_header_name}': csrftoken,

View file

@ -1,19 +1,19 @@
## -*- coding: utf-8; -*-
<% request.register_component(form.component, form.component_studly) %>
<% request.register_component(form.vue_tagname, form.vue_component) %>
<script type="text/x-template" id="${form.component}-template">
<script type="text/x-template" id="${form.vue_tagname}-template">
<div>
% if not form.readonly:
${h.form(form.action_url, id=dform.formid, method='post', enctype='multipart/form-data', **form_kwargs)}
${h.form(form.action_url, id=dform.formid, method='post', enctype='multipart/form-data', **(form_kwargs or {}))}
${h.csrf_token(request)}
% endif
<section>
% if form_body is not Undefined and form_body:
${form_body|n}
% elif form.grouping:
% elif getattr(form, 'grouping', None):
% for group in form.grouping:
<nav class="panel">
<p class="panel-heading">${group}</p>
@ -27,8 +27,8 @@
</nav>
% endfor
% else:
% for field in form.fields:
${form.render_field_complete(field)}
% for fieldname in form.fields:
${form.render_vue_field(fieldname, session=session)}
% endfor
% endif
</section>
@ -54,20 +54,20 @@
<input type="reset" value="Reset" class="button" />
% endif
## TODO: deprecate / remove the latter option here
% if form.auto_disable_save or form.auto_disable:
% if getattr(form, 'auto_disable_submit', False) or form.auto_disable_save or form.auto_disable:
<b-button type="is-primary"
native-type="submit"
:disabled="${form.component_studly}Submitting"
:disabled="${form.vue_component}Submitting"
icon-pack="fas"
icon-left="save">
{{ ${form.component_studly}ButtonText }}
icon-left="${form.button_icon_submit}">
{{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }}
</b-button>
% else:
<b-button type="is-primary"
native-type="submit"
icon-pack="fas"
icon-left="save">
${getattr(form, 'submit_label', getattr(form, 'save_label', "Submit"))}
${form.button_label_submit}
</b-button>
% endif
</div>
@ -122,8 +122,8 @@
<script type="text/javascript">
let ${form.component_studly} = {
template: '#${form.component}-template',
let ${form.vue_component} = {
template: '#${form.vue_tagname}-template',
mixins: [FormPosterMixin],
components: {},
props: {
@ -136,10 +136,9 @@
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..."
% if getattr(form, 'auto_disable_submit', False) or form.auto_disable_save or form.auto_disable:
submit${form.vue_component}() {
this.${form.vue_component}Submitting = true
},
% endif
@ -178,10 +177,10 @@
}
}
let ${form.component_studly}Data = {
let ${form.vue_component}Data = {
## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
csrftoken: ${json.dumps(h.get_csrf_token(request))|n},
% if can_edit_help:
fieldLabels: ${json.dumps(field_labels)|n},
@ -198,16 +197,14 @@
% if not form.readonly:
% for field in form.fields:
% if field in dform:
<% field = dform[field] %>
field_model_${field.name}: ${form.get_vuejs_model_value(field)|n},
field_model_${field}: ${json.dumps(form.get_vue_field_value(field))|n},
% endif
% endfor
% endif
## 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},
% if getattr(form, 'auto_disable_submit', False) or form.auto_disable_save or form.auto_disable:
${form.vue_component}Submitting: false,
% endif
}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8; -*-
<%inherit file="/forms/deform.mako" />
${parent.body()}

View file

@ -276,9 +276,9 @@
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.featureType = ${json.dumps(feature_type)|n}
ThisPageData.resultGenerated = ${json.dumps(bool(result))|n}
@ -385,6 +385,3 @@
</script>
</%def>
${parent.body()}

View file

@ -53,11 +53,11 @@
</${b}-table-column>
% 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:
<a :href="props.row._action_url_${action.key}"
% if action.link_class:
class="${action.link_class}"
@ -68,12 +68,7 @@
@click.prevent="${action.click_handler}"
% endif
>
% if request.use_oruga:
<o-icon icon="${action.icon}" />
% else:
<i class="fas fa-${action.icon}"></i>
% endif
${action.label}
${action.render_icon_and_label()}
</a>
&nbsp;
% endfor

View file

@ -1,17 +1,79 @@
## -*- coding: utf-8; -*-
<% request.register_component(grid.component, grid.component_studly) %>
<% request.register_component(grid.vue_tagname, grid.vue_component) %>
<script type="text/x-template" id="${grid.component}-template">
<script type="text/x-template" id="${grid.vue_tagname}-template">
<div>
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5em;">
<div style="display: flex; flex-direction: column; justify-content: end;">
<div class="filters">
% if grid.filterable:
## TODO: stop using |n filter
${grid.render_filters(allow_save_defaults=allow_save_defaults)|n}
% if getattr(grid, 'filterable', False):
<form method="GET" @submit.prevent="applyFilters()">
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<grid-filter v-for="key in filtersSequence"
:key="key"
:filter="filters[key]"
ref="gridFilters">
</grid-filter>
</div>
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
<b-button type="is-primary"
native-type="submit"
icon-pack="fas"
icon-left="check">
Apply Filters
</b-button>
<b-button v-if="!addFilterShow"
icon-pack="fas"
icon-left="plus"
@click="addFilterInit()">
Add Filter
</b-button>
<b-autocomplete v-if="addFilterShow"
ref="addFilterAutocomplete"
:data="addFilterChoices"
v-model="addFilterTerm"
placeholder="Add Filter"
field="key"
:custom-formatter="formatAddFilterItem"
open-on-focus
keep-first
icon-pack="fas"
clearable
clear-on-select
@select="addFilterSelect">
</b-autocomplete>
<b-button @click="resetView()"
icon-pack="fas"
icon-left="home">
Default View
</b-button>
<b-button @click="clearFilters()"
icon-pack="fas"
icon-left="trash">
No Filters
</b-button>
% if allow_save_defaults and request.user:
<b-button @click="saveDefaults()"
icon-pack="fas"
icon-left="save"
:disabled="savingDefaults">
{{ savingDefaults ? "Working, please wait..." : "Save Defaults" }}
</b-button>
% endif
</div>
</form>
% endif
</div>
</div>
@ -55,7 +117,7 @@
:checkable="checkable"
% if grid.checkboxes:
% if getattr(grid, 'checkboxes', False):
% if request.use_oruga:
v-model:checked-rows="checkedRows"
% else:
@ -66,51 +128,64 @@
% endif
% endif
% if grid.check_handler:
% if getattr(grid, 'check_handler', None):
@check="${grid.check_handler}"
% endif
% if grid.check_all_handler:
% if getattr(grid, 'check_all_handler', None):
@check-all="${grid.check_all_handler}"
% endif
% if hasattr(grid, 'checkable'):
% if isinstance(grid.checkable, str):
:is-row-checkable="${grid.row_checkable}"
% elif grid.checkable:
:is-row-checkable="row => row._checkable"
% endif
% if grid.sortable:
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. 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"
## user must ctrl-click column header to do multi-sort
sort-multiple-key="ctrlKey"
% endif
% if grid.click_handlers:
## sorting
% if grid.sortable:
## 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):
@cellclick="cellClick"
% endif
:paginated="paginated"
:per-page="perPage"
:current-page="currentPage"
backend-pagination
:total="total"
@page-change="onPageChange"
## paging
% if grid.paginated:
paginated
pagination-size="${'small' if request.use_oruga else '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?
icon-pack="fas"
@ -119,17 +194,15 @@
:hoverable="true"
:narrowed="true">
% for column in grid_columns:
% for column in grid.get_vue_columns():
<${b}-table-column field="${column['field']}"
label="${column['label']}"
v-slot="props"
:sortable="${json.dumps(column['sortable'])}"
% if grid.is_searchable(column['field']):
searchable
% endif
:sortable="${json.dumps(column.get('sortable', False))|n}"
:searchable="${json.dumps(column.get('searchable', False))|n}"
cell-class="c_${column['field']}"
:visible="${json.dumps(column['visible'])}">
% if column['field'] in grid.raw_renderers:
:visible="${json.dumps(column.get('visible', True))}">
% if hasattr(grid, 'raw_renderers') and column['field'] in grid.raw_renderers:
${grid.raw_renderers[column['field']]()}
% elif grid.is_linked(column['field']):
<a :href="props.row._action_url_view"
@ -144,30 +217,24 @@
</${b}-table-column>
% endfor
% if grid.main_actions or grid.more_actions:
% if grid.actions:
<${b}-table-column field="actions"
label="Actions"
v-slot="props">
## TODO: we do not currently differentiate for "main vs. more"
## here, but ideally we would tuck "more" away in a drawer etc.
% for action in grid.main_actions + grid.more_actions:
% for action in grid.actions:
<a v-if="props.row._action_url_${action.key}"
:href="props.row._action_url_${action.key}"
class="grid-action${' has-text-danger' if action.key == 'delete' else ''} ${action.link_class or ''}"
% if action.click_handler:
% if getattr(action, 'click_handler', None):
@click.prevent="${action.click_handler}"
% endif
% if action.target:
% if getattr(action, 'target', None):
target="${action.target}"
% endif
>
% if request.use_oruga:
<o-icon icon="${action.icon}" />
<span>${action.render_label()|n}</span>
% else:
${action.render_icon()|n}
${action.render_label()|n}
% endif
${action.render_icon_and_label()}
</a>
&nbsp;
% endfor
@ -192,7 +259,7 @@
<template #footer>
<div style="display: flex; justify-content: space-between;">
% if grid.expose_direct_link:
% if getattr(grid, 'expose_direct_link', False):
<b-button type="is-primary"
size="is-small"
@click="copyDirectLink()"
@ -207,13 +274,14 @@
<div></div>
% endif
% if grid.pageable:
<div v-if="firstItem"
% if grid.paginated:
<div v-if="pagerStats.first_item"
style="display: flex; gap: 0.5rem; align-items: center;">
<span>
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;
</span>
<b-select v-model="perPage"
size="is-small"
@ -234,7 +302,7 @@
</${b}-table>
## dummy input field needed for sharing links on *insecure* sites
% if request.scheme == 'http':
% if getattr(request, 'scheme', None) == 'http':
<b-input v-model="shareLink" ref="shareLink" v-show="shareLink"></b-input>
% endif
@ -243,65 +311,72 @@
<script type="text/javascript">
let ${grid.component_studly}CurrentData = ${json.dumps(grid_data['data'])|n}
const ${grid.vue_component}Context = ${json.dumps(grid.get_vue_context())|n}
let ${grid.vue_component}CurrentData = ${grid.vue_component}Context.data
let ${grid.component_studly}Data = {
let ${grid.vue_component}Data = {
loading: false,
ajaxDataUrl: ${json.dumps(grid.ajax_data_url)|n},
ajaxDataUrl: ${json.dumps(getattr(grid, 'ajax_data_url', request.path_url))|n},
## nb. this tracks whether grid.fetchFirstData() happened
fetchedFirstData: false,
savingDefaults: false,
data: ${grid.component_studly}CurrentData,
rowStatusMap: ${json.dumps(grid_data['row_status_map'])|n},
data: ${grid.vue_component}CurrentData,
rowStatusMap: ${json.dumps(grid_data['row_status_map'] if grid_data is not Undefined else {})|n},
checkable: ${json.dumps(grid.checkboxes)|n},
% if grid.checkboxes:
checkable: ${json.dumps(getattr(grid, 'checkboxes', False))|n},
% if getattr(grid, 'checkboxes', False):
checkedRows: ${grid_data['checked_rows_code']|n},
% endif
paginated: ${json.dumps(grid.pageable)|n},
total: ${len(grid_data['data']) if static_data else grid_data['total_items']},
perPage: ${json.dumps(grid.pagesize if grid.pageable else None)|n},
currentPage: ${json.dumps(grid.page if grid.pageable else None)|n},
firstItem: ${json.dumps(grid_data['first_item'] if grid.pageable else None)|n},
lastItem: ${json.dumps(grid_data['last_item'] if grid.pageable else None)|n},
% if grid.sortable:
## 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 contains all truly active sorters
backendSorters: ${json.dumps(grid.active_sorters)|n},
## nb. whereas 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: [],
## paging
% if grid.paginated:
pageSizeOptions: ${json.dumps(grid.pagesize_options)|n},
perPage: ${json.dumps(grid.pagesize)|n},
currentPage: ${json.dumps(grid.page)|n},
% if grid.paginate_on_backend:
pagerStats: ${json.dumps(grid.get_vue_pager_stats())|n},
% endif
% endif
## sorting
% if grid.sortable:
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},
filters: ${json.dumps(filters_data if grid.filterable else None)|n},
filtersSequence: ${json.dumps(filters_sequence if grid.filterable else None)|n},
filters: ${json.dumps(filters_data if getattr(grid, 'filterable', False) else None)|n},
filtersSequence: ${json.dumps(filters_sequence if getattr(grid, 'filterable', False) else None)|n},
addFilterTerm: '',
addFilterShow: false,
## dummy input value needed for sharing links on *insecure* sites
% if request.scheme == 'http':
% if getattr(request, 'scheme', None) == 'http':
shareLink: null,
% endif
}
let ${grid.component_studly} = {
template: '#${grid.component}-template',
let ${grid.vue_component} = {
template: '#${grid.vue_tagname}-template',
mixins: [FormPosterMixin],
@ -311,6 +386,32 @@
computed: {
## TODO: this should be temporary? but anyway 'total' is
## still referenced in other places, e.g. "delete results"
% if grid.paginated:
total() { return this.pagerStats.item_count },
% endif
% if not grid.paginate_on_backend:
pagerStats() {
const data = this.visibleData
let last = this.currentPage * this.perPage
let first = last - this.perPage + 1
if (last > 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
@ -358,21 +459,32 @@
directLink() {
let params = new URLSearchParams(this.getAllParams())
return `${request.current_route_url(_query=None)}?${'$'}{params}`
return `${request.path_url}?${'$'}{params}`
},
},
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: {
renderNumber(value) {
if (value != undefined) {
return value.toLocaleString('en')
}
},
formatAddFilterItem(filtr) {
if (!filtr.key) {
filtr = this.filters[filtr]
@ -380,7 +492,7 @@
return filtr.label || filtr.key
},
% if grid.click_handlers:
% if getattr(grid, 'click_handlers', None):
cellClick(row, column, rowIndex, columnIndex) {
% for key in grid.click_handlers:
if (column._props.field == '${key}') {
@ -436,17 +548,18 @@
},
getBasicParams() {
let params = {}
% if grid.sortable:
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].field
params['sort'+i+'dir'] = this.sorters[i-1].order
}
% endif
% if grid.pageable:
params.pagesize = this.perPage
params.page = this.currentPage
% endif
return params
},
@ -470,6 +583,17 @@
...this.getFilterParams()}
},
## nb. this is meant to call for a grid which is hidden at
## first, when it is first being shown to the user. and if
## it was initialized with empty data set.
async fetchFirstData() {
if (this.fetchedFirstData) {
return
}
await this.loadAsyncData()
this.fetchedFirstData = true
},
## TODO: i noticed buefy docs show using `async` keyword here,
## so now i am too. knowing nothing at all of if/how this is
## supposed to improve anything. we shall see i guess
@ -480,27 +604,29 @@
} else {
params = new URLSearchParams(params)
}
params.append('partial', true)
if (!params.has('partial')) {
params.append('partial', true)
}
params = params.toString()
this.loading = true
this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(({ data }) => {
if (!data.error) {
${grid.component_studly}CurrentData = data.data
this.data = ${grid.component_studly}CurrentData
this.rowStatusMap = data.row_status_map
this.total = data.total_items
this.firstItem = data.first_item
this.lastItem = data.last_item
this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(response => {
if (!response.data.error) {
${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.row_status_map || {}
this.loading = false
this.savingDefaults = false
this.checkedRows = this.locateCheckedRows(data.checked_rows)
this.checkedRows = this.locateCheckedRows(response.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
})
@ -512,8 +638,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) {
@ -552,55 +681,72 @@
})
},
onSort(field, order, event) {
% if grid.sortable and grid.sort_on_backend:
// nb. buefy passes field name, oruga passes object
if (field.field) {
field = field.field
}
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
let sorter = this.backendSorters.filter(i => i.field === field)[0]
if (sorter) {
sorter.order = sorter.order === 'desc' ? 'asc' : 'desc'
} else {
this.backendSorters.push({field, order})
}
this.sortingPriority = this.backendSorters
% 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.backendSorters = [{field, 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.backendSorters = this.backendSorters.filter(
(sorter) => sorter.field !== 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.backendSorters
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
// use current url proper, plus reset param
let url = '?reset-to-default-filters=true'
let url = '?reset-view=true'
// add current hash, to preserve that in redirect
if (location.hash) {
@ -774,7 +920,7 @@
} else {
this.checkedRows.push(row)
}
% if grid.check_handler:
% if getattr(grid, 'check_handler', None):
this.${grid.check_handler}(this.checkedRows, row)
% endif
},

View file

@ -1,67 +0,0 @@
## -*- coding: utf-8; -*-
<form action="${form.action_url}" method="GET" @submit.prevent="applyFilters()">
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
<grid-filter v-for="key in filtersSequence"
:key="key"
:filter="filters[key]"
ref="gridFilters">
</grid-filter>
</div>
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
<b-button type="is-primary"
native-type="submit"
icon-pack="fas"
icon-left="check">
Apply Filters
</b-button>
<b-button v-if="!addFilterShow"
icon-pack="fas"
icon-left="plus"
@click="addFilterInit()">
Add Filter
</b-button>
<b-autocomplete v-if="addFilterShow"
ref="addFilterAutocomplete"
:data="addFilterChoices"
v-model="addFilterTerm"
placeholder="Add Filter"
field="key"
:custom-formatter="formatAddFilterItem"
open-on-focus
keep-first
icon-pack="fas"
clearable
clear-on-select
@select="addFilterSelect">
</b-autocomplete>
<b-button @click="resetView()"
icon-pack="fas"
icon-left="home">
Default View
</b-button>
<b-button @click="clearFilters()"
icon-pack="fas"
icon-left="trash">
No Filters
</b-button>
% if allow_save_defaults and request.user:
<b-button @click="saveDefaults()"
icon-pack="fas"
icon-left="save"
:disabled="savingDefaults">
{{ savingDefaults ? "Working, please wait..." : "Save Defaults" }}
</b-button>
% endif
</div>
</form>

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8; -*-
<%inherit file="/grids/complete.mako" />
${parent.body()}

View file

@ -1,33 +1,7 @@
## -*- coding: utf-8; -*-
<%inherit file="/page.mako" />
<%namespace name="base_meta" file="/base_meta.mako" />
<%def name="title()">Home</%def>
<%def name="extra_styles()">
${parent.extra_styles()}
<style type="text/css">
.logo {
text-align: center;
}
.logo img {
margin: 3em auto;
max-height: 350px;
max-width: 800px;
}
</style>
</%def>
<%inherit file="wuttaweb:templates/home.mako" />
## DEPRECATED; remains for back-compat
<%def name="render_this_page()">
${self.page_content()}
</%def>
<%def name="page_content()">
<div class="logo">
${h.image(image_url, "{} logo".format(capture(base_meta.app_title)))}
<h1>Welcome to ${base_meta.app_title()}</h1>
</div>
</%def>
${parent.body()}

View file

@ -144,9 +144,9 @@
</b-modal>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.handlersData = ${json.dumps(handlers_data)|n}
@ -203,6 +203,3 @@
</script>
</%def>
${parent.body()}

View file

@ -63,28 +63,26 @@
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
${form.component_studly}Data.submittingRun = false
${form.component_studly}Data.submittingExplain = false
${form.component_studly}Data.runJob = false
${form.vue_component}Data.submittingRun = false
${form.vue_component}Data.submittingExplain = false
${form.vue_component}Data.runJob = false
${form.component_studly}.methods.submitRun = function() {
${form.vue_component}.methods.submitRun = function() {
this.submittingRun = true
this.runJob = true
this.$nextTick(() => {
this.$refs.${form.component_studly}.submit()
this.$refs.${form.vue_component}.submit()
})
}
${form.component_studly}.methods.submitExplain = function() {
${form.vue_component}.methods.submitExplain = function() {
this.submittingExplain = true
this.$refs.${form.component_studly}.submit()
this.$refs.${form.vue_component}.submit()
}
</script>
</%def>
${parent.body()}

View file

@ -1,86 +1,17 @@
## -*- coding: utf-8; -*-
<%inherit file="/form.mako" />
<%namespace name="base_meta" file="/base_meta.mako" />
<%def name="title()">Login</%def>
<%inherit file="wuttaweb:templates/auth/login.mako" />
## TODO: this will not be needed with wuttaform
<%def name="extra_styles()">
${parent.extra_styles()}
<style type="text/css">
.logo img {
display: block;
margin: 3rem auto;
max-height: 350px;
max-width: 800px;
}
/* must force a particular label with, in order to make sure */
/* the username and password inputs are the same size */
.field.is-horizontal .field-label .label {
text-align: left;
width: 6rem;
}
.buttons {
<style>
.card-content .buttons {
justify-content: right;
}
</style>
</%def>
<%def name="logo()">
${h.image(image_url, "{} logo".format(capture(base_meta.app_title)))}
</%def>
<%def name="login_form()">
<div class="form">
${form.render_deform(form_kwargs={'data-ajax': 'false'})|n}
</div>
</%def>
## DEPRECATED; remains for back-compat
<%def name="render_this_page()">
${self.page_content()}
</%def>
<%def name="page_content()">
<div class="logo">
${self.logo()}
</div>
<div class="columns is-centered">
<div class="column is-narrow">
<div class="card">
<div class="card-content">
<tailbone-form></tailbone-form>
</div>
</div>
</div>
</div>
</%def>
<%def name="modify_this_page_vars()">
<script type="text/javascript">
${form.component_studly}Data.usernameInput = null
${form.component_studly}.mounted = function() {
this.$refs.username.focus()
this.usernameInput = this.$refs.username.$el.querySelector('input')
this.usernameInput.addEventListener('keydown', this.usernameKeydown)
}
${form.component_studly}.beforeDestroy = function() {
this.usernameInput.removeEventListener('keydown', this.usernameKeydown)
}
${form.component_studly}.methods.usernameKeydown = function(event) {
if (event.which == 13) {
event.preventDefault()
this.$refs.password.focus()
}
}
</script>
</%def>
${parent.body()}

View file

@ -297,9 +297,9 @@
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.overnightTasks = ${json.dumps(overnight_tasks)|n}
ThisPageData.overnightTaskShowDialog = false
@ -425,6 +425,3 @@
</script>
</%def>
${parent.body()}

View file

@ -79,8 +79,13 @@
@click="overnightTaskLaunchInit(props.row)">
Launch
</b-button>
<b-modal has-modal-card
:active.sync="overnightTaskShowLaunchDialog">
<${b}-modal has-modal-card
% if request.use_oruga:
v-model:active="overnightTaskShowLaunchDialog"
% else:
:active.sync="overnightTaskShowLaunchDialog"
% endif
>
<div class="modal-card">
<header class="modal-card-head">
@ -127,7 +132,7 @@
</b-button>
</footer>
</div>
</b-modal>
</${b}-modal>
</${b}-table-column>
<template #empty>
<p class="block">No tasks defined.</p>
@ -182,8 +187,13 @@
</template>
</${b}-table>
<b-modal has-modal-card
:active.sync="backfillTaskShowLaunchDialog">
<${b}-modal has-modal-card
% if request.use_oruga:
v-model:active="backfillTaskShowLaunchDialog"
% else:
:active.sync="backfillTaskShowLaunchDialog"
% endif
>
<div class="modal-card">
<header class="modal-card-head">
@ -238,16 +248,16 @@
</b-button>
</footer>
</div>
</b-modal>
</${b}-modal>
% endif
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if master.has_perm('restart_scheduler'):
@ -364,6 +374,3 @@
</script>
</%def>
${parent.body()}

View file

@ -34,9 +34,9 @@
${h.end_form()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
TailboneFormData.formSubmitting = false
TailboneFormData.submitButtonText = "Yes, please clone away"
@ -48,6 +48,3 @@
</script>
</%def>
${parent.body()}

View file

@ -1,6 +1,6 @@
## -*- coding: utf-8; -*-
<%inherit file="/form.mako" />
<%def name="title()">New ${model_title_plural if master.creates_multiple else model_title}</%def>
<%def name="title()">New ${model_title_plural if getattr(master, 'creates_multiple', False) else model_title}</%def>
${parent.body()}

View file

@ -27,26 +27,21 @@
<b-button type="is-primary is-danger"
native-type="submit"
:disabled="formSubmitting">
{{ formButtonText }}
{{ formSubmitting ? "Working, please wait..." : "${form.button_label_submit}" }}
</b-button>
</div>
${h.end_form()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
TailboneFormData.formSubmitting = false
TailboneFormData.formButtonText = "Yes, please DELETE this data forever!"
${form.vue_component}Data.formSubmitting = false
TailboneForm.methods.submitForm = function() {
${form.vue_component}.methods.submitForm = function() {
this.formSubmitting = true
this.formButtonText = "Working, please wait..."
}
</script>
</%def>
${parent.body()}

View file

@ -1,18 +1,18 @@
## -*- coding: utf-8; -*-
<%inherit file="/form.mako" />
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
## declare extra data needed by form
% if form is not Undefined:
% if form is not Undefined and getattr(form, 'json_data', None):
% for key, value in form.json_data.items():
${form.component_studly}Data.${key} = ${json.dumps(value)|n}
${form.vue_component}Data.${key} = ${json.dumps(value)|n}
% endfor
% endif
% if master.deletable and instance_deletable and master.has_perm('delete') and master.delete_confirm == 'simple':
% if master.deletable and instance_deletable and master.has_perm('delete') and getattr(master, 'delete_confirm', 'full') == 'simple':
ThisPage.methods.deleteObject = function() {
if (confirm("Are you sure you wish to delete this ${model_title}?")) {
@ -23,11 +23,8 @@
% endif
</script>
% if form is not Undefined:
% if form is not Undefined and hasattr(form, 'render_included_templates'):
${form.render_included_templates()}
% endif
</%def>
${parent.body()}

View file

@ -15,7 +15,7 @@
<%def name="grid_tools()">
## grid totals
% if master.supports_grid_totals:
% if getattr(master, 'supports_grid_totals', False):
<div style="display: flex; align-items: center;">
<b-button v-if="gridTotalsDisplay == null"
:disabled="gridTotalsFetching"
@ -30,7 +30,7 @@
% endif
## download search results
% if master.results_downloadable and master.has_perm('download_results'):
% if getattr(master, 'results_downloadable', False) and master.has_perm('download_results'):
<div>
<b-button type="is-primary"
icon-pack="fas"
@ -180,7 +180,7 @@
% endif
## download rows for search results
% if master.has_rows and master.results_rows_downloadable and master.has_perm('download_results_rows'):
% if getattr(master, 'has_rows', False) and master.results_rows_downloadable and master.has_perm('download_results_rows'):
<b-button type="is-primary"
icon-pack="fas"
icon-left="download"
@ -194,7 +194,7 @@
% endif
## merge 2 objects
% if master.mergeable and request.has_perm('{}.merge'.format(permission_prefix)):
% if getattr(master, 'mergeable', False) and request.has_perm('{}.merge'.format(permission_prefix)):
${h.form(url('{}.merge'.format(route_prefix)), class_='control', **{'@submit': 'submitMergeForm'})}
${h.csrf_token(request)}
@ -212,7 +212,7 @@
% endif
## enable / disable selected objects
% if master.supports_set_enabled_toggle and master.has_perm('enable_disable_set'):
% if getattr(master, 'supports_set_enabled_toggle', False) and master.has_perm('enable_disable_set'):
${h.form(url('{}.enable_set'.format(route_prefix)), class_='control', ref='enable_selected_form')}
${h.csrf_token(request)}
@ -234,7 +234,7 @@
% endif
## delete selected objects
% if master.set_deletable and master.has_perm('delete_set'):
% if getattr(master, 'set_deletable', False) and master.has_perm('delete_set'):
${h.form(url('{}.delete_set'.format(route_prefix)), ref='delete_selected_form', class_='control')}
${h.csrf_token(request)}
${h.hidden('uuids', v_model='selected_uuids')}
@ -249,7 +249,7 @@
% endif
## delete search results
% if master.bulk_deletable and request.has_perm('{}.bulk_delete'.format(permission_prefix)):
% if getattr(master, 'bulk_deletable', False) and request.has_perm('{}.bulk_delete'.format(permission_prefix)):
${h.form(url('{}.bulk_delete'.format(route_prefix)), ref='delete_results_form', class_='control')}
${h.csrf_token(request)}
<b-button type="is-danger"
@ -265,6 +265,11 @@
</%def>
## DEPRECATED; remains for back-compat
<%def name="render_this_page()">
${self.page_content()}
</%def>
<%def name="page_content()">
% if download_results_path:
@ -283,56 +288,42 @@
${self.render_grid_component()}
% if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple':
% if master.deletable and master.has_perm('delete') and getattr(master, 'delete_confirm', 'full') == 'simple':
${h.form('#', ref='deleteObjectForm')}
${h.csrf_token(request)}
${h.end_form()}
% endif
</%def>
<%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}
</%def>
<%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.component}>
${grid.render_vue_tag()}
</%def>
<%def name="make_this_page_component()">
##############################
## vue components
##############################
## define grid
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
## DEPRECATED; called for back-compat
${self.make_grid_component()}
${parent.make_this_page_component()}
## finalize grid
<script>
${grid.component_studly}.data = () => { return ${grid.component_studly}Data }
Vue.component('${grid.component}', ${grid.component_studly})
</script>
</%def>
<%def name="render_this_page()">
${self.page_content()}
## DEPRECATED; remains for back-compat
<%def name="make_grid_component()">
${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script type="text/javascript">
% if master.supports_grid_totals:
${grid.component_studly}Data.gridTotalsDisplay = null
${grid.component_studly}Data.gridTotalsFetching = false
% if getattr(master, 'supports_grid_totals', False):
${grid.vue_component}Data.gridTotalsDisplay = null
${grid.vue_component}Data.gridTotalsFetching = false
${grid.component_studly}.methods.gridTotalsFetch = function() {
${grid.vue_component}.methods.gridTotalsFetch = function() {
this.gridTotalsFetching = true
let url = '${url(f'{route_prefix}.fetch_grid_totals')}'
@ -344,7 +335,7 @@
})
}
${grid.component_studly}.methods.appliedFiltersHook = function() {
${grid.vue_component}.methods.appliedFiltersHook = function() {
this.gridTotalsDisplay = null
this.gridTotalsFetching = false
}
@ -388,7 +379,7 @@
% endif
## delete single object
% if master.deletable and master.has_perm('delete') and master.delete_confirm == 'simple':
% if master.deletable and master.has_perm('delete') and getattr(master, 'delete_confirm', 'full') == 'simple':
ThisPage.methods.deleteObject = function(url) {
if (confirm("Are you sure you wish to delete this ${model_title}?")) {
let form = this.$refs.deleteObjectForm
@ -399,19 +390,19 @@
% endif
## download results
% if master.results_downloadable and master.has_perm('download_results'):
% if getattr(master, 'results_downloadable', False) and master.has_perm('download_results'):
${grid.component_studly}Data.downloadResultsFormat = '${master.download_results_default_format()}'
${grid.component_studly}Data.showDownloadResultsDialog = false
${grid.component_studly}Data.downloadResultsFieldsMode = 'default'
${grid.component_studly}Data.downloadResultsFieldsAvailable = ${json.dumps(download_results_fields_available)|n}
${grid.component_studly}Data.downloadResultsFieldsDefault = ${json.dumps(download_results_fields_default)|n}
${grid.component_studly}Data.downloadResultsFieldsIncluded = ${json.dumps(download_results_fields_default)|n}
${grid.vue_component}Data.downloadResultsFormat = '${master.download_results_default_format()}'
${grid.vue_component}Data.showDownloadResultsDialog = false
${grid.vue_component}Data.downloadResultsFieldsMode = 'default'
${grid.vue_component}Data.downloadResultsFieldsAvailable = ${json.dumps(download_results_fields_available)|n}
${grid.vue_component}Data.downloadResultsFieldsDefault = ${json.dumps(download_results_fields_default)|n}
${grid.vue_component}Data.downloadResultsFieldsIncluded = ${json.dumps(download_results_fields_default)|n}
${grid.component_studly}Data.downloadResultsExcludedFieldsSelected = []
${grid.component_studly}Data.downloadResultsIncludedFieldsSelected = []
${grid.vue_component}Data.downloadResultsExcludedFieldsSelected = []
${grid.vue_component}Data.downloadResultsIncludedFieldsSelected = []
${grid.component_studly}.computed.downloadResultsFieldsExcluded = function() {
${grid.vue_component}.computed.downloadResultsFieldsExcluded = function() {
let excluded = []
this.downloadResultsFieldsAvailable.forEach(field => {
if (!this.downloadResultsFieldsIncluded.includes(field)) {
@ -421,7 +412,7 @@
return excluded
}
${grid.component_studly}.methods.downloadResultsExcludeFields = function() {
${grid.vue_component}.methods.downloadResultsExcludeFields = function() {
const selected = Array.from(this.downloadResultsIncludedFieldsSelected)
if (!selected) {
return
@ -445,7 +436,7 @@
})
}
${grid.component_studly}.methods.downloadResultsIncludeFields = function() {
${grid.vue_component}.methods.downloadResultsIncludeFields = function() {
const selected = Array.from(this.downloadResultsExcludedFieldsSelected)
if (!selected) {
return
@ -466,28 +457,28 @@
})
}
${grid.component_studly}.methods.downloadResultsUseDefaultFields = function() {
${grid.vue_component}.methods.downloadResultsUseDefaultFields = function() {
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsDefault)
this.downloadResultsFieldsMode = 'default'
}
${grid.component_studly}.methods.downloadResultsUseAllFields = function() {
${grid.vue_component}.methods.downloadResultsUseAllFields = function() {
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsAvailable)
this.downloadResultsFieldsMode = 'all'
}
${grid.component_studly}.methods.downloadResultsSubmit = function() {
${grid.vue_component}.methods.downloadResultsSubmit = function() {
this.$refs.download_results_form.submit()
}
% endif
## download rows for results
% if master.has_rows and master.results_rows_downloadable and master.has_perm('download_results_rows'):
% if getattr(master, 'has_rows', False) and master.results_rows_downloadable and master.has_perm('download_results_rows'):
${grid.component_studly}Data.downloadResultsRowsButtonDisabled = false
${grid.component_studly}Data.downloadResultsRowsButtonText = "Download Rows for Results"
${grid.vue_component}Data.downloadResultsRowsButtonDisabled = false
${grid.vue_component}Data.downloadResultsRowsButtonText = "Download Rows for Results"
${grid.component_studly}.methods.downloadResultsRows = function() {
${grid.vue_component}.methods.downloadResultsRows = function() {
if (confirm("This will generate an Excel file which contains "
+ "not the results themselves, but the *rows* for "
+ "each.\n\nAre you sure you want this?")) {
@ -499,12 +490,12 @@
% endif
## enable / disable selected objects
% if master.supports_set_enabled_toggle and master.has_perm('enable_disable_set'):
% if getattr(master, 'supports_set_enabled_toggle', False) and master.has_perm('enable_disable_set'):
${grid.component_studly}Data.enableSelectedSubmitting = false
${grid.component_studly}Data.enableSelectedText = "Enable Selected"
${grid.vue_component}Data.enableSelectedSubmitting = false
${grid.vue_component}Data.enableSelectedText = "Enable Selected"
${grid.component_studly}.computed.enableSelectedDisabled = function() {
${grid.vue_component}.computed.enableSelectedDisabled = function() {
if (this.enableSelectedSubmitting) {
return true
}
@ -514,7 +505,7 @@
return false
}
${grid.component_studly}.methods.enableSelectedSubmit = function() {
${grid.vue_component}.methods.enableSelectedSubmit = function() {
let uuids = this.checkedRowUUIDs()
if (!uuids.length) {
alert("You must first select one or more objects to disable.")
@ -529,10 +520,10 @@
this.$refs.enable_selected_form.submit()
}
${grid.component_studly}Data.disableSelectedSubmitting = false
${grid.component_studly}Data.disableSelectedText = "Disable Selected"
${grid.vue_component}Data.disableSelectedSubmitting = false
${grid.vue_component}Data.disableSelectedText = "Disable Selected"
${grid.component_studly}.computed.disableSelectedDisabled = function() {
${grid.vue_component}.computed.disableSelectedDisabled = function() {
if (this.disableSelectedSubmitting) {
return true
}
@ -542,7 +533,7 @@
return false
}
${grid.component_studly}.methods.disableSelectedSubmit = function() {
${grid.vue_component}.methods.disableSelectedSubmit = function() {
let uuids = this.checkedRowUUIDs()
if (!uuids.length) {
alert("You must first select one or more objects to disable.")
@ -560,12 +551,12 @@
% endif
## delete selected objects
% if master.set_deletable and master.has_perm('delete_set'):
% if getattr(master, 'set_deletable', False) and master.has_perm('delete_set'):
${grid.component_studly}Data.deleteSelectedSubmitting = false
${grid.component_studly}Data.deleteSelectedText = "Delete Selected"
${grid.vue_component}Data.deleteSelectedSubmitting = false
${grid.vue_component}Data.deleteSelectedText = "Delete Selected"
${grid.component_studly}.computed.deleteSelectedDisabled = function() {
${grid.vue_component}.computed.deleteSelectedDisabled = function() {
if (this.deleteSelectedSubmitting) {
return true
}
@ -575,7 +566,7 @@
return false
}
${grid.component_studly}.methods.deleteSelectedSubmit = function() {
${grid.vue_component}.methods.deleteSelectedSubmit = function() {
let uuids = this.checkedRowUUIDs()
if (!uuids.length) {
alert("You must first select one or more objects to disable.")
@ -591,12 +582,12 @@
}
% endif
% if master.bulk_deletable and master.has_perm('bulk_delete'):
% if getattr(master, 'bulk_deletable', False) and master.has_perm('bulk_delete'):
${grid.component_studly}Data.deleteResultsSubmitting = false
${grid.component_studly}Data.deleteResultsText = "Delete Results"
${grid.vue_component}Data.deleteResultsSubmitting = false
${grid.vue_component}Data.deleteResultsText = "Delete Results"
${grid.component_studly}.computed.deleteResultsDisabled = function() {
${grid.vue_component}.computed.deleteResultsDisabled = function() {
if (this.deleteResultsSubmitting) {
return true
}
@ -606,7 +597,7 @@
return false
}
${grid.component_studly}.methods.deleteResultsSubmit = function() {
${grid.vue_component}.methods.deleteResultsSubmit = function() {
// TODO: show "plural model title" here?
if (!confirm("You are about to delete " + this.total.toLocaleString('en') + " objects.\n\nAre you sure?")) {
return
@ -619,12 +610,12 @@
% endif
% if master.mergeable and master.has_perm('merge'):
% if getattr(master, 'mergeable', False) and master.has_perm('merge'):
${grid.component_studly}Data.mergeFormButtonText = "Merge 2 ${model_title_plural}"
${grid.component_studly}Data.mergeFormSubmitting = false
${grid.vue_component}Data.mergeFormButtonText = "Merge 2 ${model_title_plural}"
${grid.vue_component}Data.mergeFormSubmitting = false
${grid.component_studly}.methods.submitMergeForm = function() {
${grid.vue_component}.methods.submitMergeForm = function() {
this.mergeFormSubmitting = true
this.mergeFormButtonText = "Working, please wait..."
}
@ -632,5 +623,10 @@
</script>
</%def>
${parent.body()}
<%def name="make_vue_components()">
${parent.make_vue_components()}
<script>
${grid.vue_component}.data = function() { return ${grid.vue_component}Data }
Vue.component('${grid.vue_tagname}', ${grid.vue_component})
</script>
</%def>

View file

@ -109,8 +109,8 @@
<merge-buttons></merge-buttons>
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
<script type="text/x-template" id="merge-buttons-template">
<div class="level" style="margin-top: 2em;">
@ -147,11 +147,7 @@
</div>
</div>
</script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript">
<script>
const MergeButtons = {
template: '#merge-buttons-template',
@ -175,12 +171,13 @@
}
}
Vue.component('merge-buttons', MergeButtons)
<% request.register_component('merge-buttons', 'MergeButtons') %>
</script>
</%def>
${parent.body()}
<%def name="make_vue_components()">
${parent.make_vue_components()}
<script>
Vue.component('merge-buttons', MergeButtons)
<% request.register_component('merge-buttons', 'MergeButtons') %>
</script>
</%def>

View file

@ -16,27 +16,16 @@
${self.page_content()}
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript">
TailboneGrid.data = function() { return TailboneGridData }
Vue.component('tailbone-grid', TailboneGrid)
</script>
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
## TODO: stop using |n filter
${grid.render_complete()|n}
</%def>
<%def name="page_content()">
<tailbone-grid :csrftoken="csrftoken">
</tailbone-grid>
${grid.render_vue_tag(**{':csrftoken': 'csrftoken'})}
</%def>
${parent.body()}
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${grid.render_vue_template()}
</%def>
<%def name="make_vue_components()">
${parent.make_vue_components()}
${grid.render_vue_finalize()}
</%def>

View file

@ -8,7 +8,7 @@
</%def>
<%def name="render_instance_header_title_extras()">
% if master.touchable and master.has_perm('touch'):
% if getattr(master, 'touchable', False) and master.has_perm('touch'):
<b-button title="&quot;Touch&quot; this record to trigger sync"
@click="touchRecord()"
:disabled="touchSubmitting">
@ -93,7 +93,7 @@
${parent.render_this_page()}
## render row grid
% if master.has_rows:
% if getattr(master, 'has_rows', False):
<br />
% if rows_title:
<h4 class="block is-size-4">${rows_title}</h4>
@ -120,9 +120,7 @@
</p>
</div>
<versions-grid ref="versionsGrid"
@view-revision="viewRevision">
</versions-grid>
${versions_grid.render_vue_tag(ref='versionsGrid', **{'@view-revision': 'viewRevision'})}
<${b}-modal :width="1200"
% if request.use_oruga:
@ -198,6 +196,7 @@
<p class="block has-text-weight-bold">
{{ version.model_title }}
({{ version.operation }})
</p>
<table class="diff monospace is-size-7"
@ -237,25 +236,37 @@
</%def>
<%def name="render_row_grid_component()">
<tailbone-grid ref="rowGrid" id="rowGrid"></tailbone-grid>
${rows_grid.render_vue_tag(id='rowGrid', ref='rowGrid')}
</%def>
<%def name="render_this_page_template()">
% if master.has_rows:
## TODO: stop using |n filter
${rows_grid.render_complete(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))|n}
<%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_complete()|n}
${versions_grid.render_vue_template()}
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
% if expose_versions:
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if getattr(master, 'touchable', False) and master.has_perm('touch'):
WholePageData.touchSubmitting = false
WholePage.methods.touchRecord = function() {
this.touchSubmitting = true
location.href = '${master.get_action_url('touch', instance)}'
}
% endif
% if expose_versions:
WholePageData.viewingHistory = false
ThisPage.props.viewingHistory = Boolean
ThisPageData.gettingRevisions = false
@ -310,48 +321,16 @@
this.viewVersionShowAllFields = !this.viewVersionShowAllFields
}
</script>
% endif
</script>
</%def>
<%def name="make_vue_components()">
${parent.make_vue_components()}
% if getattr(master, 'has_rows', False):
${rows_grid.render_vue_finalize()}
% endif
% if expose_versions:
${versions_grid.render_vue_finalize()}
% endif
</%def>
<%def name="modify_whole_page_vars()">
${parent.modify_whole_page_vars()}
<script type="text/javascript">
% if master.touchable and master.has_perm('touch'):
WholePageData.touchSubmitting = false
WholePage.methods.touchRecord = function() {
this.touchSubmitting = true
location.href = '${master.get_action_url('touch', instance)}'
}
% endif
% if expose_versions:
WholePageData.viewingHistory = false
% endif
</script>
</%def>
<%def name="finalize_this_page_vars()">
${parent.finalize_this_page_vars()}
<script type="text/javascript">
% if master.has_rows:
TailboneGrid.data = function() { return TailboneGridData }
Vue.component('tailbone-grid', TailboneGrid)
% endif
% if expose_versions:
VersionsGrid.data = function() { return VersionsGridData }
Vue.component('versions-grid', VersionsGrid)
% endif
</script>
</%def>
${parent.body()}

View file

@ -52,9 +52,9 @@
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPage.methods.getLabelForKey = function(key) {
switch (key) {
@ -75,6 +75,3 @@
</script>
</%def>
${parent.body()}

View file

@ -32,14 +32,14 @@
% endif
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${message_recipients_template()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
TailboneFormData.possibleRecipients = new Map(${json.dumps(available_recipients)|n})
TailboneFormData.recipientDisplayMap = ${json.dumps(recipient_display_map)|n}
@ -59,6 +59,3 @@
</script>
</%def>
${parent.body()}

View file

@ -22,15 +22,15 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
% if request.matched_route.name in ('messages.inbox', 'messages.archive'):
<script type="text/javascript">
<script>
TailboneGridData.moveMessagesSubmitting = false
TailboneGridData.moveMessagesText = null
${grid.vue_component}Data.moveMessagesSubmitting = false
${grid.vue_component}Data.moveMessagesText = null
TailboneGrid.computed.moveMessagesTextCurrent = function() {
${grid.vue_component}.computed.moveMessagesTextCurrent = function() {
if (this.moveMessagesText) {
return this.moveMessagesText
}
@ -38,7 +38,7 @@
return "Move " + count.toString() + " selected to ${'Archive' if request.matched_route.name == 'messages.inbox' else 'Inbox'}"
}
TailboneGrid.methods.moveMessagesSubmit = function() {
${grid.vue_component}.methods.moveMessagesSubmit = function() {
this.moveMessagesSubmitting = true
this.moveMessagesText = "Working, please wait..."
}
@ -46,6 +46,3 @@
</script>
% endif
</%def>
${parent.body()}

View file

@ -82,22 +82,19 @@
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
TailboneFormData.showingAllRecipients = false
${form.vue_component}Data.showingAllRecipients = false
TailboneForm.methods.showMoreRecipients = function() {
${form.vue_component}.methods.showMoreRecipients = function() {
this.showingAllRecipients = true
}
TailboneForm.methods.hideMoreRecipients = function() {
${form.vue_component}.methods.hideMoreRecipients = function() {
this.showingAllRecipients = false
}
</script>
</%def>
${parent.body()}

View file

@ -0,0 +1,74 @@
## -*- coding: utf-8; -*-
<%inherit file="/configure.mako" />
<%def name="form_content()">
<h3 class="block is-size-3">Workflows</h3>
<div class="block" style="padding-left: 2rem;">
<p class="block">
Users can only choose from the workflows enabled below.
</p>
<b-field>
<b-checkbox name="rattail.batch.purchase.allow_ordering_from_scratch"
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_from_scratch']"
native-value="true"
@input="settingsNeedSaved = true">
From Scratch
</b-checkbox>
</b-field>
<b-field>
<b-checkbox name="rattail.batch.purchase.allow_ordering_from_file"
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_from_file']"
native-value="true"
@input="settingsNeedSaved = true">
From Order File
</b-checkbox>
</b-field>
</div>
<h3 class="block is-size-3">Vendors</h3>
<div class="block" style="padding-left: 2rem;">
<b-field message="If not set, user must choose a &quot;supported&quot; vendor.">
<b-checkbox name="rattail.batch.purchase.allow_ordering_any_vendor"
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_any_vendor']"
native-value="true"
@input="settingsNeedSaved = true">
Allow ordering for <span class="has-text-weight-bold">any</span> vendor
</b-checkbox>
</b-field>
</div>
<h3 class="block is-size-3">Order Parsers</h3>
<div class="block" style="padding-left: 2rem;">
<p class="block">
Only the selected file parsers will be exposed to users.
</p>
% for Parser in order_parsers:
<b-field message="${Parser.key}">
<b-checkbox name="order_parser_${Parser.key}"
v-model="orderParsers['${Parser.key}']"
native-value="true"
@input="settingsNeedSaved = true">
${Parser.title}
</b-checkbox>
</b-field>
% endfor
</div>
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.orderParsers = ${json.dumps(order_parsers_data)|n}
</script>
</%def>

View file

@ -21,8 +21,8 @@
% endif
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
% if not batch.executed and not batch.complete and master.has_perm('edit_row'):
<script type="text/x-template" id="ordering-scanner-template">
<div>
@ -185,10 +185,10 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
% if not batch.executed and not batch.complete and master.has_perm('edit_row'):
<script type="text/javascript">
<script>
let OrderingScanner = {
template: '#ordering-scanner-template',
@ -204,7 +204,7 @@
saving: false,
## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
csrftoken: ${json.dumps(h.get_csrf_token(request))|n},
}
},
computed: {
@ -408,16 +408,11 @@
% endif
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<%def name="make_vue_components()">
${parent.make_vue_components()}
% if not batch.executed and not batch.complete and master.has_perm('edit_row'):
<script type="text/javascript">
<script>
Vue.component('ordering-scanner', OrderingScanner)
</script>
% endif
</%def>
${parent.body()}

View file

@ -199,9 +199,8 @@
<ordering-worksheet></ordering-worksheet>
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
<script type="text/x-template" id="ordering-worksheet-template">
<div>
<div class="form-wrapper">
@ -239,11 +238,7 @@
${self.order_form_grid()}
</div>
</script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript">
<script>
const OrderingWorksheet = {
template: '#ordering-worksheet-template',
@ -255,7 +250,7 @@
submitting: false,
## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
csrftoken: ${json.dumps(h.get_csrf_token(request))|n},
}
},
methods: {
@ -298,14 +293,12 @@
},
}
Vue.component('ordering-worksheet', OrderingWorksheet)
</script>
</%def>
##############################
## page body
##############################
${parent.body()}
<%def name="make_vue_components()">
${parent.make_vue_components()}
<script>
Vue.component('ordering-worksheet', OrderingWorksheet)
</script>
</%def>

View file

@ -1,42 +1,26 @@
## -*- coding: utf-8; -*-
<%inherit file="/base.mako" />
<%def name="context_menu_items()">
% if context_menu_list_items is not Undefined:
% for item in context_menu_list_items:
<li>${item}</li>
% endfor
% endif
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${self.render_vue_template_this_page()}
</%def>
<%def name="page_content()"></%def>
<%def name="render_this_page()">
<div style="display: flex;">
<div class="this-page-content" style="flex-grow: 1;">
${self.page_content()}
</div>
<ul id="context-menu">
${self.context_menu_items()}
</ul>
</div>
<%def name="render_vue_template_this_page()">
## DEPRECATED; called for back-compat
${self.render_this_page_template()}
</%def>
<%def name="render_this_page_template()">
<script type="text/x-template" id="this-page-template">
<div>
## DEPRECATED; called for back-compat
${self.render_this_page()}
</div>
</script>
</%def>
<script>
<%def name="declare_this_page_vars()">
<script type="text/javascript">
let ThisPage = {
const ThisPage = {
template: '#this-page-template',
mixins: [SimpleRequestMixin],
props: {
@ -52,37 +36,71 @@
},
}
let ThisPageData = {
const ThisPageData = {
## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
csrftoken: ${json.dumps(h.get_csrf_token(request))|n},
}
</script>
</%def>
<%def name="modify_this_page_vars()">
## NOTE: if you override this, must use <script> tags
## DEPRECATED; remains for back-compat
<%def name="render_this_page()">
<div style="display: flex;">
<div class="this-page-content" style="flex-grow: 1;">
${self.page_content()}
</div>
## DEPRECATED; remains for back-compat
<ul id="context-menu">
${self.context_menu_items()}
</ul>
</div>
</%def>
<%def name="finalize_this_page_vars()">
## NOTE: if you override this, must use <script> tags
## nb. this is the canonical block for page content!
<%def name="page_content()"></%def>
## DEPRECATED; remains for back-compat
<%def name="context_menu_items()">
% if context_menu_list_items is not Undefined:
% for item in context_menu_list_items:
<li>${item}</li>
% endfor
% endif
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
## DEPRECATED; called for back-compat
${self.declare_this_page_vars()}
${self.modify_this_page_vars()}
</%def>
<%def name="make_vue_components()">
${parent.make_vue_components()}
## DEPRECATED; called for back-compat
${self.make_this_page_component()}
</%def>
<%def name="make_this_page_component()">
${self.declare_this_page_vars()}
${self.modify_this_page_vars()}
${self.finalize_this_page_vars()}
<script type="text/javascript">
<script>
ThisPage.data = function() { return ThisPageData }
Vue.component('this-page', ThisPage)
<% request.register_component('this-page', 'ThisPage') %>
</script>
</%def>
##############################
## DEPRECATED
##############################
${self.render_this_page_template()}
${self.make_this_page_component()}
<%def name="declare_this_page_vars()"></%def>
<%def name="modify_this_page_vars()"></%def>
<%def name="finalize_this_page_vars()"></%def>

View file

@ -3,7 +3,7 @@
<%def name="grid_tools()">
% if master.mergeable and master.has_perm('request_merge'):
% if getattr(master, 'mergeable', False) and master.has_perm('request_merge'):
<b-button @click="showMergeRequest()"
icon-pack="fas"
icon-left="object-ungroup"
@ -61,37 +61,37 @@
${parent.grid_tools()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if master.mergeable and master.has_perm('request_merge'):
% if getattr(master, 'mergeable', False) and master.has_perm('request_merge'):
${grid.component_studly}Data.mergeRequestShowDialog = false
${grid.component_studly}Data.mergeRequestRows = []
${grid.component_studly}Data.mergeRequestSubmitText = "Submit Merge Request"
${grid.component_studly}Data.mergeRequestSubmitting = false
${grid.vue_component}Data.mergeRequestShowDialog = false
${grid.vue_component}Data.mergeRequestRows = []
${grid.vue_component}Data.mergeRequestSubmitText = "Submit Merge Request"
${grid.vue_component}Data.mergeRequestSubmitting = false
${grid.component_studly}.computed.mergeRequestRemovingUUID = function() {
${grid.vue_component}.computed.mergeRequestRemovingUUID = function() {
if (this.mergeRequestRows.length) {
return this.mergeRequestRows[0].uuid
}
return null
}
${grid.component_studly}.computed.mergeRequestKeepingUUID = function() {
${grid.vue_component}.computed.mergeRequestKeepingUUID = function() {
if (this.mergeRequestRows.length) {
return this.mergeRequestRows[1].uuid
}
return null
}
${grid.component_studly}.methods.showMergeRequest = function() {
${grid.vue_component}.methods.showMergeRequest = function() {
this.mergeRequestRows = this.checkedRows
this.mergeRequestShowDialog = true
}
${grid.component_studly}.methods.submitMergeRequest = function() {
${grid.vue_component}.methods.submitMergeRequest = function() {
this.mergeRequestSubmitting = true
this.mergeRequestSubmitText = "Working, please wait..."
}
@ -100,5 +100,3 @@
</script>
</%def>
${parent.body()}

View file

@ -18,10 +18,10 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
% if not instance.merged and request.has_perm('people.merge'):
<script type="text/javascript">
<script>
ThisPageData.mergeFormButtonText = "Perform Merge"
ThisPageData.mergeFormSubmitting = false
@ -34,5 +34,3 @@
</script>
% endif
</%def>
${parent.body()}

View file

@ -2,34 +2,6 @@
<%inherit file="/master/view.mako" />
<%namespace file="/util.mako" import="view_profiles_helper" />
<%def name="object_helpers()">
${parent.object_helpers()}
${view_profiles_helper([instance])}
</%def>
<%def name="render_form()">
<div class="form">
<tailbone-form v-on:make-user="makeUser"></tailbone-form>
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
TailboneForm.methods.clickMakeUser = function(event) {
this.$emit('make-user')
}
ThisPage.methods.makeUser = function(event) {
if (confirm("Really make a user account for this person?")) {
this.$refs.makeUserForm.submit()
}
}
</script>
</%def>
<%def name="page_content()">
${parent.page_content()}
% if not instance.users and request.has_perm('users.create'):
@ -40,6 +12,30 @@
% endif
</%def>
<%def name="object_helpers()">
${parent.object_helpers()}
${view_profiles_helper([instance])}
</%def>
${parent.body()}
<%def name="render_form()">
<div class="form">
<${form.vue_tagname} v-on:make-user="makeUser"></${form.vue_tagname}>
</div>
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
${form.vue_component}.methods.clickMakeUser = function(event) {
this.$emit('make-user')
}
ThisPage.methods.makeUser = function(event) {
if (confirm("Really make a user account for this person?")) {
this.$refs.makeUserForm.submit()
}
}
</script>
</%def>

View file

@ -15,7 +15,7 @@
</%def>
<%def name="content_title()">
${dynamic_content_title}
${dynamic_content_title or str(instance)}
</%def>
<%def name="render_instance_header_title_extras()">
@ -1008,7 +1008,7 @@
<div style="display: flex; justify-content: space-between; width: 100%;">
<div style="flex-grow: 1;">
<b-field horizontal label="${customer_key_label}">
<b-field horizontal label="${customer_key_label or 'TODO: Customer Key'}">
{{ customer._key }}
</b-field>
@ -1966,37 +1966,106 @@
</div>
</script>
</%def>
<script>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
${self.render_personal_tab_template()}
let ProfileInfoData = {
activeTab: location.hash ? location.hash.substring(1) : 'personal',
tabchecks: ${json.dumps(tabchecks or {})|n},
today: '${rattail_app.today()}',
profileLastChanged: Date.now(),
person: ${json.dumps(person_data or {})|n},
phoneTypeOptions: ${json.dumps(phone_type_options or [])|n},
emailTypeOptions: ${json.dumps(email_type_options or [])|n},
maxLengths: ${json.dumps(max_lengths or {})|n},
% if expose_members:
${self.render_member_tab_template()}
% endif
% if request.has_perm('people_profile.view_versions'):
loadingRevisions: false,
showingRevisionDialog: false,
revision: {},
revisionShowAllFields: false,
% endif
}
${self.render_customer_tab_template()}
% if expose_customer_shoppers:
${self.render_shopper_tab_template()}
% endif
${self.render_employee_tab_template()}
${self.render_notes_tab_template()}
let ProfileInfo = {
template: '#profile-info-template',
props: {
% if request.has_perm('people_profile.view_versions'):
viewingHistory: Boolean,
gettingRevisions: Boolean,
revisions: Array,
revisionVersionMap: null,
% endif
},
computed: {},
mounted() {
% if expose_transactions:
${transactions_grid.render_complete(allow_save_defaults=False)|n}
${self.render_transactions_tab_template()}
% endif
// auto-refresh whichever tab is shown first
## TODO: how to not assume 'personal' is the default tab?
let tab = this.$refs['tab_' + (this.activeTab || 'personal')]
if (tab && tab.refreshTab) {
tab.refreshTab()
}
},
methods: {
${self.render_user_tab_template()}
${self.render_profile_info_template()}
profileChanged(data) {
this.$emit('change-content-title', data.person.dynamic_content_title)
this.person = data.person
this.tabchecks = data.tabchecks
this.profileLastChanged = Date.now()
},
activeTabChanged(value) {
location.hash = value
this.refreshTabIfNeeded(value)
this.activeTabChangedExtra(value)
},
refreshTabIfNeeded(key) {
// TODO: this is *always* refreshing, should be more selective (?)
let tab = this.$refs['tab_' + key]
if (tab && tab.refreshIfNeeded) {
tab.refreshIfNeeded(this.profileLastChanged)
}
},
activeTabChangedExtra(value) {},
% if request.has_perm('people_profile.view_versions'):
viewRevision(row) {
this.revision = this.revisionVersionMap[row.txnid]
this.showingRevisionDialog = true
},
viewPrevRevision() {
let txnid = this.revision.prev_txnid
this.revision = this.revisionVersionMap[txnid]
},
viewNextRevision() {
let txnid = this.revision.next_txnid
this.revision = this.revisionVersionMap[txnid]
},
toggleVersionFields() {
this.revisionShowAllFields = !this.revisionShowAllFields
},
% endif
},
}
</script>
</%def>
<%def name="declare_personal_tab_vars()">
<script type="text/javascript">
let PersonalTabData = {
% if hasattr(master, 'profile_tab_personal'):
refreshTabURL: '${url('people.profile_tab_personal', uuid=person.uuid)}',
% endif
// nb. hack to force refresh for vue3
refreshPersonalCard: 1,
@ -2447,7 +2516,9 @@
<script type="text/javascript">
let CustomerTabData = {
% if hasattr(master, 'profile_tab_customer'):
refreshTabURL: '${url('people.profile_tab_customer', uuid=person.uuid)}',
% endif
customers: [],
}
@ -2521,7 +2592,9 @@
<script type="text/javascript">
let EmployeeTabData = {
% if hasattr(master, 'profile_tab_employee'):
refreshTabURL: '${url('people.profile_tab_employee', uuid=person.uuid)}',
% endif
employee: {},
employeeHistory: [],
@ -2756,7 +2829,9 @@
<script type="text/javascript">
let NotesTabData = {
% if hasattr(master, 'profile_tab_notes'):
refreshTabURL: '${url('people.profile_tab_notes', uuid=person.uuid)}',
% endif
notes: [],
noteTypeOptions: [],
@ -2920,7 +2995,9 @@
<script type="text/javascript">
let UserTabData = {
% if hasattr(master, 'profile_tab_user'):
refreshTabURL: '${url('people.profile_tab_user', uuid=person.uuid)}',
% endif
users: [],
% if request.has_perm('users.create'):
@ -2976,7 +3053,9 @@
createUserSave() {
this.createUserSaving = true
% if hasattr(master, 'profile_make_user'):
let url = '${master.get_action_url('profile_make_user', instance)}'
% endif
let params = {
username: this.createUserUsername,
active: this.createUserActive,
@ -3010,114 +3089,46 @@
</script>
</%def>
<%def name="declare_profile_info_vars()">
<script type="text/javascript">
let ProfileInfoData = {
activeTab: location.hash ? location.hash.substring(1) : 'personal',
tabchecks: ${json.dumps(tabchecks)|n},
today: '${rattail_app.today()}',
profileLastChanged: Date.now(),
person: ${json.dumps(person_data)|n},
phoneTypeOptions: ${json.dumps(phone_type_options)|n},
emailTypeOptions: ${json.dumps(email_type_options)|n},
maxLengths: ${json.dumps(max_lengths)|n},
% if request.has_perm('people_profile.view_versions'):
loadingRevisions: false,
showingRevisionDialog: false,
revision: {},
revisionShowAllFields: false,
% endif
}
let ProfileInfo = {
template: '#profile-info-template',
props: {
% if request.has_perm('people_profile.view_versions'):
viewingHistory: Boolean,
gettingRevisions: Boolean,
revisions: Array,
revisionVersionMap: null,
% endif
},
computed: {},
mounted() {
// auto-refresh whichever tab is shown first
## TODO: how to not assume 'personal' is the default tab?
let tab = this.$refs['tab_' + (this.activeTab || 'personal')]
if (tab && tab.refreshTab) {
tab.refreshTab()
}
},
methods: {
profileChanged(data) {
this.$emit('change-content-title', data.person.dynamic_content_title)
this.person = data.person
this.tabchecks = data.tabchecks
this.profileLastChanged = Date.now()
},
activeTabChanged(value) {
location.hash = value
this.refreshTabIfNeeded(value)
this.activeTabChangedExtra(value)
},
refreshTabIfNeeded(key) {
// TODO: this is *always* refreshing, should be more selective (?)
let tab = this.$refs['tab_' + key]
if (tab && tab.refreshIfNeeded) {
tab.refreshIfNeeded(this.profileLastChanged)
}
},
activeTabChangedExtra(value) {},
% if request.has_perm('people_profile.view_versions'):
viewRevision(row) {
this.revision = this.revisionVersionMap[row.txnid]
this.showingRevisionDialog = true
},
viewPrevRevision() {
let txnid = this.revision.prev_txnid
this.revision = this.revisionVersionMap[txnid]
},
viewNextRevision() {
let txnid = this.revision.next_txnid
this.revision = this.revisionVersionMap[txnid]
},
toggleVersionFields() {
this.revisionShowAllFields = !this.revisionShowAllFields
},
% endif
},
}
</script>
</%def>
<%def name="make_profile_info_component()">
${self.declare_profile_info_vars()}
<script type="text/javascript">
## DEPRECATED; called for back-compat
${self.declare_profile_info_vars()}
<script>
ProfileInfo.data = function() { return ProfileInfoData }
Vue.component('profile-info', ProfileInfo)
<% request.register_component('profile-info', 'ProfileInfo') %>
</script>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${self.render_personal_tab_template()}
% if expose_members:
${self.render_member_tab_template()}
% endif
${self.render_customer_tab_template()}
% if expose_customer_shoppers:
${self.render_shopper_tab_template()}
% endif
${self.render_employee_tab_template()}
${self.render_notes_tab_template()}
% if expose_transactions:
${transactions_grid.render_complete(allow_save_defaults=False)|n}
${self.render_transactions_tab_template()}
% endif
${self.render_user_tab_template()}
${self.render_profile_info_template()}
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if request.has_perm('people_profile.view_versions'):
ThisPage.props.viewingHistory = Boolean
@ -3165,45 +3176,8 @@
},
}
</script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
${self.make_personal_tab_component()}
% if expose_members:
${self.make_member_tab_component()}
% endif
${self.make_customer_tab_component()}
% if expose_customer_shoppers:
${self.make_shopper_tab_component()}
% endif
${self.make_employee_tab_component()}
${self.make_notes_tab_component()}
% if expose_transactions:
<script type="text/javascript">
TransactionsGrid.data = function() { return TransactionsGridData }
Vue.component('transactions-grid', TransactionsGrid)
## TODO: why is this line not needed?
## <% request.register_component('transactions-grid', 'TransactionsGrid') %>
</script>
${self.make_transactions_tab_component()}
% endif
${self.make_user_tab_component()}
${self.make_profile_info_component()}
</%def>
<%def name="modify_whole_page_vars()">
${parent.modify_whole_page_vars()}
% if request.has_perm('people_profile.view_versions'):
<script type="text/javascript">
% if request.has_perm('people_profile.view_versions'):
WholePageData.viewingHistory = false
WholePageData.gettingRevisions = false
@ -3239,9 +3213,44 @@
})
}
</script>
% endif
% endif
</script>
</%def>
<%def name="make_vue_components()">
${parent.make_vue_components()}
${parent.body()}
${self.make_personal_tab_component()}
% if expose_members:
${self.make_member_tab_component()}
% endif
${self.make_customer_tab_component()}
% if expose_customer_shoppers:
${self.make_shopper_tab_component()}
% endif
${self.make_employee_tab_component()}
${self.make_notes_tab_component()}
% if expose_transactions:
<script type="text/javascript">
TransactionsGrid.data = function() { return TransactionsGridData }
Vue.component('transactions-grid', TransactionsGrid)
## TODO: why is this line not needed?
## <% request.register_component('transactions-grid', 'TransactionsGrid') %>
</script>
${self.make_transactions_tab_component()}
% endif
${self.make_user_tab_component()}
${self.make_profile_info_component()}
</%def>
##############################
## DEPRECATED
##############################
<%def name="declare_profile_info_vars()"></%def>

View file

@ -62,19 +62,13 @@
<br />
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
% if master.has_perm('replace'):
<script type="text/javascript">
${form.component_studly}Data.showUploadForm = false
${form.component_studly}Data.uploadFile = null
${form.component_studly}Data.uploadSubmitting = false
</script>
<script>
${form.vue_component}Data.showUploadForm = false
${form.vue_component}Data.uploadFile = null
${form.vue_component}Data.uploadSubmitting = false
</script>
% endif
</%def>
${parent.body()}

View file

@ -118,14 +118,9 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.setupSubmitting = false
</script>
</%def>
${parent.body()}

View file

@ -10,12 +10,20 @@
</find-principals>
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
<%def name="principal_table()">
<div
style="width: 50%;"
>
${grid.render_table_element(data_prop='principalsData')|n}
</div>
</%def>
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
<script type="text/x-template" id="find-principals-template">
<div>
${h.form(request.current_route_url(), method='GET', **{'@submit': 'formSubmitting = true'})}
${h.form(request.url, method='GET', **{'@submit': 'formSubmitting = true'})}
<div style="margin-left: 10rem; max-width: 50%;">
${h.hidden('permission_group', **{':value': 'selectedGroup'})}
@ -63,7 +71,7 @@
<b-field horizontal>
<div class="buttons" style="margin-top: 1rem;">
<once-button tag="a"
href="${request.current_route_url(_query=None)}"
href="${request.path_url}"
text="Reset Form">
</once-button>
<b-button type="is-primary"
@ -90,28 +98,6 @@
</div>
</script>
</%def>
<%def name="principal_table()">
<div
style="width: 50%;"
>
${grid.render_table_element(data_prop='principalsData')|n}
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
ThisPageData.permissionGroups = ${json.dumps(perms_data)|n}
ThisPageData.sortedGroups = ${json.dumps(sorted_groups_data)|n}
</script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript">
const FindPrincipals = {
@ -240,12 +226,21 @@
}
}
Vue.component('find-principals', FindPrincipals)
<% request.register_component('find-principals', 'FindPrincipals') %>
</script>
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.permissionGroups = ${json.dumps(perms_data)|n}
ThisPageData.sortedGroups = ${json.dumps(sorted_groups_data)|n}
</script>
</%def>
${parent.body()}
<%def name="make_vue_components()">
${parent.make_vue_components()}
<script>
Vue.component('find-principals', FindPrincipals)
<% request.register_component('find-principals', 'FindPrincipals') %>
</script>
</%def>

View file

@ -22,7 +22,7 @@
</%def>
<%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)}
<section>
@ -43,8 +43,8 @@
<div class="buttons">
<b-button type="is-primary"
native-type="submit"
:disabled="${form.component_studly}Submitting">
{{ ${form.component_studly}ButtonText }}
:disabled="${form.vue_component}Submitting">
{{ ${form.vue_component}ButtonText }}
</b-button>
<b-button tag="a" href="${url('products')}">
Cancel
@ -55,32 +55,33 @@
</%def>
<%def name="render_form_template()">
<script type="text/x-template" id="${form.component}-template">
<script type="text/x-template" id="${form.vue_tagname}-template">
${self.render_form_innards()}
</script>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<% request.register_component(form.vue_tagname, form.vue_component) %>
<script>
## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako)
let ${form.component_studly} = {
template: '#${form.component}-template',
let ${form.vue_component} = {
template: '#${form.vue_tagname}-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 +96,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
@ -114,6 +115,3 @@
</script>
</%def>
${parent.body()}

View file

@ -95,9 +95,9 @@
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPage.methods.getTitleForKey = function(key) {
switch (key) {
@ -118,6 +118,3 @@
</script>
</%def>
${parent.body()}

View file

@ -36,16 +36,16 @@
</${grid.component}>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
% if label_profiles and master.has_perm('print_labels'):
<script type="text/javascript">
<script>
${grid.component_studly}Data.quickLabelProfile = ${json.dumps(label_profiles[0].uuid)|n}
${grid.component_studly}Data.quickLabelQuantity = 1
${grid.component_studly}Data.quickLabelSpeedbumpThreshold = ${json.dumps(quick_label_speedbump_threshold)|n}
${grid.vue_component}Data.quickLabelProfile = ${json.dumps(label_profiles[0].uuid)|n}
${grid.vue_component}Data.quickLabelQuantity = 1
${grid.vue_component}Data.quickLabelSpeedbumpThreshold = ${json.dumps(quick_label_speedbump_threshold)|n}
${grid.component_studly}.methods.quickLabelPrint = function(row) {
${grid.vue_component}.methods.quickLabelPrint = function(row) {
let quantity = parseInt(this.quickLabelQuantity)
if (isNaN(quantity)) {
@ -83,6 +83,3 @@
</script>
% endif
</%def>
${parent.body()}

View file

@ -2,11 +2,6 @@
<%inherit file="/master/view.mako" />
<%namespace name="product_lookup" file="/products/lookup.mako" />
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
${product_lookup.tailbone_product_lookup_template()}
</%def>
<%def name="page_content()">
${parent.page_content()}
@ -67,9 +62,14 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${product_lookup.tailbone_product_lookup_template()}
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if master.has_perm('ignore_product') and instance.status_code in (enum.PENDING_PRODUCT_STATUS_PENDING, enum.PENDING_PRODUCT_STATUS_READY):
@ -124,10 +124,7 @@
</script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<%def name="make_vue_components()">
${parent.make_vue_components()}
${product_lookup.tailbone_product_lookup_component()}
</%def>
${parent.body()}

View file

@ -282,9 +282,9 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.vendorSourcesData = ${json.dumps(vendor_sources['data'])|n}
ThisPageData.lookupCodesData = ${json.dumps(lookup_codes['data'])|n}
@ -411,6 +411,3 @@
% endif
</script>
</%def>
${parent.body()}

View file

@ -59,27 +59,24 @@
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
${grid.component_studly}Data.changeStatusShowDialog = false
${grid.component_studly}Data.changeStatusOptions = ${json.dumps(status_options)|n}
${grid.component_studly}Data.changeStatusValue = null
${grid.component_studly}Data.changeStatusSubmitting = false
${grid.vue_component}Data.changeStatusShowDialog = false
${grid.vue_component}Data.changeStatusOptions = ${json.dumps(status_options)|n}
${grid.vue_component}Data.changeStatusValue = null
${grid.vue_component}Data.changeStatusSubmitting = false
${grid.component_studly}.methods.changeStatusInit = function() {
${grid.vue_component}.methods.changeStatusInit = function() {
this.changeStatusValue = null
this.changeStatusShowDialog = true
}
${grid.component_studly}.methods.changeStatusSubmit = function() {
${grid.vue_component}.methods.changeStatusSubmit = function() {
this.changeStatusSubmitting = true
this.$refs.changeStatusForm.submit()
}
</script>
</%def>
${parent.body()}

View file

@ -69,12 +69,12 @@
<h3 class="block is-size-3">Vendors</h3>
<div class="block" style="padding-left: 2rem;">
<b-field message="If set, user must choose a &quot;supported&quot; vendor; otherwise they may choose &quot;any&quot; vendor.">
<b-checkbox name="rattail.batch.purchase.supported_vendors_only"
v-model="simpleSettings['rattail.batch.purchase.supported_vendors_only']"
<b-field message="If not set, user must choose a &quot;supported&quot; vendor.">
<b-checkbox name="rattail.batch.purchase.allow_receiving_any_vendor"
v-model="simpleSettings['rattail.batch.purchase.allow_receiving_any_vendor']"
native-value="true"
@input="settingsNeedSaved = true">
Only allow batch for "supported" vendors
Allow receiving for <span class="has-text-weight-bold">any</span> vendor
</b-checkbox>
</b-field>

View file

@ -139,9 +139,15 @@
% endif
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
<%def name="object_helpers()">
${self.render_status_breakdown()}
${self.render_po_vs_invoice_helper()}
${self.render_execute_helper()}
${self.render_tools_helper()}
</%def>
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
% if allow_edit_catalog_unit_cost or allow_edit_invoice_unit_cost:
<script type="text/x-template" id="receiving-cost-editor-template">
<div>
@ -162,16 +168,9 @@
% endif
</%def>
<%def name="object_helpers()">
${self.render_status_breakdown()}
${self.render_po_vs_invoice_helper()}
${self.render_execute_helper()}
${self.render_tools_helper()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if allow_confirm_all_costs:
@ -318,13 +317,13 @@
% if allow_edit_catalog_unit_cost:
${rows_grid.component_studly}.methods.catalogUnitCostClicked = function(row) {
${rows_grid.vue_component}.methods.catalogUnitCostClicked = function(row) {
// start edit for clicked cell
this.$refs['catalogUnitCost_' + row.uuid].startEdit()
}
${rows_grid.component_studly}.methods.catalogCostConfirmed = function(amount, index) {
${rows_grid.vue_component}.methods.catalogCostConfirmed = function(amount, index) {
// update display to indicate cost was confirmed
this.addRowClass(index, 'catalog_cost_confirmed')
@ -353,13 +352,13 @@
% if allow_edit_invoice_unit_cost:
${rows_grid.component_studly}.methods.invoiceUnitCostClicked = function(row) {
${rows_grid.vue_component}.methods.invoiceUnitCostClicked = function(row) {
// start edit for clicked cell
this.$refs['invoiceUnitCost_' + row.uuid].startEdit()
}
${rows_grid.component_studly}.methods.invoiceCostConfirmed = function(amount, index) {
${rows_grid.vue_component}.methods.invoiceCostConfirmed = function(amount, index) {
// update display to indicate cost was confirmed
this.addRowClass(index, 'invoice_cost_confirmed')
@ -389,6 +388,3 @@
</script>
</%def>
${parent.body()}

View file

@ -484,9 +484,9 @@
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
## ThisPage.methods.editUnitCost = function() {
## alert("TODO: not yet implemented")
@ -720,6 +720,3 @@
</script>
</%def>
${parent.body()}

View file

@ -53,13 +53,13 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
TailboneFormData.reportDescriptions = ${json.dumps(report_descriptions)|n}
${form.vue_component}Data.reportDescriptions = ${json.dumps(report_descriptions)|n}
TailboneForm.methods.reportTypeChanged = function(reportType) {
${form.vue_component}.methods.reportTypeChanged = function(reportType) {
this.$emit('report-change', this.reportDescriptions[reportType])
}
@ -71,6 +71,3 @@
</script>
</%def>
${parent.body()}

View file

@ -1,16 +1,11 @@
## -*- coding: utf-8; -*-
<%inherit file="/master/delete.mako" />
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if params_data is not Undefined:
${form.component_studly}Data.paramsData = ${json.dumps(params_data)|n}
${form.vue_component}Data.paramsData = ${json.dumps(params_data)|n}
% endif
</script>
</%def>
${parent.body()}

View file

@ -23,16 +23,11 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if params_data is not Undefined:
${form.component_studly}Data.paramsData = ${json.dumps(params_data)|n}
${form.vue_component}Data.paramsData = ${json.dumps(params_data)|n}
% endif
</script>
</%def>
${parent.body()}

View file

@ -48,15 +48,10 @@
${h.end_form()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.departments = ${json.dumps([{'uuid': d.uuid, 'name': d.name} for d in departments])|n}
ThisPageData.excludeNotForSale = true
</script>
</%def>
${parent.body()}

View file

@ -81,9 +81,9 @@
<%def name="extra_fields()"></%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.vendorUUID = null
ThisPageData.departments = []
@ -127,6 +127,3 @@
</script>
</%def>
${parent.body()}

View file

@ -45,11 +45,10 @@
<b-button @click="runReportShowDialog = false">
Cancel
</b-button>
${h.form(master.get_action_url('execute', instance))}
${h.form(master.get_action_url('execute', instance), **{'@submit': 'runReportSubmitting = true'})}
${h.csrf_token(request)}
<b-button type="is-primary"
native-type="submit"
@click="runReportSubmitting = true"
:disabled="runReportSubmitting"
icon-pack="fas"
icon-left="arrow-circle-right">
@ -62,12 +61,12 @@
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if weekdays_data is not Undefined:
${form.component_studly}Data.weekdaysData = ${json.dumps(weekdays_data)|n}
${form.vue_component}Data.weekdaysData = ${json.dumps(weekdays_data)|n}
% endif
ThisPageData.runReportShowDialog = false
@ -75,6 +74,3 @@
</script>
</%def>
${parent.body()}

View file

@ -6,15 +6,11 @@
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
// TODO: this variable name should be more dynamic (?) since this is
// connected to (and only here b/c of) the permissions field
TailboneFormData.showingPermissionGroup = ''
${form.vue_component}Data.showingPermissionGroup = ''
</script>
</%def>
${parent.body()}

View file

@ -6,15 +6,11 @@
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
// TODO: this variable name should be more dynamic (?) since this is
// connected to (and only here b/c of) the permissions field
TailboneFormData.showingPermissionGroup = ''
${form.vue_component}Data.showingPermissionGroup = ''
</script>
</%def>
${parent.body()}

View file

@ -6,12 +6,12 @@
${h.stylesheet_link(request.static_url('tailbone:static/css/perms.css'))}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
% if users_data is not Undefined:
${form.component_studly}Data.usersData = ${json.dumps(users_data)|n}
${form.vue_component}Data.usersData = ${json.dumps(users_data)|n}
% endif
ThisPage.methods.detachPerson = function(url) {
@ -23,5 +23,3 @@
</script>
</%def>
${parent.body()}

View file

@ -86,9 +86,9 @@
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
ThisPageData.testRecipient = ${json.dumps(user_email_address)|n}
ThisPageData.sendingTest = false
@ -137,6 +137,3 @@
% endif
</script>
</%def>
${parent.body()}

View file

@ -15,10 +15,10 @@
${parent.render_grid_component()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
% if master.has_perm('configure'):
<script type="text/javascript">
<script>
ThisPageData.showEmails = 'available'
@ -26,9 +26,9 @@
this.$refs.grid.showEmails = this.showEmails
}
${grid.component_studly}Data.showEmails = 'available'
${grid.vue_component}Data.showEmails = 'available'
${grid.component_studly}.computed.visibleData = function() {
${grid.vue_component}.computed.visibleData = function() {
if (this.showEmails == 'available') {
return this.data.filter(email => email.hidden == 'No')
@ -41,11 +41,11 @@
return this.data
}
${grid.component_studly}.methods.renderLabelToggleHidden = function(row) {
${grid.vue_component}.methods.renderLabelToggleHidden = function(row) {
return row.hidden == 'Yes' ? "Un-hide" : "Hide"
}
${grid.component_studly}.methods.toggleHidden = function(row) {
${grid.vue_component}.methods.toggleHidden = function(row) {
let url = '${url('{}.toggle_hidden'.format(route_prefix))}'
let params = {
key: row.key,
@ -65,5 +65,3 @@
</script>
% endif
</%def>
${parent.body()}

Some files were not shown because too many files have changed in this diff Show more