Compare commits
320 commits
Author | SHA1 | Date | |
---|---|---|---|
e150453801 | |||
e2582ffec5 | |||
a6508154cb | |||
7348eec671 | |||
4221fa50dd | |||
e0ebd43e7a | |||
c7ee9de9eb | |||
950db697a0 | |||
358b3b75a5 | |||
7e559a01b3 | |||
23bdde245a | |||
2c269b640b | |||
![]() |
f1c8ffedda | ||
![]() |
aace6033c5 | ||
![]() |
7171c7fb06 | ||
![]() |
993f066f2c | ||
![]() |
980031f524 | ||
![]() |
bcaf0d08bc | ||
![]() |
ac439c949b | ||
![]() |
20b3f87dbe | ||
![]() |
9e55717041 | ||
![]() |
772b6610cb | ||
![]() |
3f27f626df | ||
![]() |
29743e70b7 | ||
![]() |
54220601ed | ||
![]() |
9a6f8970ae | ||
![]() |
28f90ad6b5 | ||
![]() |
535317e4f7 | ||
![]() |
072db39233 | ||
![]() |
c6365f2631 | ||
![]() |
d520f64fee | ||
![]() |
2308d2e240 | ||
![]() |
0b4efae392 | ||
![]() |
0b646d2d18 | ||
![]() |
a4d81a6e3c | ||
![]() |
5e742eab17 | ||
![]() |
b9b8bbd2ea | ||
![]() |
8df52bf2a2 | ||
![]() |
55f45ae8a0 | ||
![]() |
2219cf8198 | ||
![]() |
9be2f63475 | ||
![]() |
812d8d2349 | ||
![]() |
20dcdd8b86 | ||
![]() |
bc399182ba | ||
![]() |
71d63f6b93 | ||
![]() |
0b6cfaa9c5 | ||
![]() |
b81914fbf5 | ||
![]() |
b30f066c41 | ||
![]() |
2e20fc5b75 | ||
![]() |
ca05e68890 | ||
![]() |
7a9d5772db | ||
![]() |
dffd951369 | ||
![]() |
d67eb2f1cc | ||
![]() |
3a9bf69aa7 | ||
![]() |
d1f4c0f150 | ||
![]() |
b7991b5dc6 | ||
![]() |
c1a2c9cc70 | ||
![]() |
37f760959d | ||
![]() |
cea3e4b927 | ||
![]() |
29531c83c4 | ||
![]() |
4c3e3aeb6a | ||
![]() |
c176d97870 | ||
![]() |
7d6f75bb05 | ||
![]() |
7b40c527c8 | ||
![]() |
f292850d05 | ||
![]() |
8d5427e92f | ||
![]() |
b8131c8393 | ||
![]() |
e52a83751e | ||
![]() |
ffa724ef37 | ||
![]() |
1d00fe994a | ||
![]() |
71abbe06da | ||
![]() |
f755460242 | ||
![]() |
2ffc067097 | ||
![]() |
b6a8e508bf | ||
![]() |
1def26a35b | ||
![]() |
07871188aa | ||
![]() |
c8dc60cb68 | ||
![]() |
526c84dfa6 | ||
![]() |
21f90f3f32 | ||
![]() |
83586ef90f | ||
![]() |
59bd58aca7 | ||
![]() |
1ec1eba496 | ||
![]() |
d29b840343 | ||
![]() |
b762a0782a | ||
![]() |
15ab0c9592 | ||
![]() |
41945c5e37 | ||
![]() |
f5661fe349 | ||
![]() |
0eeeb4bd35 | ||
![]() |
1d56a4c0d0 | ||
![]() |
b642c98d40 | ||
![]() |
0fb3c0f3d2 | ||
![]() |
b7955a5871 | ||
![]() |
290f8fd51e | ||
![]() |
ec36df4a34 | ||
![]() |
c95e42bf82 | ||
![]() |
5e82fe3946 | ||
![]() |
f4c8176d83 | ||
![]() |
9da2a148c6 | ||
![]() |
2a0b6da2f9 | ||
![]() |
f7641218cb | ||
![]() |
1b78bd617c | ||
![]() |
09612b1921 | ||
![]() |
bbd98e7b2f | ||
![]() |
da0f6bd5e1 | ||
![]() |
bbc2c584ec | ||
![]() |
7f0c571a44 | ||
![]() |
53040dc6be | ||
![]() |
1cacfab2a6 | ||
![]() |
bab09e3fe7 | ||
![]() |
dd176a5e9e | ||
![]() |
a6ce5eb21d | ||
![]() |
b53479f8e4 | ||
![]() |
1f752530d2 | ||
![]() |
2c46fde742 | ||
![]() |
d57efba381 | ||
![]() |
f2fce2e305 | ||
![]() |
b5f0ecb165 | ||
![]() |
7e683dfc4a | ||
![]() |
0b8315fc78 | ||
![]() |
ffd694e7b7 | ||
![]() |
80dc4eb7a9 | ||
![]() |
518c108c88 | ||
![]() |
bd1993f440 | ||
![]() |
91ea9021d7 | ||
![]() |
2903b376b5 | ||
![]() |
9d2684046f | ||
![]() |
3b92bb3a9e | ||
![]() |
5ec899cf08 | ||
![]() |
458c95696a | ||
![]() |
08a89c490a | ||
![]() |
a9495b6a70 | ||
![]() |
1bba6d9947 | ||
![]() |
f4f79f170a | ||
![]() |
9c466796da | ||
![]() |
e88b8fc9bc | ||
![]() |
3aafe578f0 | ||
![]() |
af0f84762c | ||
![]() |
be6eb5f815 | ||
![]() |
57fdacdb83 | ||
![]() |
ece29d7b6c | ||
![]() |
5e1c0a5187 | ||
![]() |
25e62fe6ef | ||
![]() |
d70bac74f0 | ||
![]() |
fd1ec01128 | ||
![]() |
0b4629ea29 | ||
![]() |
27214cc62f | ||
![]() |
d2d0206b45 | ||
![]() |
eede274529 | ||
![]() |
ee781ec489 | ||
![]() |
ca660f4087 | ||
![]() |
ce156d6278 | ||
![]() |
e531f98079 | ||
![]() |
09ce2d5a40 | ||
![]() |
ae8212069c | ||
![]() |
4eb5866379 | ||
![]() |
a86a33445e | ||
![]() |
12f8b7bdf7 | ||
![]() |
2f2ebd0f07 | ||
![]() |
2917463bb6 | ||
![]() |
16bf13787d | ||
![]() |
b7d26b6b8c | ||
![]() |
19e65f5bb9 | ||
![]() |
735327e46b | ||
![]() |
2988ff3ee9 | ||
![]() |
431a4d7433 | ||
![]() |
58be7e9d5b | ||
![]() |
ddec77c37f | ||
![]() |
89d7009a18 | ||
![]() |
793a15883e | ||
![]() |
76897c24de | ||
![]() |
5e11a2ecf6 | ||
![]() |
e23193b730 | ||
![]() |
9146cdc835 | ||
![]() |
1f38894f02 | ||
![]() |
d72d6f8c7c | ||
![]() |
aab4dec27e | ||
![]() |
db67630363 | ||
![]() |
c887412825 | ||
![]() |
2feb07e1d3 | ||
![]() |
6f8b825b0b | ||
![]() |
cad50c9149 | ||
![]() |
d6939e52b4 | ||
![]() |
3f7de5872e | ||
![]() |
1dc632174e | ||
![]() |
eff5341335 | ||
![]() |
83e4d95741 | ||
![]() |
9b6447c4cb | ||
![]() |
ec5ed490d9 | ||
![]() |
d17bd35909 | ||
![]() |
3b7cc19faa | ||
![]() |
067ca5bd43 | ||
![]() |
525a28f3fe | ||
![]() |
a0cd8835e0 | ||
![]() |
231ca0363a | ||
![]() |
88e7d86087 | ||
![]() |
0212e52b66 | ||
![]() |
da4450b574 | ||
![]() |
ab4dbbedf0 | ||
![]() |
6e741f6156 | ||
![]() |
fb0c538a2b | ||
![]() |
f9cb6cb59b | ||
![]() |
1402d437b5 | ||
![]() |
dd58c640fa | ||
![]() |
2c2727bf66 | ||
![]() |
b8ace1eb98 | ||
![]() |
7c3d5b46f3 | ||
![]() |
a849d8452b | ||
![]() |
610e1666c0 | ||
![]() |
94d7836321 | ||
![]() |
0491d8517c | ||
![]() |
f6f2a53a0c | ||
![]() |
ce290f5f8b | ||
![]() |
d9911cf23d | ||
![]() |
1afc70e788 | ||
![]() |
c189273471 | ||
![]() |
22aceb4d67 | ||
![]() |
da6ccf4425 | ||
![]() |
d02bf0e5c7 | ||
![]() |
10aac388f0 | ||
![]() |
00e2af1561 | ||
![]() |
6a7c06d26e | ||
![]() |
efe477d0db | ||
![]() |
e17ef2edd8 | ||
![]() |
30a8b8e5e4 | ||
![]() |
2498da3909 | ||
![]() |
2791e1c385 | ||
![]() |
0303014acb | ||
![]() |
9243edf7af | ||
![]() |
ab523719a6 | ||
![]() |
b27987f1d1 | ||
![]() |
30238528fe | ||
![]() |
29c9ea1a2b | ||
![]() |
58f9588261 | ||
![]() |
3dc8deef67 | ||
![]() |
fa25857680 | ||
![]() |
254df6d6f2 | ||
![]() |
40edde2694 | ||
![]() |
9258237b85 | ||
![]() |
1bf28eb286 | ||
![]() |
77eeb63b62 | ||
![]() |
43db60bbee | ||
![]() |
6b1c313efd | ||
![]() |
d05458c7fb | ||
![]() |
b87b1a3801 | ||
![]() |
ba519334d1 | ||
![]() |
3ac131cb51 | ||
![]() |
9b88f01378 | ||
![]() |
49cd050272 | ||
![]() |
0d8928bdf5 | ||
![]() |
54b75dbe1a | ||
![]() |
b98d651144 | ||
![]() |
9a841ba5e2 | ||
![]() |
4ccdf99a43 | ||
![]() |
f8ab8d462c | ||
![]() |
fb9bc01939 | ||
![]() |
ec61444b3d | ||
![]() |
66304a418e | ||
![]() |
6bb6c16bc7 | ||
![]() |
c43deb1307 | ||
![]() |
b65b514270 | ||
![]() |
9b65e18261 | ||
![]() |
b40423fc2d | ||
![]() |
28fb3f44a7 | ||
![]() |
d607ab2981 | ||
![]() |
9cd648f78f | ||
![]() |
703d583f6f | ||
![]() |
f0d694cfe5 | ||
![]() |
3d319cbd09 | ||
![]() |
e4c4259674 | ||
![]() |
15fedf5976 | ||
![]() |
68384a00dc | ||
![]() |
e9ddd6dc36 | ||
![]() |
6ce65badeb | ||
![]() |
9ee6521d6a | ||
![]() |
72f48fa963 | ||
![]() |
b3784dcc4a | ||
![]() |
34878f9293 | ||
![]() |
adaa39f572 | ||
![]() |
1d5a0630ef | ||
![]() |
855fa7e1e2 | ||
![]() |
f2f023e7b3 | ||
![]() |
33251e880e | ||
![]() |
358816d9e7 | ||
![]() |
362d545f34 | ||
![]() |
fb81a8302c | ||
![]() |
e7a44d9979 | ||
![]() |
2eaeb1891d | ||
![]() |
5aa8d1f9a3 | ||
![]() |
098ed5b1cf | ||
![]() |
890ec64f3c | ||
![]() |
ba32422059 | ||
![]() |
8b3a9c9dad | ||
![]() |
2a22e8939c | ||
![]() |
6bee65780c | ||
![]() |
e030dc841d | ||
![]() |
25a27af29c | ||
![]() |
daf68cad01 | ||
![]() |
ab57fb3f0f | ||
![]() |
f43259fbc1 | ||
![]() |
bfe6b5bc25 | ||
![]() |
23e6eef604 | ||
![]() |
d2aa91502a | ||
![]() |
4f6ee1fb22 | ||
![]() |
ddafa9ed97 | ||
![]() |
0ca3b31b2e | ||
![]() |
9f984241c4 | ||
![]() |
d6fa83cd87 | ||
![]() |
8781e34c98 | ||
![]() |
49da9776e7 | ||
![]() |
36b9e00dc9 | ||
![]() |
5cb643a32a | ||
![]() |
1fa6e35663 | ||
![]() |
e82f0f37d8 | ||
![]() |
7fa39d42e2 | ||
![]() |
a95cc2b9e8 | ||
![]() |
e7b8b6e818 | ||
![]() |
5a7deadba2 | ||
![]() |
9065f42195 | ||
![]() |
b37981e83f | ||
![]() |
0d9c5a078b |
249 changed files with 14546 additions and 7017 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,8 @@
|
||||||
|
*~
|
||||||
|
*.pyc
|
||||||
.coverage
|
.coverage
|
||||||
.tox/
|
.tox/
|
||||||
|
dist/
|
||||||
docs/_build/
|
docs/_build/
|
||||||
htmlcov/
|
htmlcov/
|
||||||
Tailbone.egg-info/
|
Tailbone.egg-info/
|
||||||
|
|
683
CHANGELOG.md
Normal file
683
CHANGELOG.md
Normal file
|
@ -0,0 +1,683 @@
|
||||||
|
|
||||||
|
# Changelog
|
||||||
|
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
|
||||||
|
|
||||||
|
- update usage of auth handler, per rattail changes
|
||||||
|
- fix model reference in menu handler
|
||||||
|
- fix bug when making "integration" menus
|
||||||
|
|
||||||
|
## v0.14.0 (2024-07-14)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- move core menu logic to wuttaweb
|
||||||
|
|
||||||
|
## v0.13.2 (2024-07-13)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix logic bug for datasync/config settings save
|
||||||
|
|
||||||
|
## v0.13.1 (2024-07-13)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix settings persistence bug(s) for datasync/configure page
|
||||||
|
|
||||||
|
## v0.13.0 (2024-07-12)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- begin integrating WuttaWeb as upstream dependency
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- cast enum as list to satisfy deform widget
|
||||||
|
|
||||||
|
## v0.12.1 (2024-07-11)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- refactor `config.get_model()` => `app.model`
|
||||||
|
|
||||||
|
## v0.12.0 (2024-07-09)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- drop python 3.6 support, use pyproject.toml (again)
|
||||||
|
|
||||||
|
## v0.11.10 (2024-07-05)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- make the Members tab optional, for profile view
|
||||||
|
|
||||||
|
## v0.11.9 (2024-07-05)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- do not show flash message when changing app theme
|
||||||
|
|
||||||
|
- improve collapse panels for butterball theme
|
||||||
|
|
||||||
|
- expand input for butterball theme
|
||||||
|
|
||||||
|
- add xref button to customer profile, for trainwreck txn view
|
||||||
|
|
||||||
|
- add optional Transactions tab for profile view
|
||||||
|
|
||||||
|
## v0.11.8 (2024-07-04)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix grid action icons for datasync/configure, per oruga
|
||||||
|
|
||||||
|
- allow view supplements to add extra links for profile employee tab
|
||||||
|
|
||||||
|
- leverage import handler method to determine command/subcommand
|
||||||
|
|
||||||
|
- add tool to make user account from profile view
|
||||||
|
|
||||||
|
## v0.11.7 (2024-07-04)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- add stacklevel to deprecation warnings
|
||||||
|
|
||||||
|
- require zope.sqlalchemy >= 1.5
|
||||||
|
|
||||||
|
- include edit profile email/phone dialogs only if user has perms
|
||||||
|
|
||||||
|
- allow view supplements to add to profile member context
|
||||||
|
|
||||||
|
- cast enum as list to satisfy deform widget
|
||||||
|
|
||||||
|
- expand POD image URL setting input
|
||||||
|
|
||||||
|
## v0.11.6 (2024-07-01)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- set explicit referrer when changing dbkey
|
||||||
|
|
||||||
|
- remove references, dependency for `six` package
|
||||||
|
|
||||||
|
## v0.11.5 (2024-06-30)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- allow comma in numeric filter input
|
||||||
|
|
||||||
|
- add custom url prefix if needed, for fanstatic
|
||||||
|
|
||||||
|
- use vue 3.4.31 and oruga 0.8.12 by default
|
||||||
|
|
||||||
|
## v0.11.4 (2024-06-30)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- start/stop being root should submit POST instead of GET
|
||||||
|
|
||||||
|
- require vendor when making new ordering batch via api
|
||||||
|
|
||||||
|
- don't escape each address for email attempts grid
|
||||||
|
|
||||||
|
## v0.11.3 (2024-06-28)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- add link to "resolved by" user for pending products
|
||||||
|
|
||||||
|
- handle error when merging 2 records fails
|
||||||
|
|
||||||
|
## v0.11.2 (2024-06-18)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- hide certain custorder settings if not applicable
|
||||||
|
|
||||||
|
- use different logic for buefy/oruga for product lookup keydown
|
||||||
|
|
||||||
|
- product records should be touchable
|
||||||
|
|
||||||
|
- show flash error message if resolve pending product fails
|
||||||
|
|
||||||
|
## v0.11.1 (2024-06-14)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- revert back to setup.py + setup.cfg
|
||||||
|
|
||||||
|
## v0.11.0 (2024-06-10)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- switch from setup.cfg to pyproject.toml + hatchling
|
||||||
|
|
||||||
|
## v0.10.16 (2024-06-10)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- standardize how app, package versions are determined
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- avoid deprecated config methods for app/node title
|
||||||
|
|
||||||
|
## v0.10.15 (2024-06-07)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- do *not* Use `pkg_resources` to determine package versions
|
||||||
|
|
||||||
|
## v0.10.14 (2024-06-06)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- use `pkg_resources` to determine package versions
|
||||||
|
|
||||||
|
## v0.10.13 (2024-06-06)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- remove old/unused scaffold for use with `pcreate`
|
||||||
|
|
||||||
|
- add 'fanstatic' support for sake of libcache assets
|
||||||
|
|
||||||
|
## v0.10.12 (2024-06-04)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- require pyramid 2.x; remove 1.x-style auth policies
|
||||||
|
|
||||||
|
- remove version cap for deform
|
||||||
|
|
||||||
|
- set explicit referrer when changing app theme
|
||||||
|
|
||||||
|
- add `<b-tooltip>` component shim
|
||||||
|
|
||||||
|
- include extra styles from `base_meta` template for butterball
|
||||||
|
|
||||||
|
- include butterball theme by default for new apps
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix product lookup component, per butterball
|
||||||
|
|
||||||
|
## v0.10.11 (2024-06-03)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- fix vue3 refresh bugs for various views
|
||||||
|
|
||||||
|
- fix grid bug for tempmon appliance view, per oruga
|
||||||
|
|
||||||
|
- fix ordering worksheet generator, per butterball
|
||||||
|
|
||||||
|
- fix inventory worksheet generator, per butterball
|
||||||
|
|
||||||
|
## v0.10.10 (2024-06-03)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- more butterball fixes for "view profile" template
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix focus for `<b-select>` shim component
|
||||||
|
|
||||||
|
## v0.10.9 (2024-06-03)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- let master view control context menu items for page
|
||||||
|
|
||||||
|
- fix the "new custorder" page for butterball
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix panel style for PO vs. Invoice breakdown in receiving batch
|
||||||
|
|
||||||
|
## v0.10.8 (2024-06-02)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- add styling for checked grid rows, per oruga/butterball
|
||||||
|
|
||||||
|
- fix product view template for oruga/butterball
|
||||||
|
|
||||||
|
- allow per-user custom styles for butterball
|
||||||
|
|
||||||
|
- use oruga 0.8.9 by default
|
||||||
|
|
||||||
|
## v0.10.7 (2024-06-01)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- add setting to allow decimal quantities for receiving
|
||||||
|
|
||||||
|
- log error if registry has no rattail config
|
||||||
|
|
||||||
|
- add column filters for import/export main grid
|
||||||
|
|
||||||
|
- escape all unsafe html for grid data
|
||||||
|
|
||||||
|
- add speedbumps for delete, set preferred email/phone in profile view
|
||||||
|
|
||||||
|
- fix file upload widget for oruga
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix overflow when instance header title is too long (butterball)
|
||||||
|
|
||||||
|
## v0.10.6 (2024-05-29)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- add way to flag organic products within lookup dialog
|
||||||
|
|
||||||
|
- expose db picker for butterball theme
|
||||||
|
|
||||||
|
- expose quickie lookup for butterball theme
|
||||||
|
|
||||||
|
- fix basic problems with people profile view, per butterball
|
||||||
|
|
||||||
|
## v0.10.5 (2024-05-29)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- add `<tailbone-timepicker>` component for oruga
|
||||||
|
|
||||||
|
## v0.10.4 (2024-05-12)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix styles for grid actions, per butterball
|
||||||
|
|
||||||
|
## v0.10.3 (2024-05-10)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix bug with grid date filters
|
||||||
|
|
||||||
|
## v0.10.2 (2024-05-08)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- remove version restriction for pyramid_beaker dependency
|
||||||
|
|
||||||
|
- rename some attrs etc. for buefy components used with oruga
|
||||||
|
|
||||||
|
- fix "tools" helper for receiving batch view, per oruga
|
||||||
|
|
||||||
|
- more data type fixes for ``<tailbone-datepicker>``
|
||||||
|
|
||||||
|
- fix "view receiving row" page, per oruga
|
||||||
|
|
||||||
|
- tweak styles for grid action links, per butterball
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix employees grid when viewing department (per oruga)
|
||||||
|
|
||||||
|
- fix login "enter" key behavior, per oruga
|
||||||
|
|
||||||
|
- fix button text for autocomplete
|
||||||
|
|
||||||
|
## v0.10.1 (2024-04-28)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- sort list of available themes
|
||||||
|
|
||||||
|
- update various icon names for oruga compatibility
|
||||||
|
|
||||||
|
- show "View This" button when cloning a record
|
||||||
|
|
||||||
|
- stop including 'falafel' as available theme
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix vertical alignment in main menu bar, for butterball
|
||||||
|
|
||||||
|
- fix upgrade execution logic/UI per oruga
|
||||||
|
|
||||||
|
## v0.10.0 (2024-04-28)
|
||||||
|
|
||||||
|
This version bump is to reflect adding support for Vue 3 + Oruga via
|
||||||
|
the 'butterball' theme. There is likely more work to be done for that
|
||||||
|
yet, but it mostly works at this point.
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- misc. template and view logic tweaks (applicable to all themes) for
|
||||||
|
better patterns, consistency etc.
|
||||||
|
|
||||||
|
- add initial support for Vue 3 + Oruga, via "butterball" theme
|
||||||
|
|
||||||
|
|
||||||
|
## Older Releases
|
||||||
|
|
||||||
|
Please see `docs/OLDCHANGES.rst` for older release notes.
|
|
@ -1,10 +1,8 @@
|
||||||
|
|
||||||
Tailbone
|
# Tailbone
|
||||||
========
|
|
||||||
|
|
||||||
Tailbone is an extensible web application based on Rattail. It provides a
|
Tailbone is an extensible web application based on Rattail. It provides a
|
||||||
"back-office network environment" (BONE) for use in managing retail data.
|
"back-office network environment" (BONE) for use in managing retail data.
|
||||||
|
|
||||||
Please see Rattail's `home page`_ for more information.
|
Please see Rattail's [home page](http://rattailproject.org/) for more
|
||||||
|
information.
|
||||||
.. _home page: http://rattailproject.org/
|
|
|
@ -2,8 +2,55 @@
|
||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
Unreleased
|
NB. this file contains "old" release notes only. for newer releases
|
||||||
----------
|
see the `CHANGELOG.md` file in the source root folder.
|
||||||
|
|
||||||
|
|
||||||
|
0.9.96 (2024-04-25)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
* Remove unused code for ``webhelpers2_grid``.
|
||||||
|
|
||||||
|
* Rename setting for custom user css (remove "buefy").
|
||||||
|
|
||||||
|
* Fix permission checks for root user with pyramid 2.x.
|
||||||
|
|
||||||
|
* Cleanup grid/filters logic a bit.
|
||||||
|
|
||||||
|
* Use normal (not checkbox) button for grid filters.
|
||||||
|
|
||||||
|
* Tweak icon for Download Results button.
|
||||||
|
|
||||||
|
* Use v-model to track selection etc. for download results fields.
|
||||||
|
|
||||||
|
* Allow deleting rows from executed batches.
|
||||||
|
|
||||||
|
|
||||||
|
0.9.95 (2024-04-19)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
* Fix ASGI websockets when serving on sub-path under site root.
|
||||||
|
|
||||||
|
* Fix raw query to avoid SQLAlchemy 2.x warnings.
|
||||||
|
|
||||||
|
* Remove config "style" from appinfo page.
|
||||||
|
|
||||||
|
|
||||||
|
0.9.94 (2024-04-16)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
* Fix master template bug when no form in context.
|
||||||
|
|
||||||
|
|
||||||
|
0.9.93 (2024-04-16)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
* Improve form support for view supplements.
|
||||||
|
|
||||||
|
* Prevent multi-click for grid filters "Save Defaults" button.
|
||||||
|
|
||||||
|
* Fix typo when getting app instance.
|
||||||
|
|
||||||
|
|
||||||
0.9.92 (2024-04-16)
|
0.9.92 (2024-04-16)
|
||||||
-------------------
|
-------------------
|
||||||
|
@ -4945,7 +4992,7 @@ and related technologies.
|
||||||
0.6.47 (2017-11-08)
|
0.6.47 (2017-11-08)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
* Fix manifest to include *.pt deform templates
|
* Fix manifest to include ``*.pt`` deform templates
|
||||||
|
|
||||||
|
|
||||||
0.6.46 (2017-11-08)
|
0.6.46 (2017-11-08)
|
||||||
|
@ -5278,13 +5325,13 @@ and related technologies.
|
||||||
|
|
||||||
|
|
||||||
0.6.13 (2017-07-26)
|
0.6.13 (2017-07-26)
|
||||||
------------------
|
-------------------
|
||||||
|
|
||||||
* Allow master view to decide whether each grid checkbox is checked
|
* Allow master view to decide whether each grid checkbox is checked
|
||||||
|
|
||||||
|
|
||||||
0.6.12 (2017-07-26)
|
0.6.12 (2017-07-26)
|
||||||
------------------
|
-------------------
|
||||||
|
|
||||||
* Add basic support for product inventory and status
|
* Add basic support for product inventory and status
|
||||||
|
|
||||||
|
@ -5292,7 +5339,7 @@ and related technologies.
|
||||||
|
|
||||||
|
|
||||||
0.6.11 (2017-07-18)
|
0.6.11 (2017-07-18)
|
||||||
------------------
|
-------------------
|
||||||
|
|
||||||
* Tweak some basic styles for forms/grids
|
* Tweak some basic styles for forms/grids
|
||||||
|
|
||||||
|
@ -5300,7 +5347,7 @@ and related technologies.
|
||||||
|
|
||||||
|
|
||||||
0.6.10 (2017-07-18)
|
0.6.10 (2017-07-18)
|
||||||
------------------
|
-------------------
|
||||||
|
|
||||||
* Fix grid bug if "current page" becomes invalid
|
* Fix grid bug if "current page" becomes invalid
|
||||||
|
|
6
docs/api/db.rst
Normal file
6
docs/api/db.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
``tailbone.db``
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. automodule:: tailbone.db
|
||||||
|
:members:
|
|
@ -3,5 +3,4 @@
|
||||||
========================
|
========================
|
||||||
|
|
||||||
.. automodule:: tailbone.subscribers
|
.. automodule:: tailbone.subscribers
|
||||||
|
:members:
|
||||||
.. autofunction:: new_request
|
|
||||||
|
|
6
docs/api/util.rst
Normal file
6
docs/api/util.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
``tailbone.util``
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. automodule:: tailbone.util
|
||||||
|
:members:
|
8
docs/changelog.rst
Normal file
8
docs/changelog.rst
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
Changelog Archive
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
OLDCHANGES
|
278
docs/conf.py
278
docs/conf.py
|
@ -1,38 +1,21 @@
|
||||||
# -*- coding: utf-8; -*-
|
# Configuration file for the Sphinx documentation builder.
|
||||||
#
|
#
|
||||||
# Tailbone documentation build configuration file, created by
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
# sphinx-quickstart on Sat Feb 15 23:15:27 2014.
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
#
|
|
||||||
# This file is exec()d with the current directory set to its
|
|
||||||
# containing dir.
|
|
||||||
#
|
|
||||||
# Note that not all possible configuration values are present in this
|
|
||||||
# autogenerated file.
|
|
||||||
#
|
|
||||||
# All configuration values have a default; values that are commented out
|
|
||||||
# serve to show the default.
|
|
||||||
|
|
||||||
import sys
|
# -- Project information -----------------------------------------------------
|
||||||
import os
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
import sphinx_rtd_theme
|
from importlib.metadata import version as get_version
|
||||||
|
|
||||||
exec(open(os.path.join(os.pardir, 'tailbone', '_version.py')).read())
|
project = 'Tailbone'
|
||||||
|
copyright = '2010 - 2024, Lance Edgar'
|
||||||
|
author = 'Lance Edgar'
|
||||||
|
release = get_version('Tailbone')
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
||||||
#sys.path.insert(0, os.path.abspath('.'))
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
|
||||||
#needs_sphinx = '1.0'
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
|
||||||
# ones.
|
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
'sphinx.ext.todo',
|
'sphinx.ext.todo',
|
||||||
|
@ -40,241 +23,30 @@ extensions = [
|
||||||
'sphinx.ext.viewcode',
|
'sphinx.ext.viewcode',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
templates_path = ['_templates']
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
'rattail': ('https://rattailproject.org/docs/rattail/', None),
|
'rattail': ('https://docs.wuttaproject.org/rattail/', None),
|
||||||
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
||||||
|
'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None),
|
||||||
|
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# allow todo entries to show up
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The encoding of source files.
|
|
||||||
#source_encoding = 'utf-8-sig'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = u'Tailbone'
|
|
||||||
copyright = u'2010 - 2020, Lance Edgar'
|
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
|
||||||
# |version| and |release|, also used in various other places throughout the
|
|
||||||
# built documents.
|
|
||||||
#
|
|
||||||
# The short X.Y version.
|
|
||||||
# version = '0.3'
|
|
||||||
version = '.'.join(__version__.split('.')[:2])
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
|
||||||
release = __version__
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
|
||||||
# for a list of supported languages.
|
|
||||||
#language = None
|
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
|
||||||
# non-false value, then it is used:
|
|
||||||
#today = ''
|
|
||||||
# Else, today_fmt is used as the format for a strftime call.
|
|
||||||
#today_fmt = '%B %d, %Y'
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
exclude_patterns = ['_build']
|
|
||||||
|
|
||||||
# The reST default role (used for this markup: `text`) to use for all
|
|
||||||
# documents.
|
|
||||||
#default_role = None
|
|
||||||
|
|
||||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
|
||||||
#add_function_parentheses = True
|
|
||||||
|
|
||||||
# If true, the current module name will be prepended to all description
|
|
||||||
# unit titles (such as .. function::).
|
|
||||||
#add_module_names = True
|
|
||||||
|
|
||||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
|
||||||
# output. They are ignored by default.
|
|
||||||
#show_authors = False
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
|
||||||
#modindex_common_prefix = []
|
|
||||||
|
|
||||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
|
||||||
#keep_warnings = False
|
|
||||||
|
|
||||||
# Allow todo entries to show up.
|
|
||||||
todo_include_todos = True
|
todo_include_todos = True
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
html_theme = 'furo'
|
||||||
# a list of builtin themes.
|
html_static_path = ['_static']
|
||||||
# html_theme = 'classic'
|
|
||||||
html_theme = 'sphinx_rtd_theme'
|
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
|
||||||
# further. For a list of options available for each theme, see the
|
|
||||||
# documentation.
|
|
||||||
#html_theme_options = {}
|
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
|
||||||
#html_theme_path = []
|
|
||||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
|
||||||
|
|
||||||
# The name for this set of Sphinx documents. If None, it defaults to
|
|
||||||
# "<project> v<release> documentation".
|
|
||||||
#html_title = None
|
|
||||||
|
|
||||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
|
||||||
#html_short_title = None
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
# of the sidebar.
|
# of the sidebar.
|
||||||
#html_logo = None
|
#html_logo = None
|
||||||
html_logo = 'images/rattail_avatar.png'
|
#html_logo = 'images/rattail_avatar.png'
|
||||||
|
|
||||||
# The name of an image file (within the static path) to use as favicon of the
|
|
||||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
|
||||||
# pixels large.
|
|
||||||
#html_favicon = None
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
html_static_path = ['_static']
|
|
||||||
|
|
||||||
# Add any extra paths that contain custom files (such as robots.txt or
|
|
||||||
# .htaccess) here, relative to this directory. These files are copied
|
|
||||||
# directly to the root of the documentation.
|
|
||||||
#html_extra_path = []
|
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
|
||||||
# using the given strftime format.
|
|
||||||
#html_last_updated_fmt = '%b %d, %Y'
|
|
||||||
|
|
||||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
|
||||||
# typographically correct entities.
|
|
||||||
#html_use_smartypants = True
|
|
||||||
|
|
||||||
# Custom sidebar templates, maps document names to template names.
|
|
||||||
#html_sidebars = {}
|
|
||||||
|
|
||||||
# Additional templates that should be rendered to pages, maps page names to
|
|
||||||
# template names.
|
|
||||||
#html_additional_pages = {}
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
#html_domain_indices = True
|
|
||||||
|
|
||||||
# If false, no index is generated.
|
|
||||||
#html_use_index = True
|
|
||||||
|
|
||||||
# If true, the index is split into individual pages for each letter.
|
|
||||||
#html_split_index = False
|
|
||||||
|
|
||||||
# If true, links to the reST sources are added to the pages.
|
|
||||||
#html_show_sourcelink = True
|
|
||||||
|
|
||||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
|
||||||
#html_show_sphinx = True
|
|
||||||
|
|
||||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
|
||||||
#html_show_copyright = True
|
|
||||||
|
|
||||||
# If true, an OpenSearch description file will be output, and all pages will
|
|
||||||
# contain a <link> tag referring to it. The value of this option must be the
|
|
||||||
# base URL from which the finished HTML is served.
|
|
||||||
#html_use_opensearch = ''
|
|
||||||
|
|
||||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
|
||||||
#html_file_suffix = None
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = 'Tailbonedoc'
|
#htmlhelp_basename = 'Tailbonedoc'
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
|
||||||
|
|
||||||
latex_elements = {
|
|
||||||
# The paper size ('letterpaper' or 'a4paper').
|
|
||||||
#'papersize': 'letterpaper',
|
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
|
||||||
#'pointsize': '10pt',
|
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
|
||||||
#'preamble': '',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title,
|
|
||||||
# author, documentclass [howto, manual, or own class]).
|
|
||||||
latex_documents = [
|
|
||||||
('index', 'Tailbone.tex', u'Tailbone Documentation',
|
|
||||||
u'Lance Edgar', 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
|
||||||
# the title page.
|
|
||||||
#latex_logo = None
|
|
||||||
|
|
||||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
|
||||||
# not chapters.
|
|
||||||
#latex_use_parts = False
|
|
||||||
|
|
||||||
# If true, show page references after internal links.
|
|
||||||
#latex_show_pagerefs = False
|
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
|
||||||
#latex_show_urls = False
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
#latex_appendices = []
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
#latex_domain_indices = True
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for manual page output ---------------------------------------
|
|
||||||
|
|
||||||
# One entry per manual page. List of tuples
|
|
||||||
# (source start file, name, description, authors, manual section).
|
|
||||||
man_pages = [
|
|
||||||
('index', 'tailbone', u'Tailbone Documentation',
|
|
||||||
[u'Lance Edgar'], 1)
|
|
||||||
]
|
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
|
||||||
#man_show_urls = False
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for Texinfo output -------------------------------------------
|
|
||||||
|
|
||||||
# Grouping the document tree into Texinfo files. List of tuples
|
|
||||||
# (source start file, target name, title, author,
|
|
||||||
# dir menu entry, description, category)
|
|
||||||
texinfo_documents = [
|
|
||||||
('index', 'Tailbone', u'Tailbone Documentation',
|
|
||||||
u'Lance Edgar', 'Tailbone', 'One line description of project.',
|
|
||||||
'Miscellaneous'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
#texinfo_appendices = []
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
#texinfo_domain_indices = True
|
|
||||||
|
|
||||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
|
||||||
#texinfo_show_urls = 'footnote'
|
|
||||||
|
|
||||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
|
||||||
#texinfo_no_detailmenu = False
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ Package API:
|
||||||
|
|
||||||
api/api/batch/core
|
api/api/batch/core
|
||||||
api/api/batch/ordering
|
api/api/batch/ordering
|
||||||
|
api/db
|
||||||
api/diffs
|
api/diffs
|
||||||
api/forms
|
api/forms
|
||||||
api/forms.widgets
|
api/forms.widgets
|
||||||
|
@ -51,6 +52,7 @@ Package API:
|
||||||
api/grids.core
|
api/grids.core
|
||||||
api/progress
|
api/progress
|
||||||
api/subscribers
|
api/subscribers
|
||||||
|
api/util
|
||||||
api/views/batch
|
api/views/batch
|
||||||
api/views/batch.vendorcatalog
|
api/views/batch.vendorcatalog
|
||||||
api/views/core
|
api/views/core
|
||||||
|
@ -60,6 +62,14 @@ Package API:
|
||||||
api/views/purchasing.ordering
|
api/views/purchasing.ordering
|
||||||
|
|
||||||
|
|
||||||
|
Changelog:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
changelog
|
||||||
|
|
||||||
|
|
||||||
Documentation To-Do
|
Documentation To-Do
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|
103
pyproject.toml
Normal file
103
pyproject.toml
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "Tailbone"
|
||||||
|
version = "0.22.7"
|
||||||
|
description = "Backoffice Web Application for Rattail"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||||
|
license = {text = "GNU GPL v3+"}
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Web Environment",
|
||||||
|
"Framework :: Pyramid",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||||
|
"Natural Language :: English",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Topic :: Internet :: WWW/HTTP",
|
||||||
|
"Topic :: Office/Business",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
]
|
||||||
|
requires-python = ">= 3.8"
|
||||||
|
dependencies = [
|
||||||
|
"asgiref",
|
||||||
|
"colander",
|
||||||
|
"ColanderAlchemy",
|
||||||
|
"cornice",
|
||||||
|
"cornice-swagger",
|
||||||
|
"deform",
|
||||||
|
"humanize",
|
||||||
|
"Mako",
|
||||||
|
"markdown",
|
||||||
|
"openpyxl",
|
||||||
|
"paginate",
|
||||||
|
"paginate_sqlalchemy",
|
||||||
|
"passlib",
|
||||||
|
"Pillow",
|
||||||
|
"pyramid>=2",
|
||||||
|
"pyramid_beaker",
|
||||||
|
"pyramid_deform",
|
||||||
|
"pyramid_exclog",
|
||||||
|
"pyramid_fanstatic",
|
||||||
|
"pyramid_mako",
|
||||||
|
"pyramid_retry",
|
||||||
|
"pyramid_tm",
|
||||||
|
"rattail[db,bouncer]>=0.20.1",
|
||||||
|
"sa-filters",
|
||||||
|
"simplejson",
|
||||||
|
"transaction",
|
||||||
|
"waitress",
|
||||||
|
"WebHelpers2",
|
||||||
|
"WuttaWeb>=0.21.0",
|
||||||
|
"zope.sqlalchemy>=1.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
docs = ["Sphinx", "furo"]
|
||||||
|
tests = ["coverage", "mock", "pytest", "pytest-cov"]
|
||||||
|
|
||||||
|
|
||||||
|
[project.entry-points."paste.app_factory"]
|
||||||
|
main = "tailbone.app:main"
|
||||||
|
webapi = "tailbone.webapi:main"
|
||||||
|
|
||||||
|
|
||||||
|
[project.entry-points."rattail.cleaners"]
|
||||||
|
beaker = "tailbone.cleanup:BeakerCleaner"
|
||||||
|
|
||||||
|
|
||||||
|
[project.entry-points."rattail.config.extensions"]
|
||||||
|
tailbone = "tailbone.config:ConfigExtension"
|
||||||
|
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://rattailproject.org"
|
||||||
|
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]
|
||||||
|
version_provider = "pep621"
|
||||||
|
tag_format = "v$version"
|
||||||
|
update_changelog_on_bump = true
|
||||||
|
|
||||||
|
|
||||||
|
[tool.nosetests]
|
||||||
|
nocapture = 1
|
||||||
|
cover-package = "tailbone"
|
||||||
|
cover-erase = 1
|
||||||
|
cover-html = 1
|
||||||
|
cover-html-dir = "htmlcov"
|
112
setup.cfg
112
setup.cfg
|
@ -1,112 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
[nosetests]
|
|
||||||
nocapture = 1
|
|
||||||
cover-package = tailbone
|
|
||||||
cover-erase = 1
|
|
||||||
cover-html = 1
|
|
||||||
cover-html-dir = htmlcov
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
name = Tailbone
|
|
||||||
version = attr: tailbone.__version__
|
|
||||||
author = Lance Edgar
|
|
||||||
author_email = lance@edbob.org
|
|
||||||
url = http://rattailproject.org/
|
|
||||||
license = GNU GPL v3
|
|
||||||
description = Backoffice Web Application for Rattail
|
|
||||||
long_description = file: README.rst
|
|
||||||
classifiers =
|
|
||||||
Development Status :: 4 - Beta
|
|
||||||
Environment :: Web Environment
|
|
||||||
Framework :: Pyramid
|
|
||||||
Intended Audience :: Developers
|
|
||||||
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
||||||
Natural Language :: English
|
|
||||||
Operating System :: OS Independent
|
|
||||||
Programming Language :: Python
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
Programming Language :: Python :: 3.6
|
|
||||||
Programming Language :: Python :: 3.7
|
|
||||||
Programming Language :: Python :: 3.8
|
|
||||||
Programming Language :: Python :: 3.9
|
|
||||||
Programming Language :: Python :: 3.10
|
|
||||||
Programming Language :: Python :: 3.11
|
|
||||||
Topic :: Internet :: WWW/HTTP
|
|
||||||
Topic :: Office/Business
|
|
||||||
Topic :: Software Development :: Libraries :: Python Modules
|
|
||||||
|
|
||||||
|
|
||||||
[options]
|
|
||||||
install_requires =
|
|
||||||
|
|
||||||
# TODO: apparently they jumped from 0.1 to 0.9 and that broke us...
|
|
||||||
# (0.1 was released on 2014-09-14 and then 0.9 came out on 2018-09-27)
|
|
||||||
# (i've cached 0.1 at pypi.rattailproject.org just in case it disappears)
|
|
||||||
# (still, probably a better idea is to refactor so we can use 0.9)
|
|
||||||
webhelpers2_grid==0.1
|
|
||||||
|
|
||||||
# TODO: remove once their bug is fixed? idk what this is about yet...
|
|
||||||
deform<2.0.15
|
|
||||||
|
|
||||||
asgiref
|
|
||||||
colander
|
|
||||||
ColanderAlchemy
|
|
||||||
cornice
|
|
||||||
cornice-swagger
|
|
||||||
humanize
|
|
||||||
Mako
|
|
||||||
markdown
|
|
||||||
openpyxl
|
|
||||||
paginate
|
|
||||||
paginate_sqlalchemy
|
|
||||||
passlib
|
|
||||||
Pillow
|
|
||||||
pyramid
|
|
||||||
pyramid_beaker>=0.6
|
|
||||||
pyramid_deform
|
|
||||||
pyramid_exclog
|
|
||||||
pyramid_mako
|
|
||||||
pyramid_retry
|
|
||||||
pyramid_tm
|
|
||||||
rattail[db,bouncer]
|
|
||||||
six
|
|
||||||
sa-filters
|
|
||||||
simplejson
|
|
||||||
transaction
|
|
||||||
waitress
|
|
||||||
WebHelpers2
|
|
||||||
zope.sqlalchemy
|
|
||||||
|
|
||||||
tests_require = Tailbone[tests]
|
|
||||||
test_suite = nose.collector
|
|
||||||
packages = find:
|
|
||||||
include_package_data = True
|
|
||||||
zip_safe = False
|
|
||||||
|
|
||||||
|
|
||||||
[options.packages.find]
|
|
||||||
exclude =
|
|
||||||
tests.*
|
|
||||||
tests
|
|
||||||
|
|
||||||
|
|
||||||
[options.extras_require]
|
|
||||||
docs = Sphinx; sphinx-rtd-theme
|
|
||||||
tests = coverage; fixture; mock; nose; pytest; pytest-cov
|
|
||||||
|
|
||||||
|
|
||||||
[options.entry_points]
|
|
||||||
|
|
||||||
paste.app_factory =
|
|
||||||
main = tailbone.app:main
|
|
||||||
webapi = tailbone.webapi:main
|
|
||||||
|
|
||||||
rattail.cleaners =
|
|
||||||
beaker = tailbone.cleanup:BeakerCleaner
|
|
||||||
|
|
||||||
rattail.config.extensions =
|
|
||||||
tailbone = tailbone.config:ConfigExtension
|
|
||||||
|
|
||||||
pyramid.scaffold =
|
|
||||||
rattail = tailbone.scaffolds:RattailTemplate
|
|
|
@ -1,3 +1,9 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
__version__ = '0.9.92'
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
except ImportError:
|
||||||
|
from importlib_metadata import version
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = version('Tailbone')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,8 +24,6 @@
|
||||||
Tailbone Web API - Auth Views
|
Tailbone Web API - Auth Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rattail.db.auth import set_user_password
|
|
||||||
|
|
||||||
from cornice import Service
|
from cornice import Service
|
||||||
|
|
||||||
from tailbone.api import APIView, api
|
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
|
This will establish a server-side web session for the user if none
|
||||||
exists. Note that this also resets the user's session timer.
|
exists. Note that this also resets the user's session timer.
|
||||||
"""
|
"""
|
||||||
data = {'ok': True}
|
data = {'ok': True, 'permissions': []}
|
||||||
if self.request.user:
|
if self.request.user:
|
||||||
data['user'] = self.get_user_info(self.request.user)
|
data['user'] = self.get_user_info(self.request.user)
|
||||||
|
data['permissions'] = list(self.request.user_permissions)
|
||||||
data['permissions'] = list(self.request.tailbone_cached_permissions)
|
|
||||||
|
|
||||||
# background color may be set per-request, by some apps
|
# background color may be set per-request, by some apps
|
||||||
if hasattr(self.request, 'background_color') and self.request.background_color:
|
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"}
|
return {'error': "The current/old password you provided is incorrect"}
|
||||||
|
|
||||||
# okay then, set new password
|
# 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 {
|
return {
|
||||||
'ok': True,
|
'ok': True,
|
||||||
'user': self.get_user_info(self.request.user),
|
'user': self.get_user_info(self.request.user),
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,10 +24,6 @@
|
||||||
Tailbone Web API - Label Batches
|
Tailbone Web API - Label Batches
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api.batch import APIBatchView, APIBatchRowView
|
from tailbone.api.batch import APIBatchView, APIBatchRowView
|
||||||
|
@ -56,10 +52,10 @@ class LabelBatchRowViews(APIBatchRowView):
|
||||||
|
|
||||||
def normalize(self, row):
|
def normalize(self, row):
|
||||||
batch = row.batch
|
batch = row.batch
|
||||||
data = super(LabelBatchRowViews, self).normalize(row)
|
data = super().normalize(row)
|
||||||
|
|
||||||
data['item_id'] = row.item_id
|
data['item_id'] = row.item_id
|
||||||
data['upc'] = six.text_type(row.upc)
|
data['upc'] = str(row.upc)
|
||||||
data['upc_pretty'] = row.upc.pretty() if row.upc else None
|
data['upc_pretty'] = row.upc.pretty() if row.upc else None
|
||||||
data['brand_name'] = row.brand_name
|
data['brand_name'] = row.brand_name
|
||||||
data['description'] = row.description
|
data['description'] = row.description
|
||||||
|
|
|
@ -86,6 +86,8 @@ class OrderingBatchViews(APIBatchView):
|
||||||
Sets the mode to "ordering" for the new batch.
|
Sets the mode to "ordering" for the new batch.
|
||||||
"""
|
"""
|
||||||
data = dict(data)
|
data = dict(data)
|
||||||
|
if not data.get('vendor_uuid'):
|
||||||
|
raise ValueError("You must specify the vendor")
|
||||||
data['mode'] = self.enum.PURCHASE_BATCH_MODE_ORDERING
|
data['mode'] = self.enum.PURCHASE_BATCH_MODE_ORDERING
|
||||||
batch = super().create_object(data)
|
batch = super().create_object(data)
|
||||||
return batch
|
return batch
|
||||||
|
|
|
@ -29,8 +29,7 @@ import logging
|
||||||
import humanize
|
import humanize
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
|
||||||
from rattail.util import pretty_quantity
|
|
||||||
|
|
||||||
from cornice import Service
|
from cornice import Service
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
|
@ -45,7 +44,7 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class ReceivingBatchViews(APIBatchView):
|
class ReceivingBatchViews(APIBatchView):
|
||||||
|
|
||||||
model_class = model.PurchaseBatch
|
model_class = PurchaseBatch
|
||||||
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
||||||
route_prefix = 'receivingbatchviews'
|
route_prefix = 'receivingbatchviews'
|
||||||
permission_prefix = 'receiving'
|
permission_prefix = 'receiving'
|
||||||
|
@ -55,7 +54,8 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
supports_execute = True
|
supports_execute = True
|
||||||
|
|
||||||
def base_query(self):
|
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)
|
query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
|
|
||||||
# assume "receive from PO" if given a PO key
|
# assume "receive from PO" if given a PO key
|
||||||
if data.get('purchase_key'):
|
if data.get('purchase_key'):
|
||||||
data['receiving_workflow'] = 'from_po'
|
data['workflow'] = 'from_po'
|
||||||
|
|
||||||
return super().create_object(data)
|
return super().create_object(data)
|
||||||
|
|
||||||
|
@ -120,6 +120,7 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
return self._get(obj=batch)
|
return self._get(obj=batch)
|
||||||
|
|
||||||
def eligible_purchases(self):
|
def eligible_purchases(self):
|
||||||
|
model = self.app.model
|
||||||
uuid = self.request.params.get('vendor_uuid')
|
uuid = self.request.params.get('vendor_uuid')
|
||||||
vendor = self.Session.get(model.Vendor, uuid) if uuid else None
|
vendor = self.Session.get(model.Vendor, uuid) if uuid else None
|
||||||
if not vendor:
|
if not vendor:
|
||||||
|
@ -176,7 +177,7 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
|
|
||||||
class ReceivingBatchRowViews(APIBatchRowView):
|
class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
|
|
||||||
model_class = model.PurchaseBatchRow
|
model_class = PurchaseBatchRow
|
||||||
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
||||||
route_prefix = 'receiving.rows'
|
route_prefix = 'receiving.rows'
|
||||||
permission_prefix = 'receiving'
|
permission_prefix = 'receiving'
|
||||||
|
@ -185,7 +186,8 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
supports_quick_entry = True
|
supports_quick_entry = True
|
||||||
|
|
||||||
def make_filter_spec(self):
|
def make_filter_spec(self):
|
||||||
filters = super(ReceivingBatchRowViews, self).make_filter_spec()
|
model = self.app.model
|
||||||
|
filters = super().make_filter_spec()
|
||||||
if filters:
|
if filters:
|
||||||
|
|
||||||
# must translate certain convenience filters
|
# must translate certain convenience filters
|
||||||
|
@ -296,11 +298,11 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
def normalize(self, row):
|
def normalize(self, row):
|
||||||
data = super(ReceivingBatchRowViews, self).normalize(row)
|
data = super().normalize(row)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
batch = row.batch
|
batch = row.batch
|
||||||
app = self.get_rattail_app()
|
prodder = self.app.get_products_handler()
|
||||||
prodder = app.get_products_handler()
|
|
||||||
|
|
||||||
data['product_uuid'] = row.product_uuid
|
data['product_uuid'] = row.product_uuid
|
||||||
data['item_id'] = row.item_id
|
data['item_id'] = row.item_id
|
||||||
|
@ -375,7 +377,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
if accounted_for:
|
if accounted_for:
|
||||||
# some product accounted for; button should receive "remainder" only
|
# some product accounted for; button should receive "remainder" only
|
||||||
if remainder:
|
if remainder:
|
||||||
remainder = pretty_quantity(remainder)
|
remainder = self.app.render_quantity(remainder)
|
||||||
data['quick_receive_quantity'] = remainder
|
data['quick_receive_quantity'] = remainder
|
||||||
data['quick_receive_text'] = "Receive Remainder ({} {})".format(
|
data['quick_receive_text'] = "Receive Remainder ({} {})".format(
|
||||||
remainder, data['unit_uom'])
|
remainder, data['unit_uom'])
|
||||||
|
@ -386,7 +388,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
else: # nothing yet accounted for, button should receive "all"
|
else: # nothing yet accounted for, button should receive "all"
|
||||||
if not remainder:
|
if not remainder:
|
||||||
log.warning("quick receive remainder is empty for row %s", row.uuid)
|
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_quantity'] = remainder
|
||||||
data['quick_receive_text'] = "Receive ALL ({} {})".format(
|
data['quick_receive_text'] = "Receive ALL ({} {})".format(
|
||||||
remainder, data['unit_uom'])
|
remainder, data['unit_uom'])
|
||||||
|
@ -414,7 +416,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
data['received_alert'] = None
|
data['received_alert'] = None
|
||||||
if self.batch_handler.get_units_confirmed(row):
|
if self.batch_handler.get_units_confirmed(row):
|
||||||
msg = "You have already received some of this product; last update was {}.".format(
|
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
|
data['received_alert'] = msg
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -423,6 +425,8 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
"""
|
"""
|
||||||
View which handles "receiving" against a particular batch row.
|
View which handles "receiving" against a particular batch row.
|
||||||
"""
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
# first do basic input validation
|
# first do basic input validation
|
||||||
schema = ReceiveRow().bind(session=self.Session())
|
schema = ReceiveRow().bind(session=self.Session())
|
||||||
form = forms.Form(schema=schema, request=self.request)
|
form = forms.Form(schema=schema, request=self.request)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -26,15 +26,12 @@ Tailbone Web API - "Common" Views
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import rattail
|
from rattail.util import get_pkg_version
|
||||||
from rattail.db import model
|
|
||||||
from rattail.mail import send_email
|
|
||||||
|
|
||||||
from cornice import Service
|
from cornice import Service
|
||||||
from cornice.service import get_services
|
from cornice.service import get_services
|
||||||
from cornice_swagger import CorniceSwagger
|
from cornice_swagger import CorniceSwagger
|
||||||
|
|
||||||
import tailbone
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.forms.common import Feedback
|
from tailbone.forms.common import Feedback
|
||||||
from tailbone.api import APIView, api
|
from tailbone.api import APIView, api
|
||||||
|
@ -66,11 +63,12 @@ class CommonView(APIView):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_project_title(self):
|
def get_project_title(self):
|
||||||
return self.rattail_config.app_title(default="Tailbone")
|
app = self.get_rattail_app()
|
||||||
|
return app.get_title()
|
||||||
|
|
||||||
def get_project_version(self):
|
def get_project_version(self):
|
||||||
import tailbone
|
app = self.get_rattail_app()
|
||||||
return tailbone.__version__
|
return app.get_version()
|
||||||
|
|
||||||
def get_packages(self):
|
def get_packages(self):
|
||||||
"""
|
"""
|
||||||
|
@ -78,8 +76,8 @@ class CommonView(APIView):
|
||||||
'about' page.
|
'about' page.
|
||||||
"""
|
"""
|
||||||
return OrderedDict([
|
return OrderedDict([
|
||||||
('rattail', rattail.__version__),
|
('rattail', get_pkg_version('rattail')),
|
||||||
('Tailbone', tailbone.__version__),
|
('Tailbone', get_pkg_version('Tailbone')),
|
||||||
])
|
])
|
||||||
|
|
||||||
@api
|
@api
|
||||||
|
@ -87,6 +85,8 @@ class CommonView(APIView):
|
||||||
"""
|
"""
|
||||||
View to handle user feedback form submits.
|
View to handle user feedback form submits.
|
||||||
"""
|
"""
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
model = self.model
|
||||||
# TODO: this logic was copied from tailbone.views.common and is largely
|
# TODO: this logic was copied from tailbone.views.common and is largely
|
||||||
# identical; perhaps should merge somehow?
|
# identical; perhaps should merge somehow?
|
||||||
schema = Feedback().bind(session=Session())
|
schema = Feedback().bind(session=Session())
|
||||||
|
@ -106,7 +106,7 @@ class CommonView(APIView):
|
||||||
|
|
||||||
data['client_ip'] = self.request.client_addr
|
data['client_ip'] = self.request.client_addr
|
||||||
email_key = data['email_key'] or self.feedback_email_key
|
email_key = data['email_key'] or self.feedback_email_key
|
||||||
send_email(self.rattail_config, email_key, data=data)
|
app.send_email(email_key, data=data)
|
||||||
return {'ok': True}
|
return {'ok': True}
|
||||||
|
|
||||||
return {'error': "Form did not validate!"}
|
return {'error': "Form did not validate!"}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -102,7 +102,7 @@ class APIView(View):
|
||||||
auth = app.get_auth_handler()
|
auth = app.get_auth_handler()
|
||||||
|
|
||||||
# basic / default info
|
# basic / default info
|
||||||
is_admin = user.is_admin()
|
is_admin = auth.user_is_admin(user)
|
||||||
employee = app.get_employee(user)
|
employee = app.get_employee(user)
|
||||||
info = {
|
info = {
|
||||||
'uuid': user.uuid,
|
'uuid': user.uuid,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,10 +24,6 @@
|
||||||
Tailbone Web API - Customer Views
|
Tailbone Web API - Customer Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api import APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
@ -46,7 +42,7 @@ class CustomerView(APIMasterView):
|
||||||
def normalize(self, customer):
|
def normalize(self, customer):
|
||||||
return {
|
return {
|
||||||
'uuid': customer.uuid,
|
'uuid': customer.uuid,
|
||||||
'_str': six.text_type(customer),
|
'_str': str(customer),
|
||||||
'id': customer.id,
|
'id': customer.id,
|
||||||
'number': customer.number,
|
'number': customer.number,
|
||||||
'name': customer.name,
|
'name': customer.name,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -26,12 +26,11 @@ Tailbone Web API - Master View
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from rattail.config import parse_bool
|
|
||||||
from rattail.db.util import get_fieldnames
|
from rattail.db.util import get_fieldnames
|
||||||
|
|
||||||
from cornice import resource, Service
|
from cornice import resource, Service
|
||||||
|
|
||||||
from tailbone.api import APIView, api
|
from tailbone.api import APIView
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.util import SortColumn
|
from tailbone.util import SortColumn
|
||||||
|
|
||||||
|
@ -185,7 +184,7 @@ class APIMasterView(APIView):
|
||||||
if sortcol:
|
if sortcol:
|
||||||
spec = {
|
spec = {
|
||||||
'field': sortcol.field_name,
|
'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:
|
if sortcol.model_name:
|
||||||
spec['model'] = sortcol.model_name
|
spec['model'] = sortcol.model_name
|
||||||
|
@ -355,9 +354,13 @@ class APIMasterView(APIView):
|
||||||
data = self.request.json_body
|
data = self.request.json_body
|
||||||
|
|
||||||
# add instance to session, and return data for it
|
# add instance to session, and return data for it
|
||||||
obj = self.create_object(data)
|
try:
|
||||||
self.Session.flush()
|
obj = self.create_object(data)
|
||||||
return self._get(obj)
|
except Exception as error:
|
||||||
|
return self.json_response({'error': str(error)})
|
||||||
|
else:
|
||||||
|
self.Session.flush()
|
||||||
|
return self._get(obj)
|
||||||
|
|
||||||
def create_object(self, data):
|
def create_object(self, data):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,10 +24,6 @@
|
||||||
Tailbone Web API - Person Views
|
Tailbone Web API - Person Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api import APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
@ -45,7 +41,7 @@ class PersonView(APIMasterView):
|
||||||
def normalize(self, person):
|
def normalize(self, person):
|
||||||
return {
|
return {
|
||||||
'uuid': person.uuid,
|
'uuid': person.uuid,
|
||||||
'_str': six.text_type(person),
|
'_str': str(person),
|
||||||
'first_name': person.first_name,
|
'first_name': person.first_name,
|
||||||
'last_name': person.last_name,
|
'last_name': person.last_name,
|
||||||
'display_name': person.display_name,
|
'display_name': person.display_name,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,10 +24,6 @@
|
||||||
Tailbone Web API - Upgrade Views
|
Tailbone Web API - Upgrade Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api import APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
@ -53,7 +49,7 @@ class UpgradeView(APIMasterView):
|
||||||
data['status_code'] = None
|
data['status_code'] = None
|
||||||
else:
|
else:
|
||||||
data['status_code'] = self.enum.UPGRADE_STATUS.get(upgrade.status_code,
|
data['status_code'] = self.enum.UPGRADE_STATUS.get(upgrade.status_code,
|
||||||
six.text_type(upgrade.status_code))
|
str(upgrade.status_code))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,10 +24,6 @@
|
||||||
Tailbone Web API - Vendor Views
|
Tailbone Web API - Vendor Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.api import APIMasterView
|
from tailbone.api import APIMasterView
|
||||||
|
@ -44,7 +40,7 @@ class VendorView(APIMasterView):
|
||||||
def normalize(self, vendor):
|
def normalize(self, vendor):
|
||||||
return {
|
return {
|
||||||
'uuid': vendor.uuid,
|
'uuid': vendor.uuid,
|
||||||
'_str': six.text_type(vendor),
|
'_str': str(vendor),
|
||||||
'id': vendor.id,
|
'id': vendor.id,
|
||||||
'name': vendor.name,
|
'name': vendor.name,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,12 +24,8 @@
|
||||||
Tailbone Web API - Work Order Views
|
Tailbone Web API - Work Order Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from rattail.db.model import WorkOrder
|
from rattail.db.model import WorkOrder
|
||||||
|
|
||||||
from cornice import Service
|
from cornice import Service
|
||||||
|
@ -44,19 +40,19 @@ class WorkOrderView(APIMasterView):
|
||||||
object_url_prefix = '/workorder'
|
object_url_prefix = '/workorder'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(WorkOrderView, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
app = self.get_rattail_app()
|
app = self.get_rattail_app()
|
||||||
self.workorder_handler = app.get_workorder_handler()
|
self.workorder_handler = app.get_workorder_handler()
|
||||||
|
|
||||||
def normalize(self, workorder):
|
def normalize(self, workorder):
|
||||||
data = super(WorkOrderView, self).normalize(workorder)
|
data = super().normalize(workorder)
|
||||||
data.update({
|
data.update({
|
||||||
'customer_name': workorder.customer.name,
|
'customer_name': workorder.customer.name,
|
||||||
'status_label': self.enum.WORKORDER_STATUS[workorder.status_code],
|
'status_label': self.enum.WORKORDER_STATUS[workorder.status_code],
|
||||||
'date_submitted': six.text_type(workorder.date_submitted or ''),
|
'date_submitted': str(workorder.date_submitted or ''),
|
||||||
'date_received': six.text_type(workorder.date_received or ''),
|
'date_received': str(workorder.date_received or ''),
|
||||||
'date_released': six.text_type(workorder.date_released or ''),
|
'date_released': str(workorder.date_released or ''),
|
||||||
'date_delivered': six.text_type(workorder.date_delivered or ''),
|
'date_delivered': str(workorder.date_delivered or ''),
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -87,7 +83,7 @@ class WorkOrderView(APIMasterView):
|
||||||
if 'status_code' in data:
|
if 'status_code' in data:
|
||||||
data['status_code'] = int(data['status_code'])
|
data['status_code'] = int(data['status_code'])
|
||||||
|
|
||||||
return super(WorkOrderView, self).update_object(workorder, data)
|
return super().update_object(workorder, data)
|
||||||
|
|
||||||
def status_codes(self):
|
def status_codes(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -25,21 +25,19 @@ Application Entry Point
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import warnings
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
|
|
||||||
from rattail.config import make_config, parse_list
|
from wuttjamaican.util import parse_list
|
||||||
|
|
||||||
|
from rattail.config import make_config
|
||||||
from rattail.exceptions import ConfigurationError
|
from rattail.exceptions import ConfigurationError
|
||||||
from rattail.db.types import GPCType
|
|
||||||
|
|
||||||
from pyramid.config import Configurator
|
from pyramid.config import Configurator
|
||||||
from pyramid.authentication import SessionAuthenticationPolicy
|
|
||||||
from zope.sqlalchemy import register
|
from zope.sqlalchemy import register
|
||||||
|
|
||||||
import tailbone.db
|
import tailbone.db
|
||||||
from tailbone.auth import TailboneAuthorizationPolicy
|
from tailbone.auth import TailboneSecurityPolicy
|
||||||
from tailbone.config import csrf_token_name, csrf_header_name
|
from tailbone.config import csrf_token_name, csrf_header_name
|
||||||
from tailbone.util import get_effective_theme, get_theme_template_path
|
from tailbone.util import get_effective_theme, get_theme_template_path
|
||||||
from tailbone.providers import get_all_providers
|
from tailbone.providers import get_all_providers
|
||||||
|
@ -61,9 +59,23 @@ def make_rattail_config(settings):
|
||||||
rattail_config = make_config(path)
|
rattail_config = make_config(path)
|
||||||
settings['rattail_config'] = rattail_config
|
settings['rattail_config'] = rattail_config
|
||||||
|
|
||||||
|
# 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
|
# configure database sessions
|
||||||
if hasattr(rattail_config, 'rattail_engine'):
|
if hasattr(rattail_config, 'appdb_engine'):
|
||||||
tailbone.db.Session.configure(bind=rattail_config.rattail_engine)
|
tailbone.db.Session.configure(bind=rattail_config.appdb_engine)
|
||||||
if hasattr(rattail_config, 'trainwreck_engine'):
|
if hasattr(rattail_config, 'trainwreck_engine'):
|
||||||
tailbone.db.TrainwreckSession.configure(bind=rattail_config.trainwreck_engine)
|
tailbone.db.TrainwreckSession.configure(bind=rattail_config.trainwreck_engine)
|
||||||
if hasattr(rattail_config, 'tempmon_engine'):
|
if hasattr(rattail_config, 'tempmon_engine'):
|
||||||
|
@ -123,24 +135,21 @@ def make_pyramid_config(settings, configure_csrf=True):
|
||||||
config.set_root_factory(Root)
|
config.set_root_factory(Root)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
|
# declare this web app of the "classic" variety
|
||||||
|
settings.setdefault('tailbone.classic', 'true')
|
||||||
|
|
||||||
# we want the new themes feature!
|
# we want the new themes feature!
|
||||||
establish_theme(settings)
|
establish_theme(settings)
|
||||||
|
|
||||||
|
settings.setdefault('fanstatic.versioning', 'true')
|
||||||
settings.setdefault('pyramid_deform.template_search_path', 'tailbone:templates/deform')
|
settings.setdefault('pyramid_deform.template_search_path', 'tailbone:templates/deform')
|
||||||
config = Configurator(settings=settings, root_factory=Root)
|
config = Configurator(settings=settings, root_factory=Root)
|
||||||
|
|
||||||
# add rattail config directly to registry
|
# add rattail config directly to registry, for access throughout the app
|
||||||
config.registry['rattail_config'] = rattail_config
|
config.registry['rattail_config'] = rattail_config
|
||||||
|
|
||||||
# configure user authorization / authentication
|
# configure user authorization / authentication
|
||||||
# TODO: security policy should become the default, for pyramid 2.x
|
config.set_security_policy(TailboneSecurityPolicy())
|
||||||
if rattail_config.getbool('tailbone', 'pyramid.use_security_policy',
|
|
||||||
usedb=False, default=False):
|
|
||||||
from tailbone.auth import TailboneSecurityPolicy
|
|
||||||
config.set_security_policy(TailboneSecurityPolicy())
|
|
||||||
else:
|
|
||||||
config.set_authorization_policy(TailboneAuthorizationPolicy())
|
|
||||||
config.set_authentication_policy(SessionAuthenticationPolicy())
|
|
||||||
|
|
||||||
# maybe require CSRF token protection
|
# maybe require CSRF token protection
|
||||||
if configure_csrf:
|
if configure_csrf:
|
||||||
|
@ -151,6 +160,7 @@ def make_pyramid_config(settings, configure_csrf=True):
|
||||||
# Bring in some Pyramid goodies.
|
# Bring in some Pyramid goodies.
|
||||||
config.include('tailbone.beaker')
|
config.include('tailbone.beaker')
|
||||||
config.include('pyramid_deform')
|
config.include('pyramid_deform')
|
||||||
|
config.include('pyramid_fanstatic')
|
||||||
config.include('pyramid_mako')
|
config.include('pyramid_mako')
|
||||||
config.include('pyramid_tm')
|
config.include('pyramid_tm')
|
||||||
|
|
||||||
|
@ -186,9 +196,16 @@ def make_pyramid_config(settings, configure_csrf=True):
|
||||||
for spec in includes:
|
for spec in includes:
|
||||||
config.include(spec)
|
config.include(spec)
|
||||||
|
|
||||||
# Add some permissions magic.
|
# add some permissions magic
|
||||||
config.add_directive('add_tailbone_permission_group', 'tailbone.auth.add_permission_group')
|
config.add_directive('add_wutta_permission_group',
|
||||||
config.add_directive('add_tailbone_permission', 'tailbone.auth.add_permission')
|
'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
|
# and some similar magic for certain master views
|
||||||
config.add_directive('add_tailbone_index_page', 'tailbone.app.add_index_page')
|
config.add_directive('add_tailbone_index_page', 'tailbone.app.add_index_page')
|
||||||
|
@ -315,7 +332,8 @@ def main(global_config, **settings):
|
||||||
"""
|
"""
|
||||||
This function returns a Pyramid WSGI application.
|
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)
|
rattail_config = make_rattail_config(settings)
|
||||||
pyramid_config = make_pyramid_config(settings)
|
pyramid_config = make_pyramid_config(settings)
|
||||||
pyramid_config.include('tailbone')
|
pyramid_config.include('tailbone')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,14 +24,10 @@
|
||||||
ASGI App Utilities
|
ASGI App Utilities
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import configparser
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import six
|
|
||||||
from six.moves import configparser
|
|
||||||
|
|
||||||
from rattail.util import load_object
|
from rattail.util import load_object
|
||||||
|
|
||||||
from asgiref.wsgi import WsgiToAsgi
|
from asgiref.wsgi import WsgiToAsgi
|
||||||
|
@ -49,6 +45,12 @@ class TailboneWsgiToAsgi(WsgiToAsgi):
|
||||||
protocol = scope['type']
|
protocol = scope['type']
|
||||||
path = scope['path']
|
path = scope['path']
|
||||||
|
|
||||||
|
# strip off the root path, if non-empty. needed for serving
|
||||||
|
# under /poser or anything other than true site root
|
||||||
|
root_path = scope['root_path']
|
||||||
|
if root_path and path.startswith(root_path):
|
||||||
|
path = path[len(root_path):]
|
||||||
|
|
||||||
if protocol == 'websocket':
|
if protocol == 'websocket':
|
||||||
websockets = self.wsgi_application.registry.get(
|
websockets = self.wsgi_application.registry.get(
|
||||||
'tailbone_websockets', {})
|
'tailbone_websockets', {})
|
||||||
|
@ -85,7 +87,7 @@ def make_asgi_app(main_app=None):
|
||||||
# parse the settings needed for pyramid app
|
# parse the settings needed for pyramid app
|
||||||
settings = dict(parser.items('app:main'))
|
settings = dict(parser.items('app:main'))
|
||||||
|
|
||||||
if isinstance(main_app, six.string_types):
|
if isinstance(main_app, str):
|
||||||
make_wsgi_app = load_object(main_app)
|
make_wsgi_app = load_object(main_app)
|
||||||
elif callable(main_app):
|
elif callable(main_app):
|
||||||
make_wsgi_app = main_app
|
make_wsgi_app = main_app
|
||||||
|
|
153
tailbone/auth.py
153
tailbone/auth.py
|
@ -27,29 +27,28 @@ Authentication & Authorization
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from rattail.util import prettify, NOTSET
|
from wuttjamaican.util import UNSPECIFIED
|
||||||
|
|
||||||
from zope.interface import implementer
|
from pyramid.security import remember, forget
|
||||||
from pyramid.interfaces import IAuthorizationPolicy
|
|
||||||
from pyramid.security import remember, forget, Everyone, Authenticated
|
|
||||||
from pyramid.authentication import SessionAuthenticationPolicy
|
|
||||||
|
|
||||||
|
from wuttaweb.auth import WuttaSecurityPolicy
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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
|
Perform the steps necessary to login the given user. Note that this
|
||||||
returns a ``headers`` dict which you should pass to the redirect.
|
returns a ``headers`` dict which you should pass to the redirect.
|
||||||
"""
|
"""
|
||||||
app = request.rattail_config.get_app()
|
config = request.rattail_config
|
||||||
|
app = config.get_app()
|
||||||
user.record_event(app.enum.USER_EVENT_LOGIN)
|
user.record_event(app.enum.USER_EVENT_LOGIN)
|
||||||
headers = remember(request, user.uuid)
|
headers = remember(request, user.uuid)
|
||||||
if timeout is NOTSET:
|
if timeout is UNSPECIFIED:
|
||||||
timeout = session_timeout_for_user(user)
|
timeout = session_timeout_for_user(config, user)
|
||||||
log.debug("setting session timeout for '{}' to {}".format(user.username, timeout))
|
log.debug("setting session timeout for '{}' to {}".format(user.username, timeout))
|
||||||
set_session_timeout(request, timeout)
|
set_session_timeout(request, timeout)
|
||||||
return headers
|
return headers
|
||||||
|
@ -70,15 +69,18 @@ def logout_user(request):
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
def session_timeout_for_user(user):
|
def session_timeout_for_user(config, user):
|
||||||
"""
|
"""
|
||||||
Returns the "max" session timeout for the user, according to roles
|
Returns the "max" session timeout for the user, according to roles
|
||||||
"""
|
"""
|
||||||
from rattail.db.auth import authenticated_role
|
app = config.get_app()
|
||||||
|
auth = app.get_auth_handler()
|
||||||
|
|
||||||
roles = user.roles + [authenticated_role(Session())]
|
authenticated = auth.get_role_authenticated(Session())
|
||||||
|
roles = user.roles + [authenticated]
|
||||||
timeouts = [role.session_timeout for role in roles
|
timeouts = [role.session_timeout for role in roles
|
||||||
if role.session_timeout is not None]
|
if role.session_timeout is not None]
|
||||||
|
|
||||||
if timeouts and 0 not in timeouts:
|
if timeouts and 0 not in timeouts:
|
||||||
return max(timeouts)
|
return max(timeouts)
|
||||||
|
|
||||||
|
@ -90,76 +92,12 @@ def set_session_timeout(request, timeout):
|
||||||
request.session['_timeout'] = timeout or None
|
request.session['_timeout'] = timeout or None
|
||||||
|
|
||||||
|
|
||||||
class TailboneAuthenticationPolicy(SessionAuthenticationPolicy):
|
class TailboneSecurityPolicy(WuttaSecurityPolicy):
|
||||||
"""
|
|
||||||
Custom authentication policy for Tailbone.
|
|
||||||
|
|
||||||
This is mostly Pyramid's built-in session-based policy, but adds
|
|
||||||
logic to accept Rattail User API Tokens in lieu of current user
|
|
||||||
being identified via the session.
|
|
||||||
|
|
||||||
Note that the traditional Tailbone web app does *not* use this
|
|
||||||
policy, only the Tailbone web API uses it by default.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def unauthenticated_userid(self, request):
|
|
||||||
|
|
||||||
# figure out userid from header token if present
|
|
||||||
credentials = request.headers.get('Authorization')
|
|
||||||
if credentials:
|
|
||||||
match = re.match(r'^Bearer (\S+)$', credentials)
|
|
||||||
if match:
|
|
||||||
token = match.group(1)
|
|
||||||
rattail_config = request.registry.settings.get('rattail_config')
|
|
||||||
app = rattail_config.get_app()
|
|
||||||
auth = app.get_auth_handler()
|
|
||||||
user = auth.authenticate_user_token(Session(), token)
|
|
||||||
if user:
|
|
||||||
return user.uuid
|
|
||||||
|
|
||||||
# otherwise do normal session-based logic
|
|
||||||
return super().unauthenticated_userid(request)
|
|
||||||
|
|
||||||
|
|
||||||
@implementer(IAuthorizationPolicy)
|
|
||||||
class TailboneAuthorizationPolicy(object):
|
|
||||||
|
|
||||||
def permits(self, context, principals, permission):
|
|
||||||
config = context.request.rattail_config
|
|
||||||
model = config.get_model()
|
|
||||||
app = config.get_app()
|
|
||||||
auth = app.get_auth_handler()
|
|
||||||
|
|
||||||
for userid in principals:
|
|
||||||
if userid not in (Everyone, Authenticated):
|
|
||||||
if context.request.user and context.request.user.uuid == userid:
|
|
||||||
return context.request.has_perm(permission)
|
|
||||||
else:
|
|
||||||
# this is pretty rare, but can happen in dev after
|
|
||||||
# re-creating the database, which means new user uuids.
|
|
||||||
# TODO: the odds of this query returning a user in that
|
|
||||||
# case, are probably nil, and we should just skip this bit?
|
|
||||||
user = Session.get(model.User, userid)
|
|
||||||
if user:
|
|
||||||
if auth.has_permission(Session(), user, permission):
|
|
||||||
return True
|
|
||||||
if Everyone in principals:
|
|
||||||
return auth.has_permission(Session(), None, permission)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def principals_allowed_by_permission(self, context, permission):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class TailboneSecurityPolicy:
|
|
||||||
|
|
||||||
def __init__(self, api_mode=False):
|
|
||||||
from pyramid.authentication import SessionAuthenticationHelper
|
|
||||||
from pyramid.request import RequestLocalCache
|
|
||||||
|
|
||||||
|
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.api_mode = api_mode
|
||||||
self.session_helper = SessionAuthenticationHelper()
|
|
||||||
self.identity_cache = RequestLocalCache(self.load_identity)
|
|
||||||
|
|
||||||
def load_identity(self, request):
|
def load_identity(self, request):
|
||||||
config = request.registry.settings.get('rattail_config')
|
config = request.registry.settings.get('rattail_config')
|
||||||
|
@ -175,7 +113,7 @@ class TailboneSecurityPolicy:
|
||||||
if match:
|
if match:
|
||||||
token = match.group(1)
|
token = match.group(1)
|
||||||
auth = app.get_auth_handler()
|
auth = app.get_auth_handler()
|
||||||
user = auth.authenticate_user_token(Session(), token)
|
user = auth.authenticate_user_token(self.db_session, token)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
|
|
||||||
|
@ -186,59 +124,10 @@ class TailboneSecurityPolicy:
|
||||||
|
|
||||||
# fetch user object from db
|
# fetch user object from db
|
||||||
model = app.model
|
model = app.model
|
||||||
user = Session.get(model.User, uuid)
|
user = self.db_session.get(model.User, uuid)
|
||||||
if not user:
|
if not user:
|
||||||
return
|
return
|
||||||
|
|
||||||
# this user is responsible for data changes in current request
|
# this user is responsible for data changes in current request
|
||||||
Session().set_continuum_user(user)
|
self.db_session.set_continuum_user(user)
|
||||||
return 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):
|
|
||||||
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)
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -27,11 +27,11 @@ Note that most of the code for this module was copied from the beaker and
|
||||||
pyramid_beaker projects.
|
pyramid_beaker projects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from pkg_resources import parse_version
|
from pkg_resources import parse_version
|
||||||
|
|
||||||
|
from rattail.util import get_pkg_version
|
||||||
|
|
||||||
import beaker
|
import beaker
|
||||||
from beaker.session import Session
|
from beaker.session import Session
|
||||||
from beaker.util import coerce_session_params
|
from beaker.util import coerce_session_params
|
||||||
|
@ -49,7 +49,7 @@ class TailboneSession(Session):
|
||||||
"Loads the data from this session from persistent storage"
|
"Loads the data from this session from persistent storage"
|
||||||
|
|
||||||
# are we using older version of beaker?
|
# are we using older version of beaker?
|
||||||
old_beaker = parse_version(beaker.__version__) < parse_version('1.12')
|
old_beaker = parse_version(get_pkg_version('beaker')) < parse_version('1.12')
|
||||||
|
|
||||||
self.namespace = self.namespace_class(self.id,
|
self.namespace = self.namespace_class(self.id,
|
||||||
data_dir=self.data_dir,
|
data_dir=self.data_dir,
|
||||||
|
|
|
@ -26,13 +26,14 @@ Rattail config extension for Tailbone
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from rattail.config import ConfigExtension as BaseExtension
|
from wuttjamaican.conf import WuttaConfigExtension
|
||||||
|
|
||||||
from rattail.db.config import configure_session
|
from rattail.db.config import configure_session
|
||||||
|
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
|
|
||||||
|
|
||||||
class ConfigExtension(BaseExtension):
|
class ConfigExtension(WuttaConfigExtension):
|
||||||
"""
|
"""
|
||||||
Rattail config extension for Tailbone. Does the following:
|
Rattail config extension for Tailbone. Does the following:
|
||||||
|
|
||||||
|
@ -49,9 +50,12 @@ class ConfigExtension(BaseExtension):
|
||||||
configure_session(config, Session)
|
configure_session(config, Session)
|
||||||
|
|
||||||
# provide default theme selection
|
# provide default theme selection
|
||||||
config.setdefault('tailbone', 'themes.keys', 'default, falafel')
|
config.setdefault('tailbone', 'themes.keys', 'default, butterball')
|
||||||
config.setdefault('tailbone', 'themes.expose_picker', 'true')
|
config.setdefault('tailbone', 'themes.expose_picker', 'true')
|
||||||
|
|
||||||
|
# override oruga detection
|
||||||
|
config.setdefault('wuttaweb.oruga_detector.spec', 'tailbone.util:should_use_oruga')
|
||||||
|
|
||||||
|
|
||||||
def csrf_token_name(config):
|
def csrf_token_name(config):
|
||||||
return config.get('tailbone', 'csrf_token_name', default='_csrf')
|
return config.get('tailbone', 'csrf_token_name', default='_csrf')
|
||||||
|
|
231
tailbone/db.py
231
tailbone/db.py
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -21,14 +21,13 @@
|
||||||
#
|
#
|
||||||
################################################################################
|
################################################################################
|
||||||
"""
|
"""
|
||||||
Database Stuff
|
Database sessions etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from zope.sqlalchemy import datamanager
|
from zope.sqlalchemy import datamanager
|
||||||
import sqlalchemy_continuum as continuum
|
import sqlalchemy_continuum as continuum
|
||||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
from pkg_resources import get_distribution, parse_version
|
|
||||||
|
|
||||||
from rattail.db import SessionBase
|
from rattail.db import SessionBase
|
||||||
from rattail.db.continuum import versioning_manager
|
from rattail.db.continuum import versioning_manager
|
||||||
|
@ -43,23 +42,28 @@ TrainwreckSession = scoped_session(sessionmaker())
|
||||||
# empty dict for now, this must populated on app startup (if needed)
|
# empty dict for now, this must populated on app startup (if needed)
|
||||||
ExtraTrainwreckSessions = {}
|
ExtraTrainwreckSessions = {}
|
||||||
|
|
||||||
# some of the logic below may need to vary somewhat, based on which version of
|
|
||||||
# zope.sqlalchemy we have installed
|
|
||||||
zope_sqlalchemy_version = get_distribution('zope.sqlalchemy').version
|
|
||||||
zope_sqlalchemy_version_parsed = parse_version(zope_sqlalchemy_version)
|
|
||||||
|
|
||||||
|
|
||||||
class TailboneSessionDataManager(datamanager.SessionDataManager):
|
class TailboneSessionDataManager(datamanager.SessionDataManager):
|
||||||
"""Integrate a top level sqlalchemy session transaction into a zope transaction
|
"""
|
||||||
|
Integrate a top level sqlalchemy session transaction into a zope
|
||||||
|
transaction
|
||||||
|
|
||||||
One phase variant.
|
One phase variant.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This class appears to be necessary in order for the Continuum
|
|
||||||
integration to work alongside the Zope transaction integration.
|
This class appears to be necessary in order for the
|
||||||
|
SQLAlchemy-Continuum integration to work alongside the Zope
|
||||||
|
transaction integration.
|
||||||
|
|
||||||
|
It subclasses
|
||||||
|
``zope.sqlalchemy.datamanager.SessionDataManager`` but injects
|
||||||
|
some SQLAlchemy-Continuum logic within :meth:`tpc_vote()`, and
|
||||||
|
is sort of monkey-patched into the mix.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def tpc_vote(self, trans):
|
def tpc_vote(self, trans):
|
||||||
|
""" """
|
||||||
# for a one phase data manager commit last in tpc_vote
|
# for a one phase data manager commit last in tpc_vote
|
||||||
if self.tx is not None: # there may have been no work to do
|
if self.tx is not None: # there may have been no work to do
|
||||||
|
|
||||||
|
@ -71,126 +75,120 @@ class TailboneSessionDataManager(datamanager.SessionDataManager):
|
||||||
self._finish('committed')
|
self._finish('committed')
|
||||||
|
|
||||||
|
|
||||||
def join_transaction(session, initial_state=datamanager.STATUS_ACTIVE, transaction_manager=datamanager.zope_transaction.manager, keep_session=False):
|
def join_transaction(
|
||||||
"""Join a session to a transaction using the appropriate datamanager.
|
session,
|
||||||
|
initial_state=datamanager.STATUS_ACTIVE,
|
||||||
|
transaction_manager=datamanager.zope_transaction.manager,
|
||||||
|
keep_session=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Join a session to a transaction using the appropriate datamanager.
|
||||||
|
|
||||||
It is safe to call this multiple times, if the session is already joined
|
It is safe to call this multiple times, if the session is already
|
||||||
then it just returns.
|
joined then it just returns.
|
||||||
|
|
||||||
`initial_state` is either STATUS_ACTIVE, STATUS_INVALIDATED or STATUS_READONLY
|
`initial_state` is either STATUS_ACTIVE, STATUS_INVALIDATED or
|
||||||
|
STATUS_READONLY
|
||||||
|
|
||||||
If using the default initial status of STATUS_ACTIVE, you must ensure that
|
If using the default initial status of STATUS_ACTIVE, you must
|
||||||
mark_changed(session) is called when data is written to the database.
|
ensure that mark_changed(session) is called when data is written
|
||||||
|
to the database.
|
||||||
|
|
||||||
The ZopeTransactionExtesion SessionExtension can be used to ensure that this is
|
The ZopeTransactionExtesion SessionExtension can be used to ensure
|
||||||
called automatically after session write operations.
|
that this is called automatically after session write operations.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This function is copied from upstream, and tweaked so that our custom
|
|
||||||
:class:`TailboneSessionDataManager` will be used.
|
This function appears to be necessary in order for the
|
||||||
|
SQLAlchemy-Continuum integration to work alongside the Zope
|
||||||
|
transaction integration.
|
||||||
|
|
||||||
|
It overrides ``zope.sqlalchemy.datamanager.join_transaction()``
|
||||||
|
to ensure the custom :class:`TailboneSessionDataManager` is
|
||||||
|
used, and is sort of monkey-patched into the mix.
|
||||||
"""
|
"""
|
||||||
# the upstream internals of this function has changed a little over time.
|
# the upstream internals of this function has changed a little over time.
|
||||||
# unfortunately for us, that means we must include each variant here.
|
# unfortunately for us, that means we must include each variant here.
|
||||||
|
|
||||||
if zope_sqlalchemy_version_parsed >= parse_version('1.1'): # 1.1+
|
if datamanager._SESSION_STATE.get(session, None) is None:
|
||||||
if datamanager._SESSION_STATE.get(session, None) is None:
|
if session.twophase:
|
||||||
if session.twophase:
|
DataManager = datamanager.TwoPhaseSessionDataManager
|
||||||
DataManager = datamanager.TwoPhaseSessionDataManager
|
else:
|
||||||
else:
|
DataManager = TailboneSessionDataManager
|
||||||
DataManager = TailboneSessionDataManager
|
DataManager(session, initial_state, transaction_manager, keep_session=keep_session)
|
||||||
DataManager(session, initial_state, transaction_manager, keep_session=keep_session)
|
|
||||||
|
|
||||||
else: # pre-1.1
|
|
||||||
if datamanager._SESSION_STATE.get(id(session), None) is None:
|
|
||||||
if session.twophase:
|
|
||||||
DataManager = datamanager.TwoPhaseSessionDataManager
|
|
||||||
else:
|
|
||||||
DataManager = TailboneSessionDataManager
|
|
||||||
DataManager(session, initial_state, transaction_manager, keep_session=keep_session)
|
|
||||||
|
|
||||||
|
|
||||||
if zope_sqlalchemy_version_parsed >= parse_version('1.2'): # 1.2+
|
class ZopeTransactionEvents(datamanager.ZopeTransactionEvents):
|
||||||
|
"""
|
||||||
class ZopeTransactionEvents(datamanager.ZopeTransactionEvents):
|
Record that a flush has occurred on a session's connection. This
|
||||||
"""
|
allows the DataManager to rollback rather than commit on read only
|
||||||
Record that a flush has occurred on a session's
|
transactions.
|
||||||
connection. This allows the DataManager to rollback rather
|
|
||||||
than commit on read only transactions.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
This class is copied from upstream, and tweaked so that our
|
|
||||||
custom :func:`join_transaction()` will be used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def after_begin(self, session, transaction, connection):
|
|
||||||
join_transaction(session, self.initial_state,
|
|
||||||
self.transaction_manager, self.keep_session)
|
|
||||||
|
|
||||||
def after_attach(self, session, instance):
|
|
||||||
join_transaction(session, self.initial_state,
|
|
||||||
self.transaction_manager, self.keep_session)
|
|
||||||
|
|
||||||
def join_transaction(self, session):
|
|
||||||
join_transaction(session, self.initial_state,
|
|
||||||
self.transaction_manager, self.keep_session)
|
|
||||||
|
|
||||||
else: # pre-1.2
|
|
||||||
|
|
||||||
class ZopeTransactionExtension(datamanager.ZopeTransactionExtension):
|
|
||||||
"""
|
|
||||||
Record that a flush has occurred on a session's
|
|
||||||
connection. This allows the DataManager to rollback rather
|
|
||||||
than commit on read only transactions.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
This class is copied from upstream, and tweaked so that our
|
|
||||||
custom :func:`join_transaction()` will be used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def after_begin(self, session, transaction, connection):
|
|
||||||
join_transaction(session, self.initial_state,
|
|
||||||
self.transaction_manager, self.keep_session)
|
|
||||||
|
|
||||||
def after_attach(self, session, instance):
|
|
||||||
join_transaction(session, self.initial_state,
|
|
||||||
self.transaction_manager, self.keep_session)
|
|
||||||
|
|
||||||
|
|
||||||
def register(session, initial_state=datamanager.STATUS_ACTIVE,
|
|
||||||
transaction_manager=datamanager.zope_transaction.manager, keep_session=False):
|
|
||||||
"""Register ZopeTransaction listener events on the
|
|
||||||
given Session or Session factory/class.
|
|
||||||
|
|
||||||
This function requires at least SQLAlchemy 0.7 and makes use
|
|
||||||
of the newer sqlalchemy.event package in order to register event listeners
|
|
||||||
on the given Session.
|
|
||||||
|
|
||||||
The session argument here may be a Session class or subclass, a
|
|
||||||
sessionmaker or scoped_session instance, or a specific Session instance.
|
|
||||||
Event listening will be specific to the scope of the type of argument
|
|
||||||
passed, including specificity to its subclass as well as its identity.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This function is copied from upstream, and tweaked so that our custom
|
|
||||||
:class:`ZopeTransactionExtension` will be used.
|
This class appears to be necessary in order for the
|
||||||
|
SQLAlchemy-Continuum integration to work alongside the Zope
|
||||||
|
transaction integration.
|
||||||
|
|
||||||
|
It subclasses
|
||||||
|
``zope.sqlalchemy.datamanager.ZopeTransactionEvents`` but
|
||||||
|
overrides various methods to ensure the custom
|
||||||
|
:func:`join_transaction()` is called, and is sort of
|
||||||
|
monkey-patched into the mix.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def after_begin(self, session, transaction, connection):
|
||||||
|
""" """
|
||||||
|
join_transaction(session, self.initial_state,
|
||||||
|
self.transaction_manager, self.keep_session)
|
||||||
|
|
||||||
|
def after_attach(self, session, instance):
|
||||||
|
""" """
|
||||||
|
join_transaction(session, self.initial_state,
|
||||||
|
self.transaction_manager, self.keep_session)
|
||||||
|
|
||||||
|
def join_transaction(self, session):
|
||||||
|
""" """
|
||||||
|
join_transaction(session, self.initial_state,
|
||||||
|
self.transaction_manager, self.keep_session)
|
||||||
|
|
||||||
|
|
||||||
|
def register(
|
||||||
|
session,
|
||||||
|
initial_state=datamanager.STATUS_ACTIVE,
|
||||||
|
transaction_manager=datamanager.zope_transaction.manager,
|
||||||
|
keep_session=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Register ZopeTransaction listener events on the given Session or
|
||||||
|
Session factory/class.
|
||||||
|
|
||||||
|
This function requires at least SQLAlchemy 0.7 and makes use of
|
||||||
|
the newer sqlalchemy.event package in order to register event
|
||||||
|
listeners on the given Session.
|
||||||
|
|
||||||
|
The session argument here may be a Session class or subclass, a
|
||||||
|
sessionmaker or scoped_session instance, or a specific Session
|
||||||
|
instance. Event listening will be specific to the scope of the
|
||||||
|
type of argument passed, including specificity to its subclass as
|
||||||
|
well as its identity.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This function appears to be necessary in order for the
|
||||||
|
SQLAlchemy-Continuum integration to work alongside the Zope
|
||||||
|
transaction integration.
|
||||||
|
|
||||||
|
It overrides ``zope.sqlalchemy.datamanager.regsiter()`` to
|
||||||
|
ensure the custom :class:`ZopeTransactionEvents` is used.
|
||||||
"""
|
"""
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event
|
||||||
|
|
||||||
if zope_sqlalchemy_version_parsed >= parse_version('1.2'): # 1.2+
|
ext = ZopeTransactionEvents(
|
||||||
|
initial_state=initial_state,
|
||||||
ext = ZopeTransactionEvents(
|
transaction_manager=transaction_manager,
|
||||||
initial_state=initial_state,
|
keep_session=keep_session,
|
||||||
transaction_manager=transaction_manager,
|
)
|
||||||
keep_session=keep_session,
|
|
||||||
)
|
|
||||||
|
|
||||||
else: # pre-1.2
|
|
||||||
|
|
||||||
ext = ZopeTransactionExtension(
|
|
||||||
initial_state=initial_state,
|
|
||||||
transaction_manager=transaction_manager,
|
|
||||||
keep_session=keep_session,
|
|
||||||
)
|
|
||||||
|
|
||||||
event.listen(session, "after_begin", ext.after_begin)
|
event.listen(session, "after_begin", ext.after_begin)
|
||||||
event.listen(session, "after_attach", ext.after_attach)
|
event.listen(session, "after_attach", ext.after_attach)
|
||||||
|
@ -199,9 +197,8 @@ def register(session, initial_state=datamanager.STATUS_ACTIVE,
|
||||||
event.listen(session, "after_bulk_delete", ext.after_bulk_delete)
|
event.listen(session, "after_bulk_delete", ext.after_bulk_delete)
|
||||||
event.listen(session, "before_commit", ext.before_commit)
|
event.listen(session, "before_commit", ext.before_commit)
|
||||||
|
|
||||||
if zope_sqlalchemy_version_parsed >= parse_version('1.5'): # 1.5+
|
if datamanager.SA_GE_14:
|
||||||
if datamanager.SA_GE_14:
|
event.listen(session, "do_orm_execute", ext.do_orm_execute)
|
||||||
event.listen(session, "do_orm_execute", ext.do_orm_execute)
|
|
||||||
|
|
||||||
|
|
||||||
register(Session)
|
register(Session)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -270,9 +270,21 @@ class VersionDiff(Diff):
|
||||||
for field in self.fields:
|
for field in self.fields:
|
||||||
values[field] = {'before': self.render_old_value(field),
|
values[field] = {'before': self.render_old_value(field),
|
||||||
'after': self.render_new_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 {
|
return {
|
||||||
'key': id(self.version),
|
'key': id(self.version),
|
||||||
'model_title': self.title,
|
'model_title': self.title,
|
||||||
|
'operation': operation,
|
||||||
'diff_class': self.nature,
|
'diff_class': self.nature,
|
||||||
'fields': self.fields,
|
'fields': self.fields,
|
||||||
'values': values,
|
'values': values,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2020 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,10 +24,6 @@
|
||||||
Tailbone Exceptions
|
Tailbone Exceptions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from rattail.exceptions import RattailError
|
from rattail.exceptions import RattailError
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,7 +33,6 @@ class TailboneError(RattailError):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@six.python_2_unicode_compatible
|
|
||||||
class TailboneJSONFieldError(TailboneError):
|
class TailboneJSONFieldError(TailboneError):
|
||||||
"""
|
"""
|
||||||
Error raised when JSON serialization of a form field results in an error.
|
Error raised when JSON serialization of a form field results in an error.
|
||||||
|
|
|
@ -35,7 +35,7 @@ from sqlalchemy import orm
|
||||||
from sqlalchemy.ext.associationproxy import AssociationProxy, ASSOCIATION_PROXY
|
from sqlalchemy.ext.associationproxy import AssociationProxy, ASSOCIATION_PROXY
|
||||||
from wuttjamaican.util import UNSPECIFIED
|
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
|
from rattail.db.util import get_fieldnames
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
|
@ -47,12 +47,14 @@ from pyramid_deform import SessionFileUploadTempStore
|
||||||
from pyramid.renderers import render
|
from pyramid.renderers import render
|
||||||
from webhelpers2.html import tags, HTML
|
from webhelpers2.html import tags, HTML
|
||||||
|
|
||||||
|
from wuttaweb.util import FieldList, get_form_data, make_json_safe
|
||||||
|
|
||||||
from tailbone.db import Session
|
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 import types
|
||||||
from tailbone.forms.widgets import (ReadonlyWidget, PlainDateWidget,
|
from tailbone.forms.widgets import (ReadonlyWidget, PlainDateWidget,
|
||||||
JQueryDateWidget, JQueryTimeWidget,
|
JQueryDateWidget, JQueryTimeWidget,
|
||||||
MultiFileUploadWidget)
|
FileUploadWidget, MultiFileUploadWidget)
|
||||||
from tailbone.exceptions import TailboneJSONFieldError
|
from tailbone.exceptions import TailboneJSONFieldError
|
||||||
|
|
||||||
|
|
||||||
|
@ -326,7 +328,7 @@ class Form(object):
|
||||||
"""
|
"""
|
||||||
Base class for all forms.
|
Base class for all forms.
|
||||||
"""
|
"""
|
||||||
save_label = "Save"
|
save_label = "Submit"
|
||||||
update_label = "Save"
|
update_label = "Save"
|
||||||
show_cancel = True
|
show_cancel = True
|
||||||
auto_disable = True
|
auto_disable = True
|
||||||
|
@ -337,10 +339,12 @@ class Form(object):
|
||||||
model_instance=None, model_class=None, appstruct=UNSPECIFIED, nodes={}, enums={}, labels={},
|
model_instance=None, model_class=None, appstruct=UNSPECIFIED, nodes={}, enums={}, labels={},
|
||||||
assume_local_times=False, renderers=None, renderer_kwargs={},
|
assume_local_times=False, renderers=None, renderer_kwargs={},
|
||||||
hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None,
|
hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None,
|
||||||
action_url=None, cancel_url=None, component='tailbone-form',
|
action_url=None, cancel_url=None,
|
||||||
vuejs_component_kwargs=None, vuejs_field_converters={},
|
vue_tagname=None,
|
||||||
|
vuejs_component_kwargs=None, vuejs_field_converters={}, json_data={}, included_templates={},
|
||||||
# TODO: ugh this is getting out hand!
|
# TODO: ugh this is getting out hand!
|
||||||
can_edit_help=False, edit_help_url=None, route_prefix=None,
|
can_edit_help=False, edit_help_url=None, route_prefix=None,
|
||||||
|
**kwargs
|
||||||
):
|
):
|
||||||
self.fields = None
|
self.fields = None
|
||||||
if fields is not None:
|
if fields is not None:
|
||||||
|
@ -378,21 +382,79 @@ class Form(object):
|
||||||
self.focus_spec = focus_spec
|
self.focus_spec = focus_spec
|
||||||
self.action_url = action_url
|
self.action_url = action_url
|
||||||
self.cancel_url = cancel_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_component_kwargs = vuejs_component_kwargs or {}
|
||||||
self.vuejs_field_converters = vuejs_field_converters or {}
|
self.vuejs_field_converters = vuejs_field_converters or {}
|
||||||
|
self.json_data = json_data or {}
|
||||||
|
self.included_templates = included_templates or {}
|
||||||
self.can_edit_help = can_edit_help
|
self.can_edit_help = can_edit_help
|
||||||
self.edit_help_url = edit_help_url
|
self.edit_help_url = edit_help_url
|
||||||
self.route_prefix = route_prefix
|
self.route_prefix = route_prefix
|
||||||
|
|
||||||
|
self.button_icon_submit = kwargs.get('button_icon_submit', 'save')
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.fields)
|
return iter(self.fields)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def component_studly(self):
|
def vue_component(self):
|
||||||
words = self.component.split('-')
|
"""
|
||||||
|
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])
|
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):
|
def __contains__(self, item):
|
||||||
return item in self.fields
|
return item in self.fields
|
||||||
|
|
||||||
|
@ -568,7 +630,9 @@ class Form(object):
|
||||||
self.schema[key].title = label
|
self.schema[key].title = label
|
||||||
|
|
||||||
def get_label(self, key):
|
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):
|
def set_readonly(self, key, readonly=True):
|
||||||
if readonly:
|
if readonly:
|
||||||
|
@ -644,7 +708,7 @@ class Form(object):
|
||||||
self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8))
|
self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8))
|
||||||
elif type_ == 'file':
|
elif type_ == 'file':
|
||||||
tmpstore = SessionFileUploadTempStore(self.request)
|
tmpstore = SessionFileUploadTempStore(self.request)
|
||||||
kw = {'widget': dfwidget.FileUploadWidget(tmpstore),
|
kw = {'widget': FileUploadWidget(tmpstore, request=self.request),
|
||||||
'title': self.get_label(key)}
|
'title': self.get_label(key)}
|
||||||
if 'required' in kwargs and not kwargs['required']:
|
if 'required' in kwargs and not kwargs['required']:
|
||||||
kw['missing'] = colander.null
|
kw['missing'] = colander.null
|
||||||
|
@ -799,6 +863,10 @@ class Form(object):
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
return self.render_deform(**kwargs)
|
return self.render_deform(**kwargs)
|
||||||
|
|
||||||
|
def get_deform(self):
|
||||||
|
""" """
|
||||||
|
return self.make_deform_form()
|
||||||
|
|
||||||
def make_deform_form(self):
|
def make_deform_form(self):
|
||||||
if not hasattr(self, 'deform_form'):
|
if not hasattr(self, 'deform_form'):
|
||||||
|
|
||||||
|
@ -837,6 +905,11 @@ class Form(object):
|
||||||
|
|
||||||
return self.deform_form
|
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):
|
def render_deform(self, dform=None, template=None, **kwargs):
|
||||||
if not template:
|
if not template:
|
||||||
template = '/forms/deform.mako'
|
template = '/forms/deform.mako'
|
||||||
|
@ -859,8 +932,8 @@ class Form(object):
|
||||||
context.setdefault('form_kwargs', {})
|
context.setdefault('form_kwargs', {})
|
||||||
# TODO: deprecate / remove the latter option here
|
# TODO: deprecate / remove the latter option here
|
||||||
if self.auto_disable_save or self.auto_disable:
|
if self.auto_disable_save or self.auto_disable:
|
||||||
context['form_kwargs'].setdefault('ref', self.component_studly)
|
context['form_kwargs'].setdefault('ref', self.vue_component)
|
||||||
context['form_kwargs']['@submit'] = 'submit{}'.format(self.component_studly)
|
context['form_kwargs']['@submit'] = 'submit{}'.format(self.vue_component)
|
||||||
if self.focus_spec:
|
if self.focus_spec:
|
||||||
context['form_kwargs']['data-focus'] = self.focus_spec
|
context['form_kwargs']['data-focus'] = self.focus_spec
|
||||||
context['request'] = self.request
|
context['request'] = self.request
|
||||||
|
@ -872,11 +945,13 @@ class Form(object):
|
||||||
return dict([(field, self.get_label(field))
|
return dict([(field, self.get_label(field))
|
||||||
for field in self])
|
for field in self])
|
||||||
|
|
||||||
def get_field_markdowns(self):
|
def get_field_markdowns(self, session=None):
|
||||||
model = self.request.rattail_config.get_model()
|
app = self.request.rattail_config.get_app()
|
||||||
|
model = app.model
|
||||||
|
session = session or Session()
|
||||||
|
|
||||||
if not hasattr(self, 'field_markdowns'):
|
if not hasattr(self, 'field_markdowns'):
|
||||||
infos = Session.query(model.TailboneFieldInfo)\
|
infos = session.query(model.TailboneFieldInfo)\
|
||||||
.filter(model.TailboneFieldInfo.route_prefix == self.route_prefix)\
|
.filter(model.TailboneFieldInfo.route_prefix == self.route_prefix)\
|
||||||
.all()
|
.all()
|
||||||
self.field_markdowns = dict([(info.field_name, info.markdown_text)
|
self.field_markdowns = dict([(info.field_name, info.markdown_text)
|
||||||
|
@ -884,6 +959,18 @@ class Form(object):
|
||||||
|
|
||||||
return self.field_markdowns
|
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):
|
def get_vuejs_model_value(self, field):
|
||||||
"""
|
"""
|
||||||
This method must return "raw" JS which will be assigned as the initial
|
This method must return "raw" JS which will be assigned as the initial
|
||||||
|
@ -950,7 +1037,11 @@ class Form(object):
|
||||||
def set_vuejs_component_kwargs(self, **kwargs):
|
def set_vuejs_component_kwargs(self, **kwargs):
|
||||||
self.vuejs_component_kwargs.update(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.
|
Render the Vue.js component HTML for the form.
|
||||||
|
|
||||||
|
@ -961,12 +1052,42 @@ class Form(object):
|
||||||
<tailbone-form :configure-fields-help="configureFieldsHelp">
|
<tailbone-form :configure-fields-help="configureFieldsHelp">
|
||||||
</tailbone-form>
|
</tailbone-form>
|
||||||
"""
|
"""
|
||||||
kwargs = dict(self.vuejs_component_kwargs)
|
kw = dict(self.vuejs_component_kwargs)
|
||||||
|
kw.update(kwargs)
|
||||||
if self.can_edit_help:
|
if self.can_edit_help:
|
||||||
kwargs.setdefault(':configure-fields-help', 'configureFieldsHelp')
|
kw.setdefault(':configure-fields-help', 'configureFieldsHelp')
|
||||||
return HTML.tag(self.component, **kwargs)
|
return HTML.tag(self.vue_tagname, **kw)
|
||||||
|
|
||||||
def render_field_complete(self, fieldname, bfield_attrs={}):
|
def set_json_data(self, key, value):
|
||||||
|
"""
|
||||||
|
Establish a data value for use in client-side JS. This value
|
||||||
|
will be JSON-encoded and made available to the
|
||||||
|
`<tailbone-form>` component within the client page.
|
||||||
|
"""
|
||||||
|
self.json_data[key] = value
|
||||||
|
|
||||||
|
def include_template(self, template, context):
|
||||||
|
"""
|
||||||
|
Declare a JS template as required by the current form. This
|
||||||
|
template will then be included in the final page, so all
|
||||||
|
widgets behave correctly.
|
||||||
|
"""
|
||||||
|
self.included_templates[template] = context
|
||||||
|
|
||||||
|
def render_included_templates(self):
|
||||||
|
templates = []
|
||||||
|
for template, context in self.included_templates.items():
|
||||||
|
context = dict(context)
|
||||||
|
context['form'] = self
|
||||||
|
templates.append(HTML.literal(render(template, context)))
|
||||||
|
return HTML.literal('\n').join(templates)
|
||||||
|
|
||||||
|
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>``
|
Render the given field completely, i.e. with ``<b-field>``
|
||||||
wrapper. Note that this is meant to render *editable* fields,
|
wrapper. Note that this is meant to render *editable* fields,
|
||||||
|
@ -984,7 +1105,7 @@ class Form(object):
|
||||||
|
|
||||||
if self.field_visible(fieldname):
|
if self.field_visible(fieldname):
|
||||||
label = self.get_label(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)
|
# these attrs will be for the <b-field> (*not* the widget)
|
||||||
attrs = {
|
attrs = {
|
||||||
|
@ -1017,9 +1138,17 @@ class Form(object):
|
||||||
if field_type:
|
if field_type:
|
||||||
attrs['type'] = field_type
|
attrs['type'] = field_type
|
||||||
if messages:
|
if messages:
|
||||||
attrs[':message'] = '[{}]'.format(', '.join([
|
if len(messages) == 1:
|
||||||
"'{}'".format(msg.replace("'", r"\'"))
|
msg = messages[0]
|
||||||
for msg in messages]))
|
if msg.startswith('`') and msg.endswith('`'):
|
||||||
|
attrs[':message'] = msg
|
||||||
|
else:
|
||||||
|
attrs['message'] = msg
|
||||||
|
else:
|
||||||
|
# nb. must pass an array as JSON string
|
||||||
|
attrs[':message'] = '[{}]'.format(', '.join([
|
||||||
|
"'{}'".format(msg.replace("'", r"\'"))
|
||||||
|
for msg in messages]))
|
||||||
|
|
||||||
# merge anything caller provided
|
# merge anything caller provided
|
||||||
attrs.update(bfield_attrs)
|
attrs.update(bfield_attrs)
|
||||||
|
@ -1067,15 +1196,27 @@ class Form(object):
|
||||||
label_contents.append(HTML.literal(' '))
|
label_contents.append(HTML.literal(' '))
|
||||||
label_contents.append(icon)
|
label_contents.append(icon)
|
||||||
|
|
||||||
# nb. must apply hack to get <template #label> as final result
|
# only declare label template if it's complex
|
||||||
label_template = HTML.tag('template', c=label_contents,
|
html = [html]
|
||||||
**{'#label': 1})
|
# TODO: figure out why complex label does not work for oruga
|
||||||
label_template = label_template.replace(
|
if self.request.use_oruga:
|
||||||
HTML.literal('<template #label="1"'),
|
attrs['label'] = label
|
||||||
HTML.literal('<template #label'))
|
else:
|
||||||
|
if len(label_contents) > 1:
|
||||||
|
|
||||||
|
# nb. must apply hack to get <template #label> as final result
|
||||||
|
label_template = HTML.tag('template', c=label_contents,
|
||||||
|
**{'#label': 1})
|
||||||
|
label_template = label_template.replace(
|
||||||
|
HTML.literal('<template #label="1"'),
|
||||||
|
HTML.literal('<template #label'))
|
||||||
|
html.insert(0, label_template)
|
||||||
|
|
||||||
|
else: # simple label
|
||||||
|
attrs['label'] = label
|
||||||
|
|
||||||
# and finally wrap it all in a <b-field>
|
# and finally wrap it all in a <b-field>
|
||||||
return HTML.tag('b-field', c=[label_template, html], **attrs)
|
return HTML.tag('b-field', c=html, **attrs)
|
||||||
|
|
||||||
elif field: # hidden field
|
elif field: # hidden field
|
||||||
|
|
||||||
|
@ -1083,6 +1224,18 @@ class Form(object):
|
||||||
# TODO: again, why does serialize() not return literal?
|
# TODO: again, why does serialize() not return literal?
|
||||||
return HTML.literal(field.serialize())
|
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):
|
def render_field_readonly(self, field_name, **kwargs):
|
||||||
"""
|
"""
|
||||||
Render the given field completely, but in read-only fashion.
|
Render the given field completely, but in read-only fashion.
|
||||||
|
@ -1093,20 +1246,30 @@ class Form(object):
|
||||||
if field_name not in self.fields:
|
if field_name not in self.fields:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
# TODO: fair bit of duplication here, should merge with deform.mako
|
|
||||||
label = kwargs.get('label')
|
label = kwargs.get('label')
|
||||||
if not label:
|
if not label:
|
||||||
label = self.get_label(field_name)
|
label = self.get_label(field_name)
|
||||||
label = HTML.tag('label', label, for_=field_name)
|
|
||||||
field = self.render_field_value(field_name) or ''
|
|
||||||
field_div = HTML.tag('div', class_='field', c=[field])
|
|
||||||
contents = [label, field_div]
|
|
||||||
|
|
||||||
if self.has_helptext(field_name):
|
value = self.render_field_value(field_name) or ''
|
||||||
contents.append(HTML.tag('span', class_='instructions',
|
|
||||||
c=[self.render_helptext(field_name)]))
|
|
||||||
|
|
||||||
return HTML.tag('div', class_='field-wrapper {}'.format(field_name), c=contents)
|
if not self.request.use_oruga:
|
||||||
|
|
||||||
|
label = HTML.tag('label', label, for_=field_name)
|
||||||
|
field_div = HTML.tag('div', class_='field', c=[value])
|
||||||
|
contents = [label, field_div]
|
||||||
|
|
||||||
|
if self.has_helptext(field_name):
|
||||||
|
contents.append(HTML.tag('span', class_='instructions',
|
||||||
|
c=[self.render_helptext(field_name)]))
|
||||||
|
|
||||||
|
return HTML.tag('div', class_='field-wrapper {}'.format(field_name), c=contents)
|
||||||
|
|
||||||
|
# nb. for some reason we must wrap once more for oruga,
|
||||||
|
# otherwise it splits up the field?!
|
||||||
|
value = HTML.tag('span', c=[value])
|
||||||
|
|
||||||
|
# oruga uses <o-field>
|
||||||
|
return HTML.tag('o-field', label=label, c=[value], **{':horizontal': 'true'})
|
||||||
|
|
||||||
def render_field_value(self, field_name):
|
def render_field_value(self, field_name):
|
||||||
record = self.model_instance
|
record = self.model_instance
|
||||||
|
@ -1130,7 +1293,7 @@ class Form(object):
|
||||||
value = self.obtain_value(record, field_name)
|
value = self.obtain_value(record, field_name)
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
app = self.get_rattail_app()
|
app = self.request.rattail_config.get_app()
|
||||||
value = app.localtime(value)
|
value = app.localtime(value)
|
||||||
return raw_datetime(self.request.rattail_config, value)
|
return raw_datetime(self.request.rattail_config, value)
|
||||||
|
|
||||||
|
@ -1160,7 +1323,7 @@ class Form(object):
|
||||||
value = self.obtain_value(obj, field)
|
value = self.obtain_value(obj, field)
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
app = self.get_rattail_app()
|
app = self.request.rattail_config.get_app()
|
||||||
return app.render_quantity(value)
|
return app.render_quantity(value)
|
||||||
|
|
||||||
def render_percent(self, obj, field):
|
def render_percent(self, obj, field):
|
||||||
|
@ -1212,12 +1375,19 @@ class Form(object):
|
||||||
|
|
||||||
def obtain_value(self, record, field_name):
|
def obtain_value(self, record, field_name):
|
||||||
if record:
|
if record:
|
||||||
|
|
||||||
|
if isinstance(record, dict):
|
||||||
|
return record[field_name]
|
||||||
|
|
||||||
|
try:
|
||||||
|
return getattr(record, field_name)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return record[field_name]
|
return record[field_name]
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return getattr(record, field_name, None)
|
pass
|
||||||
|
|
||||||
# TODO: is this always safe to do?
|
# TODO: is this always safe to do?
|
||||||
elif self.defaults and field_name in self.defaults:
|
elif self.defaults and field_name in self.defaults:
|
||||||
|
@ -1271,30 +1441,6 @@ class Form(object):
|
||||||
return False
|
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
|
@colander.deferred
|
||||||
def upload_widget(node, kw):
|
def upload_widget(node, kw):
|
||||||
request = kw['request']
|
request = kw['request']
|
||||||
|
|
|
@ -27,6 +27,7 @@ Form Widgets
|
||||||
import json
|
import json
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
import re
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
|
@ -249,6 +250,8 @@ class FalafelDateTimeWidget(dfwidget.DateTimeInputWidget):
|
||||||
"""
|
"""
|
||||||
template = 'datetime_falafel'
|
template = 'datetime_falafel'
|
||||||
|
|
||||||
|
new_pattern = re.compile(r'^\d\d?:\d\d:\d\d [AP]M$')
|
||||||
|
|
||||||
def serialize(self, field, cstruct, **kw):
|
def serialize(self, field, cstruct, **kw):
|
||||||
""" """
|
""" """
|
||||||
readonly = kw.get('readonly', self.readonly)
|
readonly = kw.get('readonly', self.readonly)
|
||||||
|
@ -260,6 +263,13 @@ class FalafelDateTimeWidget(dfwidget.DateTimeInputWidget):
|
||||||
""" """
|
""" """
|
||||||
if pstruct == '':
|
if pstruct == '':
|
||||||
return colander.null
|
return colander.null
|
||||||
|
|
||||||
|
# nb. we now allow '4:20:00 PM' on the widget side, but the
|
||||||
|
# true node needs it to be '16:20:00' instead
|
||||||
|
if self.new_pattern.match(pstruct['time']):
|
||||||
|
time = datetime.datetime.strptime(pstruct['time'], '%I:%M:%S %p')
|
||||||
|
pstruct['time'] = time.strftime('%H:%M:%S')
|
||||||
|
|
||||||
return pstruct
|
return pstruct
|
||||||
|
|
||||||
|
|
||||||
|
@ -327,6 +337,23 @@ class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget):
|
||||||
return field.renderer(template, **tmpl_values)
|
return field.renderer(template, **tmpl_values)
|
||||||
|
|
||||||
|
|
||||||
|
class FileUploadWidget(dfwidget.FileUploadWidget):
|
||||||
|
"""
|
||||||
|
Widget to handle file upload. Must override to add ``use_oruga``
|
||||||
|
to field template context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.request = kwargs.pop('request')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_template_values(self, field, cstruct, kw):
|
||||||
|
values = super().get_template_values(field, cstruct, kw)
|
||||||
|
if self.request:
|
||||||
|
values['use_oruga'] = self.request.use_oruga
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
class MultiFileUploadWidget(dfwidget.FileUploadWidget):
|
class MultiFileUploadWidget(dfwidget.FileUploadWidget):
|
||||||
"""
|
"""
|
||||||
Widget to handle multiple (arbitrary number) of file uploads.
|
Widget to handle multiple (arbitrary number) of file uploads.
|
||||||
|
@ -450,7 +477,8 @@ class CustomerAutocompleteWidget(JQueryAutocompleteWidget):
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.request = request
|
self.request = request
|
||||||
model = self.request.rattail_config.get_model()
|
app = self.request.rattail_config.get_app()
|
||||||
|
model = app.model
|
||||||
|
|
||||||
# must figure out URL providing autocomplete service
|
# must figure out URL providing autocomplete service
|
||||||
if 'service_url' not in kwargs:
|
if 'service_url' not in kwargs:
|
||||||
|
@ -471,7 +499,8 @@ class CustomerAutocompleteWidget(JQueryAutocompleteWidget):
|
||||||
""" """
|
""" """
|
||||||
# fetch customer to provide button label, if we have a value
|
# fetch customer to provide button label, if we have a value
|
||||||
if cstruct:
|
if cstruct:
|
||||||
model = self.request.rattail_config.get_model()
|
app = self.request.rattail_config.get_app()
|
||||||
|
model = app.model
|
||||||
customer = Session.get(model.Customer, cstruct)
|
customer = Session.get(model.Customer, cstruct)
|
||||||
if customer:
|
if customer:
|
||||||
self.field_display = str(customer)
|
self.field_display = str(customer)
|
||||||
|
@ -525,7 +554,8 @@ class DepartmentWidget(dfwidget.SelectWidget):
|
||||||
def __init__(self, request, **kwargs):
|
def __init__(self, request, **kwargs):
|
||||||
|
|
||||||
if 'values' not in kwargs:
|
if 'values' not in kwargs:
|
||||||
model = request.rattail_config.get_model()
|
app = request.rattail_config.get_app()
|
||||||
|
model = app.model
|
||||||
departments = Session.query(model.Department)\
|
departments = Session.query(model.Department)\
|
||||||
.order_by(model.Department.number)
|
.order_by(model.Department.number)
|
||||||
values = [(dept.uuid, str(dept))
|
values = [(dept.uuid, str(dept))
|
||||||
|
@ -567,7 +597,8 @@ class VendorAutocompleteWidget(JQueryAutocompleteWidget):
|
||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.request = request
|
self.request = request
|
||||||
model = self.request.rattail_config.get_model()
|
app = self.request.rattail_config.get_app()
|
||||||
|
model = app.model
|
||||||
|
|
||||||
# must figure out URL providing autocomplete service
|
# must figure out URL providing autocomplete service
|
||||||
if 'service_url' not in kwargs:
|
if 'service_url' not in kwargs:
|
||||||
|
@ -588,7 +619,8 @@ class VendorAutocompleteWidget(JQueryAutocompleteWidget):
|
||||||
""" """
|
""" """
|
||||||
# fetch vendor to provide button label, if we have a value
|
# fetch vendor to provide button label, if we have a value
|
||||||
if cstruct:
|
if cstruct:
|
||||||
model = self.request.rattail_config.get_model()
|
app = self.request.rattail_config.get_app()
|
||||||
|
model = app.model
|
||||||
vendor = Session.get(model.Vendor, cstruct)
|
vendor = Session.get(model.Vendor, cstruct)
|
||||||
if vendor:
|
if vendor:
|
||||||
self.field_display = str(vendor)
|
self.field_display = str(vendor)
|
||||||
|
@ -616,7 +648,8 @@ class VendorDropdownWidget(dfwidget.SelectWidget):
|
||||||
vendors = vendors()
|
vendors = vendors()
|
||||||
|
|
||||||
else: # default vendor list
|
else: # default vendor list
|
||||||
model = self.request.rattail_config.get_model()
|
app = self.request.rattail_config.get_app()
|
||||||
|
model = app.model
|
||||||
vendors = Session.query(model.Vendor)\
|
vendors = Session.query(model.Vendor)\
|
||||||
.order_by(model.Vendor.name)\
|
.order_by(model.Vendor.name)\
|
||||||
.all()
|
.all()
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -26,6 +26,7 @@ Grid Filters
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import datetime
|
import datetime
|
||||||
|
import decimal
|
||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
@ -647,12 +648,22 @@ class AlchemyNumericFilter(AlchemyGridFilter):
|
||||||
|
|
||||||
# first just make sure it's somewhat numeric
|
# first just make sure it's somewhat numeric
|
||||||
try:
|
try:
|
||||||
float(value)
|
self.parse_decimal(value)
|
||||||
except ValueError:
|
except decimal.InvalidOperation:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return bool(value and len(str(value)) > 8)
|
return bool(value and len(str(value)) > 8)
|
||||||
|
|
||||||
|
def parse_decimal(self, value):
|
||||||
|
if value:
|
||||||
|
value = value.replace(',', '')
|
||||||
|
return decimal.Decimal(value)
|
||||||
|
|
||||||
|
def encode_value(self, value):
|
||||||
|
if value:
|
||||||
|
value = str(self.parse_decimal(value))
|
||||||
|
return super().encode_value(value)
|
||||||
|
|
||||||
def filter_equal(self, query, value):
|
def filter_equal(self, query, value):
|
||||||
if self.value_invalid(value):
|
if self.value_invalid(value):
|
||||||
return query
|
return query
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,9 +24,8 @@
|
||||||
Tailbone Handler
|
Tailbone Handler
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
import warnings
|
||||||
|
|
||||||
import six
|
|
||||||
from mako.lookup import TemplateLookup
|
from mako.lookup import TemplateLookup
|
||||||
|
|
||||||
from rattail.app import GenericHandler
|
from rattail.app import GenericHandler
|
||||||
|
@ -41,7 +40,7 @@ class TailboneHandler(GenericHandler):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(TailboneHandler, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# TODO: make templates dir configurable?
|
# TODO: make templates dir configurable?
|
||||||
templates = [resource_path('rattail:templates/web')]
|
templates = [resource_path('rattail:templates/web')]
|
||||||
|
@ -49,11 +48,14 @@ class TailboneHandler(GenericHandler):
|
||||||
|
|
||||||
def get_menu_handler(self, **kwargs):
|
def get_menu_handler(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Get the configured "menu" handler.
|
DEPRECATED; use
|
||||||
|
:meth:`wuttaweb.handler.WebHandler.get_menu_handler()`
|
||||||
:returns: The :class:`~tailbone.menus.MenuHandler` instance
|
instead.
|
||||||
for the app.
|
|
||||||
"""
|
"""
|
||||||
|
warnings.warn("TailboneHandler.get_menu_handler() is deprecated; "
|
||||||
|
"please use WebHandler.get_menu_handler() instead",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
|
||||||
if not hasattr(self, 'menu_handler'):
|
if not hasattr(self, 'menu_handler'):
|
||||||
spec = self.config.get('tailbone.menus', 'handler',
|
spec = self.config.get('tailbone.menus', 'handler',
|
||||||
default='tailbone.menus:MenuHandler')
|
default='tailbone.menus:MenuHandler')
|
||||||
|
@ -67,7 +69,7 @@ class TailboneHandler(GenericHandler):
|
||||||
Returns an iterator over all registered Tailbone providers.
|
Returns an iterator over all registered Tailbone providers.
|
||||||
"""
|
"""
|
||||||
providers = get_all_providers(self.config)
|
providers = get_all_providers(self.config)
|
||||||
return six.itervalues(providers)
|
return providers.values()
|
||||||
|
|
||||||
def write_model_view(self, data, path, **kwargs):
|
def write_model_view(self, data, path, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,6 +24,9 @@
|
||||||
Template Context Helpers
|
Template Context Helpers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# start off with all from wuttaweb
|
||||||
|
from wuttaweb.helpers import *
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
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.util import pretty_quantity, pretty_hours, hours_as_decimal
|
||||||
from rattail.db.util import maxlen
|
from rattail.db.util import maxlen
|
||||||
|
|
||||||
from webhelpers2.html import *
|
from tailbone.util import (pretty_datetime, raw_datetime,
|
||||||
from webhelpers2.html.tags import *
|
|
||||||
|
|
||||||
from tailbone.util import (csrf_token, get_csrf_token,
|
|
||||||
pretty_datetime, raw_datetime,
|
|
||||||
render_markdown,
|
render_markdown,
|
||||||
route_exists,
|
route_exists)
|
||||||
get_liburl)
|
|
||||||
|
|
||||||
|
|
||||||
def pretty_date(date):
|
def pretty_date(date):
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,37 +24,48 @@
|
||||||
App Menus
|
App Menus
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from rattail.app import GenericHandler
|
|
||||||
from rattail.util import prettify, simple_error
|
from rattail.util import prettify, simple_error
|
||||||
|
|
||||||
from webhelpers2.html import tags, HTML
|
from webhelpers2.html import tags, HTML
|
||||||
|
|
||||||
|
from wuttaweb.menus import MenuHandler as WuttaMenuHandler
|
||||||
|
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MenuHandler(GenericHandler):
|
class TailboneMenuHandler(WuttaMenuHandler):
|
||||||
"""
|
"""
|
||||||
Base class and default implementation for menu handler.
|
Base class and default implementation for menu handler.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def make_raw_menus(self, request, **kwargs):
|
##############################
|
||||||
"""
|
# internal methods
|
||||||
Generate a full set of "raw" menus for the app.
|
##############################
|
||||||
|
|
||||||
The "raw" menus are basically just a set of dicts to represent
|
def _is_allowed(self, request, item):
|
||||||
the final menus.
|
"""
|
||||||
|
TODO: must override this until wuttaweb has proper user auth checks
|
||||||
|
"""
|
||||||
|
perm = item.get('perm')
|
||||||
|
if perm:
|
||||||
|
return request.has_perm(perm)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _make_raw_menus(self, request, **kwargs):
|
||||||
|
"""
|
||||||
|
We are overriding this to allow for making dynamic menus from
|
||||||
|
config/settings. Which may or may not be a good idea..
|
||||||
"""
|
"""
|
||||||
# first try to make menus from config, but this is highly
|
# first try to make menus from config, but this is highly
|
||||||
# susceptible to failure, so try to warn user of problems
|
# susceptible to failure, so try to warn user of problems
|
||||||
try:
|
try:
|
||||||
menus = self.make_menus_from_config(request)
|
menus = self._make_menus_from_config(request)
|
||||||
if menus:
|
if menus:
|
||||||
return menus
|
return menus
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
@ -71,9 +82,9 @@ class MenuHandler(GenericHandler):
|
||||||
request.session.flash(msg, 'warning')
|
request.session.flash(msg, 'warning')
|
||||||
|
|
||||||
# okay, no config, so menus will be built from code
|
# okay, no config, so menus will be built from code
|
||||||
return self.make_menus(request)
|
return self.make_menus(request, **kwargs)
|
||||||
|
|
||||||
def make_menus_from_config(self, request, **kwargs):
|
def _make_menus_from_config(self, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Try to build a complete menu set from config/settings.
|
Try to build a complete menu set from config/settings.
|
||||||
|
|
||||||
|
@ -85,7 +96,7 @@ class MenuHandler(GenericHandler):
|
||||||
if not main_keys:
|
if not main_keys:
|
||||||
return
|
return
|
||||||
|
|
||||||
model = self.model
|
model = self.app.model
|
||||||
menus = []
|
menus = []
|
||||||
|
|
||||||
# menu definition can come either from config file or db
|
# menu definition can come either from config file or db
|
||||||
|
@ -101,16 +112,15 @@ class MenuHandler(GenericHandler):
|
||||||
query=query, key='name',
|
query=query, key='name',
|
||||||
normalizer=lambda s: s.value)
|
normalizer=lambda s: s.value)
|
||||||
for key in main_keys:
|
for key in main_keys:
|
||||||
menus.append(self.make_single_menu_from_settings(request, key,
|
menus.append(self._make_single_menu_from_settings(request, key, settings))
|
||||||
settings))
|
|
||||||
|
|
||||||
else: # read from config file only
|
else: # read from config file only
|
||||||
for key in main_keys:
|
for key in main_keys:
|
||||||
menus.append(self.make_single_menu_from_config(request, key))
|
menus.append(self._make_single_menu_from_config(request, key))
|
||||||
|
|
||||||
return menus
|
return menus
|
||||||
|
|
||||||
def make_single_menu_from_config(self, request, key, **kwargs):
|
def _make_single_menu_from_config(self, request, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Makes a single top-level menu dict from config file. Note
|
Makes a single top-level menu dict from config file. Note
|
||||||
that this will read from config file(s) *only* and avoids
|
that this will read from config file(s) *only* and avoids
|
||||||
|
@ -178,7 +188,7 @@ class MenuHandler(GenericHandler):
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
def make_single_menu_from_settings(self, request, key, settings, **kwargs):
|
def _make_single_menu_from_settings(self, request, key, settings, **kwargs):
|
||||||
"""
|
"""
|
||||||
Makes a single top-level menu dict from DB settings.
|
Makes a single top-level menu dict from DB settings.
|
||||||
"""
|
"""
|
||||||
|
@ -237,6 +247,10 @@ class MenuHandler(GenericHandler):
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# menu defaults
|
||||||
|
##############################
|
||||||
|
|
||||||
def make_menus(self, request, **kwargs):
|
def make_menus(self, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Make the full set of menus for the app.
|
Make the full set of menus for the app.
|
||||||
|
@ -267,8 +281,9 @@ class MenuHandler(GenericHandler):
|
||||||
"""
|
"""
|
||||||
Make a set of menus for all registered system integrations.
|
Make a set of menus for all registered system integrations.
|
||||||
"""
|
"""
|
||||||
|
tb = self.app.get_tailbone_handler()
|
||||||
menus = []
|
menus = []
|
||||||
for provider in self.tb.iter_providers():
|
for provider in tb.iter_providers():
|
||||||
menu = provider.make_integration_menu(request)
|
menu = provider.make_integration_menu(request)
|
||||||
if menu:
|
if menu:
|
||||||
menus.append(menu)
|
menus.append(menu)
|
||||||
|
@ -379,6 +394,11 @@ class MenuHandler(GenericHandler):
|
||||||
'route': 'products',
|
'route': 'products',
|
||||||
'perm': 'products.list',
|
'perm': 'products.list',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'title': "Product Costs",
|
||||||
|
'route': 'product_costs',
|
||||||
|
'perm': 'product_costs.list',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'title': "Departments",
|
'title': "Departments",
|
||||||
'route': 'departments',
|
'route': 'departments',
|
||||||
|
@ -436,6 +456,11 @@ class MenuHandler(GenericHandler):
|
||||||
'route': 'vendors',
|
'route': 'vendors',
|
||||||
'perm': 'vendors.list',
|
'perm': 'vendors.list',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'title': "Product Costs",
|
||||||
|
'route': 'product_costs',
|
||||||
|
'perm': 'product_costs.list',
|
||||||
|
},
|
||||||
{'type': 'sep'},
|
{'type': 'sep'},
|
||||||
{
|
{
|
||||||
'title': "Ordering",
|
'title': "Ordering",
|
||||||
|
@ -688,7 +713,7 @@ class MenuHandler(GenericHandler):
|
||||||
},
|
},
|
||||||
{'type': 'sep'},
|
{'type': 'sep'},
|
||||||
{
|
{
|
||||||
'title': "App Details",
|
'title': "App Info",
|
||||||
'route': 'appinfo',
|
'route': 'appinfo',
|
||||||
'perm': 'appinfo.list',
|
'perm': 'appinfo.list',
|
||||||
},
|
},
|
||||||
|
@ -723,182 +748,25 @@ class MenuHandler(GenericHandler):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def make_simple_menus(request):
|
class MenuHandler(TailboneMenuHandler):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
warnings.warn("tailbone.menus.MenuHandler is deprecated; "
|
||||||
|
"please use tailbone.menus.TailboneMenuHandler instead",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class NullMenuHandler(WuttaMenuHandler):
|
||||||
"""
|
"""
|
||||||
Build the main menu list for the app.
|
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.
|
||||||
"""
|
"""
|
||||||
app = request.rattail_config.get_app()
|
|
||||||
tailbone_handler = app.get_tailbone_handler()
|
|
||||||
menu_handler = tailbone_handler.get_menu_handler()
|
|
||||||
|
|
||||||
raw_menus = menu_handler.make_raw_menus(request)
|
def make_menus(self, request, **kwargs):
|
||||||
|
return []
|
||||||
# now we have "simple" (raw) menus definition, but must refine
|
|
||||||
# that somewhat to produce our final menus
|
|
||||||
mark_allowed(request, raw_menus)
|
|
||||||
final_menus = []
|
|
||||||
for topitem in raw_menus:
|
|
||||||
|
|
||||||
if topitem['allowed']:
|
|
||||||
|
|
||||||
if topitem.get('type') == 'link':
|
|
||||||
final_menus.append(make_menu_entry(request, topitem))
|
|
||||||
|
|
||||||
else: # assuming 'menu' type
|
|
||||||
|
|
||||||
menu_items = []
|
|
||||||
for item in topitem['items']:
|
|
||||||
if not item['allowed']:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# nested submenu
|
|
||||||
if item.get('type') == 'menu':
|
|
||||||
submenu_items = []
|
|
||||||
for subitem in item['items']:
|
|
||||||
if subitem['allowed']:
|
|
||||||
submenu_items.append(make_menu_entry(request, subitem))
|
|
||||||
menu_items.append({
|
|
||||||
'type': 'submenu',
|
|
||||||
'title': item['title'],
|
|
||||||
'items': submenu_items,
|
|
||||||
'is_menu': True,
|
|
||||||
'is_sep': False,
|
|
||||||
})
|
|
||||||
|
|
||||||
elif item.get('type') == 'sep':
|
|
||||||
# we only want to add a sep, *if* we already have some
|
|
||||||
# menu items (i.e. there is something to separate)
|
|
||||||
# *and* the last menu item is not a sep (avoid doubles)
|
|
||||||
if menu_items and not menu_items[-1]['is_sep']:
|
|
||||||
menu_items.append(make_menu_entry(request, item))
|
|
||||||
|
|
||||||
else: # standard menu item
|
|
||||||
menu_items.append(make_menu_entry(request, item))
|
|
||||||
|
|
||||||
# remove final separator if present
|
|
||||||
if menu_items and menu_items[-1]['is_sep']:
|
|
||||||
menu_items.pop()
|
|
||||||
|
|
||||||
# only add if we wound up with something
|
|
||||||
assert menu_items
|
|
||||||
if menu_items:
|
|
||||||
group = {
|
|
||||||
'type': 'menu',
|
|
||||||
'key': topitem.get('key'),
|
|
||||||
'title': topitem['title'],
|
|
||||||
'items': menu_items,
|
|
||||||
'is_menu': True,
|
|
||||||
'is_link': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
# topitem w/ no key likely means it did not come
|
|
||||||
# from config but rather explicit definition in
|
|
||||||
# code. so we are free to "invent" a (safe) key
|
|
||||||
# for it, since that is only for editing config
|
|
||||||
if not group['key']:
|
|
||||||
group['key'] = make_menu_key(request.rattail_config,
|
|
||||||
topitem['title'])
|
|
||||||
|
|
||||||
final_menus.append(group)
|
|
||||||
|
|
||||||
return final_menus
|
|
||||||
|
|
||||||
|
|
||||||
def make_menu_key(config, value):
|
|
||||||
"""
|
|
||||||
Generate a normalized menu key for the given value.
|
|
||||||
"""
|
|
||||||
return re.sub(r'\W', '', value.lower())
|
|
||||||
|
|
||||||
|
|
||||||
def make_menu_entry(request, item):
|
|
||||||
"""
|
|
||||||
Convert a simple menu entry dict, into a proper menu-related object, for
|
|
||||||
use in constructing final menu.
|
|
||||||
"""
|
|
||||||
# separator
|
|
||||||
if item.get('type') == 'sep':
|
|
||||||
return {
|
|
||||||
'type': 'sep',
|
|
||||||
'is_menu': False,
|
|
||||||
'is_sep': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
# standard menu item
|
|
||||||
entry = {
|
|
||||||
'type': 'item',
|
|
||||||
'title': item['title'],
|
|
||||||
'perm': item.get('perm'),
|
|
||||||
'target': item.get('target'),
|
|
||||||
'is_link': True,
|
|
||||||
'is_menu': False,
|
|
||||||
'is_sep': False,
|
|
||||||
}
|
|
||||||
if item.get('route'):
|
|
||||||
entry['route'] = item['route']
|
|
||||||
try:
|
|
||||||
entry['url'] = request.route_url(entry['route'])
|
|
||||||
except KeyError: # happens if no such route
|
|
||||||
log.warning("invalid route name for menu entry: %s", entry)
|
|
||||||
entry['url'] = entry['route']
|
|
||||||
entry['key'] = entry['route']
|
|
||||||
else:
|
|
||||||
if item.get('url'):
|
|
||||||
entry['url'] = item['url']
|
|
||||||
entry['key'] = make_menu_key(request.rattail_config, entry['title'])
|
|
||||||
return entry
|
|
||||||
|
|
||||||
|
|
||||||
def is_allowed(request, item):
|
|
||||||
"""
|
|
||||||
Logic to determine if a given menu item is "allowed" for current user.
|
|
||||||
"""
|
|
||||||
perm = item.get('perm')
|
|
||||||
if perm:
|
|
||||||
return request.has_perm(perm)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def mark_allowed(request, menus):
|
|
||||||
"""
|
|
||||||
Traverse the menu set, and mark each item as "allowed" (or not) based on
|
|
||||||
current user permissions.
|
|
||||||
"""
|
|
||||||
for topitem in menus:
|
|
||||||
|
|
||||||
if topitem.get('type', 'menu') == 'menu':
|
|
||||||
topitem['allowed'] = False
|
|
||||||
|
|
||||||
for item in topitem['items']:
|
|
||||||
|
|
||||||
if item.get('type') == 'menu':
|
|
||||||
for subitem in item['items']:
|
|
||||||
subitem['allowed'] = is_allowed(request, subitem)
|
|
||||||
|
|
||||||
item['allowed'] = False
|
|
||||||
for subitem in item['items']:
|
|
||||||
if subitem['allowed'] and subitem.get('type') != 'sep':
|
|
||||||
item['allowed'] = True
|
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
|
||||||
item['allowed'] = is_allowed(request, item)
|
|
||||||
|
|
||||||
for item in topitem['items']:
|
|
||||||
if item['allowed'] and item.get('type') != 'sep':
|
|
||||||
topitem['allowed'] = True
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def make_admin_menu(request, **kwargs):
|
|
||||||
"""
|
|
||||||
Generate a typical Admin menu
|
|
||||||
"""
|
|
||||||
warnings.warn("make_admin_menu() function is deprecated; please use "
|
|
||||||
"MenuHandler.make_admin_menu() instead",
|
|
||||||
DeprecationWarning, stacklevel=2)
|
|
||||||
|
|
||||||
app = request.rattail_config.get_app()
|
|
||||||
tailbone_handler = app.get_tailbone_handler()
|
|
||||||
menu_handler = tailbone_handler.get_menu_handler()
|
|
||||||
return menu_handler.make_admin_menu(request, **kwargs)
|
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# Rattail -- Retail Software Framework
|
|
||||||
# Copyright © 2010-2017 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of Rattail.
|
|
||||||
#
|
|
||||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
|
||||||
# terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation, either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Pyramid scaffold templates
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
from rattail.files import resource_path
|
|
||||||
from rattail.util import prettify
|
|
||||||
|
|
||||||
from pyramid.scaffolds import PyramidTemplate
|
|
||||||
|
|
||||||
|
|
||||||
class RattailTemplate(PyramidTemplate):
|
|
||||||
_template_dir = resource_path('rattail:data/project')
|
|
||||||
summary = "Starter project based on Rattail / Tailbone"
|
|
||||||
|
|
||||||
def pre(self, command, output_dir, vars):
|
|
||||||
"""
|
|
||||||
Adds some more variables to the template context.
|
|
||||||
"""
|
|
||||||
vars['project_title'] = prettify(vars['project'])
|
|
||||||
vars['package_title'] = vars['package'].capitalize()
|
|
||||||
return super(RattailTemplate, self).pre(command, output_dir, vars)
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2017 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,9 +24,8 @@
|
||||||
Static Assets
|
Static Assets
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
config.include('wuttaweb.static')
|
||||||
config.add_static_view('tailbone', 'tailbone:static')
|
config.add_static_view('tailbone', 'tailbone:static')
|
||||||
config.add_static_view('deform', 'deform:static')
|
config.add_static_view('deform', 'deform:static')
|
||||||
|
|
|
@ -3,10 +3,6 @@
|
||||||
* Grid Filters
|
* Grid Filters
|
||||||
******************************/
|
******************************/
|
||||||
|
|
||||||
.filters .filter {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters .filter-fieldname .field,
|
.filters .filter-fieldname .field,
|
||||||
.filters .filter-fieldname .field label {
|
.filters .filter-fieldname .field label {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -25,6 +25,11 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-tools {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.grid-wrapper .grid-header td.tools {
|
.grid-wrapper .grid-header td.tools {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -90,6 +90,11 @@ header span.header-text {
|
||||||
* "object helper" panel
|
* "object helper" panel
|
||||||
******************************/
|
******************************/
|
||||||
|
|
||||||
|
.object-helpers .panel {
|
||||||
|
margin: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.object-helpers .panel-heading {
|
.object-helpers .panel-heading {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ const TailboneDatepicker = {
|
||||||
'icon="calendar-alt"',
|
'icon="calendar-alt"',
|
||||||
':date-formatter="formatDate"',
|
':date-formatter="formatDate"',
|
||||||
':date-parser="parseDate"',
|
':date-parser="parseDate"',
|
||||||
':value="value ? parseDate(value) : null"',
|
':value="buefyValue"',
|
||||||
'@input="dateChanged"',
|
'@input="dateChanged"',
|
||||||
':disabled="disabled"',
|
':disabled="disabled"',
|
||||||
'ref="trueDatePicker"',
|
'ref="trueDatePicker"',
|
||||||
|
@ -26,6 +26,18 @@ const TailboneDatepicker = {
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
buefyValue: this.parseDate(this.value),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
value(to, from) {
|
||||||
|
this.buefyValue = this.parseDate(to)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
|
@ -43,9 +55,12 @@ const TailboneDatepicker = {
|
||||||
},
|
},
|
||||||
|
|
||||||
parseDate(date) {
|
parseDate(date) {
|
||||||
// note, this assumes classic YYYY-MM-DD (i.e. ISO?) format
|
if (typeof(date) == 'string') {
|
||||||
var parts = date.split('-')
|
// note, this assumes classic YYYY-MM-DD (i.e. ISO?) format
|
||||||
return new Date(parts[0], parseInt(parts[1]) - 1, parts[2])
|
var parts = date.split('-')
|
||||||
|
return new Date(parts[0], parseInt(parts[1]) - 1, parts[2])
|
||||||
|
}
|
||||||
|
return date
|
||||||
},
|
},
|
||||||
|
|
||||||
dateChanged(date) {
|
dateChanged(date) {
|
||||||
|
|
|
@ -1,167 +0,0 @@
|
||||||
|
|
||||||
const GridFilterNumericValue = {
|
|
||||||
template: '#grid-filter-numeric-value-template',
|
|
||||||
props: {
|
|
||||||
value: String,
|
|
||||||
wantsRange: Boolean,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
startValue: null,
|
|
||||||
endValue: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.wantsRange) {
|
|
||||||
if (this.value.includes('|')) {
|
|
||||||
let values = this.value.split('|')
|
|
||||||
if (values.length == 2) {
|
|
||||||
this.startValue = values[0]
|
|
||||||
this.endValue = values[1]
|
|
||||||
} else {
|
|
||||||
this.startValue = this.value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.startValue = this.value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.startValue = this.value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// when changing from e.g. 'equal' to 'between' filter verbs,
|
|
||||||
// must proclaim new filter value, to reflect (lack of) range
|
|
||||||
wantsRange(val) {
|
|
||||||
if (val) {
|
|
||||||
this.$emit('input', this.startValue + '|' + this.endValue)
|
|
||||||
} else {
|
|
||||||
this.$emit('input', this.startValue)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
focus() {
|
|
||||||
this.$refs.startValue.focus()
|
|
||||||
},
|
|
||||||
startValueChanged(value) {
|
|
||||||
if (this.wantsRange) {
|
|
||||||
value += '|' + this.endValue
|
|
||||||
}
|
|
||||||
this.$emit('input', value)
|
|
||||||
},
|
|
||||||
endValueChanged(value) {
|
|
||||||
value = this.startValue + '|' + value
|
|
||||||
this.$emit('input', value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.component('grid-filter-numeric-value', GridFilterNumericValue)
|
|
||||||
|
|
||||||
|
|
||||||
const GridFilterDateValue = {
|
|
||||||
template: '#grid-filter-date-value-template',
|
|
||||||
props: {
|
|
||||||
value: String,
|
|
||||||
dateRange: Boolean,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
startDate: null,
|
|
||||||
endDate: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.dateRange) {
|
|
||||||
if (this.value.includes('|')) {
|
|
||||||
let values = this.value.split('|')
|
|
||||||
if (values.length == 2) {
|
|
||||||
this.startDate = values[0]
|
|
||||||
this.endDate = values[1]
|
|
||||||
} else {
|
|
||||||
this.startDate = this.value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.startDate = this.value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.startDate = this.value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
focus() {
|
|
||||||
this.$refs.startDate.focus()
|
|
||||||
},
|
|
||||||
startDateChanged(value) {
|
|
||||||
if (this.dateRange) {
|
|
||||||
value += '|' + this.endDate
|
|
||||||
}
|
|
||||||
this.$emit('input', value)
|
|
||||||
},
|
|
||||||
endDateChanged(value) {
|
|
||||||
value = this.startDate + '|' + value
|
|
||||||
this.$emit('input', value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.component('grid-filter-date-value', GridFilterDateValue)
|
|
||||||
|
|
||||||
|
|
||||||
const GridFilter = {
|
|
||||||
template: '#grid-filter-template',
|
|
||||||
props: {
|
|
||||||
filter: Object
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
changeVerb() {
|
|
||||||
// set focus to value input, "as quickly as we can"
|
|
||||||
this.$nextTick(function() {
|
|
||||||
this.focusValue()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
valuedVerb() {
|
|
||||||
/* this returns true if the filter's current verb should expose value input(s) */
|
|
||||||
|
|
||||||
// if filter has no "valueless" verbs, then all verbs should expose value inputs
|
|
||||||
if (!this.filter.valueless_verbs) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// if filter *does* have valueless verbs, check if "current" verb is valueless
|
|
||||||
if (this.filter.valueless_verbs.includes(this.filter.verb)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// current verb is *not* valueless
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
|
|
||||||
multiValuedVerb() {
|
|
||||||
/* this returns true if the filter's current verb should expose a multi-value input */
|
|
||||||
|
|
||||||
// if filter has no "multi-value" verbs then we safely assume false
|
|
||||||
if (!this.filter.multiple_value_verbs) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// if filter *does* have multi-value verbs, see if "current" is one
|
|
||||||
if (this.filter.multiple_value_verbs.includes(this.filter.verb)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// current verb is not multi-value
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
|
|
||||||
focusValue: function() {
|
|
||||||
this.$refs.valueInput.focus()
|
|
||||||
// this.$refs.valueInput.select()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vue.component('grid-filter', GridFilter)
|
|
|
@ -24,9 +24,10 @@
|
||||||
Event Subscribers
|
Event Subscribers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import six
|
|
||||||
import json
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
|
import warnings
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
import rattail
|
import rattail
|
||||||
|
|
||||||
|
@ -35,161 +36,169 @@ import deform
|
||||||
from pyramid import threadlocal
|
from pyramid import threadlocal
|
||||||
from webhelpers2.html import tags
|
from webhelpers2.html import tags
|
||||||
|
|
||||||
|
from wuttaweb import subscribers as base
|
||||||
|
|
||||||
import tailbone
|
import tailbone
|
||||||
from tailbone import helpers
|
from tailbone import helpers
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.config import csrf_header_name, should_expose_websockets
|
from tailbone.config import csrf_header_name, should_expose_websockets
|
||||||
from tailbone.menus import make_simple_menus
|
|
||||||
from tailbone.util import get_available_themes, get_global_search_options
|
from tailbone.util import get_available_themes, get_global_search_options
|
||||||
|
|
||||||
|
|
||||||
def new_request(event):
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def new_request(event, session=None):
|
||||||
"""
|
"""
|
||||||
Identify the current user, and cache their current permissions. Also adds
|
Event hook called when processing a new request.
|
||||||
the ``rattail_config`` attribute to the request.
|
|
||||||
|
|
||||||
A global Rattail ``config`` should already be present within the Pyramid
|
This first invokes the upstream hooks:
|
||||||
application registry's settings, which would normally be accessed via::
|
|
||||||
|
|
||||||
request.registry.settings['rattail_config']
|
|
||||||
|
|
||||||
This function merely "promotes" that config object so that it is more
|
* :func:`wuttaweb:wuttaweb.subscribers.new_request()`
|
||||||
directly accessible, a la::
|
* :func:`wuttaweb:wuttaweb.subscribers.new_request_set_user()`
|
||||||
|
|
||||||
request.rattail_config
|
It then adds more things to the request object; among them:
|
||||||
|
|
||||||
.. note::
|
.. attribute:: request.rattail_config
|
||||||
This of course assumes that a Rattail ``config`` object *has* in fact
|
|
||||||
already been placed in the application registry settings. If this is
|
|
||||||
not the case, this function will do nothing.
|
|
||||||
|
|
||||||
Also, attach some goodies to the request object:
|
Reference to the app :term:`config object`. Note that this
|
||||||
|
will be the same as :attr:`wuttaweb:request.wutta_config`.
|
||||||
|
|
||||||
* The currently logged-in user instance (if any), as ``user``.
|
.. method:: request.register_component(tagname, classname)
|
||||||
|
|
||||||
* ``is_admin`` flag indicating whether user has the Administrator role.
|
Function to register a Vue component for use with the app.
|
||||||
|
|
||||||
* ``is_root`` flag indicating whether user is currently elevated to root.
|
This can be called from wherever a component is defined, and
|
||||||
|
then in the base template all registered components will be
|
||||||
* A shortcut method for permission checking, as ``has_perm()``.
|
properly loaded.
|
||||||
"""
|
"""
|
||||||
request = event.request
|
request = event.request
|
||||||
rattail_config = request.registry.settings.get('rattail_config')
|
|
||||||
# TODO: why would this ever be null?
|
|
||||||
if rattail_config:
|
|
||||||
request.rattail_config = rattail_config
|
|
||||||
|
|
||||||
def user(request):
|
# invoke main upstream logic
|
||||||
user = None
|
# nb. this sets request.wutta_config
|
||||||
uuid = request.authenticated_userid
|
base.new_request(event)
|
||||||
if uuid:
|
|
||||||
model = request.rattail_config.get_model()
|
|
||||||
user = Session.get(model.User, uuid)
|
|
||||||
if user:
|
|
||||||
Session().set_continuum_user(user)
|
|
||||||
return user
|
|
||||||
|
|
||||||
request.set_property(user, reify=True)
|
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_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
|
||||||
|
|
||||||
|
# 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
|
# 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 = bool(request.user) and request.user.is_admin()
|
# request.register_component()
|
||||||
request.is_root = request.is_admin and request.session.get('is_root', False)
|
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 tagname in request._tailbone_registered_components:
|
||||||
if rattail_config:
|
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()
|
request._tailbone_registered_components[tagname] = classname
|
||||||
auth = app.get_auth_handler()
|
request.register_component = register_component
|
||||||
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 before_render(event):
|
def before_render(event):
|
||||||
"""
|
"""
|
||||||
Adds goodies to the global template renderer context.
|
Adds goodies to the global template renderer context.
|
||||||
"""
|
"""
|
||||||
|
# log.debug("before_render: %s", event)
|
||||||
|
|
||||||
|
# invoke upstream logic
|
||||||
|
base.before_render(event)
|
||||||
|
|
||||||
request = event.get('request') or threadlocal.get_current_request()
|
request = event.get('request') or threadlocal.get_current_request()
|
||||||
rattail_config = request.rattail_config
|
config = request.wutta_config
|
||||||
|
app = config.get_app()
|
||||||
|
|
||||||
renderer_globals = event
|
renderer_globals = event
|
||||||
renderer_globals['rattail_app'] = request.rattail_config.get_app()
|
|
||||||
renderer_globals['app_title'] = request.rattail_config.app_title()
|
# overrides
|
||||||
renderer_globals['h'] = helpers
|
renderer_globals['h'] = helpers
|
||||||
renderer_globals['url'] = request.route_url
|
|
||||||
renderer_globals['rattail'] = rattail
|
# misc.
|
||||||
renderer_globals['tailbone'] = tailbone
|
|
||||||
renderer_globals['model'] = request.rattail_config.get_model()
|
|
||||||
renderer_globals['enum'] = request.rattail_config.get_enum()
|
|
||||||
renderer_globals['six'] = six
|
|
||||||
renderer_globals['json'] = json
|
|
||||||
renderer_globals['datetime'] = datetime
|
renderer_globals['datetime'] = datetime
|
||||||
renderer_globals['colander'] = colander
|
renderer_globals['colander'] = colander
|
||||||
renderer_globals['deform'] = deform
|
renderer_globals['deform'] = deform
|
||||||
renderer_globals['csrf_header_name'] = csrf_header_name(request.rattail_config)
|
renderer_globals['csrf_header_name'] = csrf_header_name(config)
|
||||||
|
|
||||||
|
# TODO: deprecate / remove these
|
||||||
|
renderer_globals['rattail_app'] = app
|
||||||
|
renderer_globals['app_title'] = app.get_title()
|
||||||
|
renderer_globals['app_version'] = app.get_version()
|
||||||
|
renderer_globals['rattail'] = rattail
|
||||||
|
renderer_globals['tailbone'] = tailbone
|
||||||
|
renderer_globals['model'] = app.model
|
||||||
|
renderer_globals['enum'] = app.enum
|
||||||
|
|
||||||
# theme - we only want do this for classic web app, *not* API
|
# theme - we only want do this for classic web app, *not* API
|
||||||
# TODO: so, clearly we need a better way to distinguish the two
|
# TODO: so, clearly we need a better way to distinguish the two
|
||||||
if 'tailbone.theme' in request.registry.settings:
|
if 'tailbone.theme' in request.registry.settings:
|
||||||
renderer_globals['theme'] = request.registry.settings['tailbone.theme']
|
renderer_globals['theme'] = request.registry.settings['tailbone.theme']
|
||||||
# note, this is just a global flag; user still needs permission to see picker
|
# note, this is just a global flag; user still needs permission to see picker
|
||||||
expose_picker = request.rattail_config.getbool('tailbone', 'themes.expose_picker',
|
expose_picker = config.get_bool('tailbone.themes.expose_picker',
|
||||||
default=False)
|
default=False)
|
||||||
renderer_globals['expose_theme_picker'] = expose_picker
|
renderer_globals['expose_theme_picker'] = expose_picker
|
||||||
if expose_picker:
|
if expose_picker:
|
||||||
|
|
||||||
# TODO: should remove 'falafel' option altogether
|
# TODO: should remove 'falafel' option altogether
|
||||||
available = get_available_themes(request.rattail_config,
|
available = get_available_themes(config)
|
||||||
include=['falafel'])
|
|
||||||
|
|
||||||
options = [tags.Option(theme, value=theme) for theme in available]
|
options = [tags.Option(theme, value=theme) for theme in available]
|
||||||
renderer_globals['theme_picker_options'] = options
|
renderer_globals['theme_picker_options'] = options
|
||||||
|
|
||||||
# heck while we're assuming the classic web app here...
|
|
||||||
# (we don't want this to happen for the API either!)
|
|
||||||
# TODO: just..awful *shrug*
|
|
||||||
# note that we assume "simple" menus nowadays
|
|
||||||
if request.rattail_config.getbool('tailbone', 'menus.simple', default=True):
|
|
||||||
renderer_globals['menus'] = make_simple_menus(request)
|
|
||||||
|
|
||||||
# TODO: ugh, same deal here
|
# TODO: ugh, same deal here
|
||||||
renderer_globals['messaging_enabled'] = request.rattail_config.getbool(
|
renderer_globals['messaging_enabled'] = config.get_bool('tailbone.messaging.enabled',
|
||||||
'tailbone', 'messaging.enabled', default=False)
|
default=False)
|
||||||
|
|
||||||
# background color may be set per-request, by some apps
|
# background color may be set per-request, by some apps
|
||||||
if hasattr(request, 'background_color') and request.background_color:
|
if hasattr(request, 'background_color') and request.background_color:
|
||||||
renderer_globals['background_color'] = request.background_color
|
renderer_globals['background_color'] = request.background_color
|
||||||
else: # otherwise we use the one from config
|
else: # otherwise we use the one from config
|
||||||
renderer_globals['background_color'] = request.rattail_config.get(
|
renderer_globals['background_color'] = config.get('tailbone.background_color')
|
||||||
'tailbone', 'background_color')
|
|
||||||
|
|
||||||
# maybe set custom stylesheet
|
# maybe set custom stylesheet
|
||||||
css = None
|
css = None
|
||||||
if request.user:
|
if request.user:
|
||||||
css = request.rattail_config.get('tailbone.{}'.format(request.user.uuid),
|
css = config.get(f'tailbone.{request.user.uuid}', 'user_css')
|
||||||
'buefy_css')
|
if not css:
|
||||||
|
css = config.get(f'tailbone.{request.user.uuid}', 'buefy_css')
|
||||||
|
if css:
|
||||||
|
warnings.warn(f"setting 'tailbone.{request.user.uuid}.buefy_css' should be"
|
||||||
|
f"changed to 'tailbone.{request.user.uuid}.user_css'",
|
||||||
|
DeprecationWarning)
|
||||||
renderer_globals['user_css'] = css
|
renderer_globals['user_css'] = css
|
||||||
|
|
||||||
# add global search data for quick access
|
# add global search data for quick access
|
||||||
renderer_globals['global_search_data'] = get_global_search_options(request)
|
renderer_globals['global_search_data'] = get_global_search_options(request)
|
||||||
|
|
||||||
# here we globally declare widths for grid filter pseudo-columns
|
# here we globally declare widths for grid filter pseudo-columns
|
||||||
widths = request.rattail_config.get('tailbone', 'grids.filters.column_widths')
|
widths = config.get('tailbone.grids.filters.column_widths')
|
||||||
if widths:
|
if widths:
|
||||||
widths = widths.split(';')
|
widths = widths.split(';')
|
||||||
if len(widths) < 2:
|
if len(widths) < 2:
|
||||||
|
@ -200,7 +209,7 @@ def before_render(event):
|
||||||
renderer_globals['filter_verb_width'] = widths[1]
|
renderer_globals['filter_verb_width'] = widths[1]
|
||||||
|
|
||||||
# declare global support for websockets, or lack thereof
|
# declare global support for websockets, or lack thereof
|
||||||
renderer_globals['expose_websockets'] = should_expose_websockets(rattail_config)
|
renderer_globals['expose_websockets'] = should_expose_websockets(config)
|
||||||
|
|
||||||
|
|
||||||
def add_inbox_count(event):
|
def add_inbox_count(event):
|
||||||
|
@ -214,8 +223,9 @@ def add_inbox_count(event):
|
||||||
request = event.get('request') or threadlocal.get_current_request()
|
request = event.get('request') or threadlocal.get_current_request()
|
||||||
if request.user:
|
if request.user:
|
||||||
renderer_globals = event
|
renderer_globals = event
|
||||||
|
app = request.rattail_config.get_app()
|
||||||
|
model = app.model
|
||||||
enum = request.rattail_config.get_enum()
|
enum = request.rattail_config.get_enum()
|
||||||
model = request.rattail_config.get_model()
|
|
||||||
renderer_globals['inbox_count'] = Session.query(model.Message)\
|
renderer_globals['inbox_count'] = Session.query(model.Message)\
|
||||||
.outerjoin(model.MessageRecipient)\
|
.outerjoin(model.MessageRecipient)\
|
||||||
.filter(model.MessageRecipient.recipient == Session.merge(request.user))\
|
.filter(model.MessageRecipient.recipient == Session.merge(request.user))\
|
||||||
|
@ -229,27 +239,10 @@ def context_found(event):
|
||||||
|
|
||||||
The following is attached to the request:
|
The following is attached to the request:
|
||||||
|
|
||||||
* ``get_referrer()`` function
|
|
||||||
|
|
||||||
* ``get_session_timeout()`` function
|
* ``get_session_timeout()`` function
|
||||||
"""
|
"""
|
||||||
request = event.request
|
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():
|
def get_session_timeout():
|
||||||
"""
|
"""
|
||||||
Returns the timeout in effect for the current session
|
Returns the timeout in effect for the current session
|
||||||
|
|
|
@ -1,241 +1,2 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/configure.mako" />
|
<%inherit file="wuttaweb:templates/appinfo/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)">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
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
|
|
||||||
:active.sync="editWebLibraryShowDialog">
|
|
||||||
<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">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Effective URL (as of last page load)">
|
|
||||||
<b-input v-model="editWebLibraryRecord.live_url"
|
|
||||||
disabled>
|
|
||||||
</b-input>
|
|
||||||
</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()}
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/master/index.mako" />
|
<%inherit file="wuttaweb:templates/appinfo/index.mako" />
|
||||||
|
|
||||||
<%def name="render_grid_component()">
|
|
||||||
|
|
||||||
|
<%def name="page_content()">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
|
||||||
<once-button type="is-primary"
|
<once-button type="is-primary"
|
||||||
|
@ -28,98 +27,5 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-collapse class="panel" open>
|
${parent.page_content()}
|
||||||
|
|
||||||
<template #trigger="props">
|
|
||||||
<div class="panel-heading"
|
|
||||||
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 (style: ${request.rattail_config._style})</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"
|
|
||||||
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>
|
|
||||||
</%def>
|
</%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()}
|
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
<app-settings :groups="groups" :showing-group="showingGroup"></app-settings>
|
<app-settings :groups="groups" :showing-group="showingGroup"></app-settings>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_this_page_template()">
|
<%def name="render_vue_templates()">
|
||||||
${parent.render_this_page_template()}
|
${parent.render_vue_templates()}
|
||||||
<script type="text/x-template" id="app-settings-template">
|
<script type="text/x-template" id="app-settings-template">
|
||||||
|
|
||||||
<div class="form">
|
<div class="form">
|
||||||
|
@ -150,19 +150,18 @@
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%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.groups = ${json.dumps(settings_data)|n}
|
||||||
ThisPageData.showingGroup = ${json.dumps(current_group or '')|n}
|
ThisPageData.showingGroup = ${json.dumps(current_group or '')|n}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="make_this_page_component()">
|
<%def name="make_vue_components()">
|
||||||
${parent.make_this_page_component()}
|
${parent.make_vue_components()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
Vue.component('app-settings', {
|
Vue.component('app-settings', {
|
||||||
template: '#app-settings-template',
|
template: '#app-settings-template',
|
||||||
|
@ -193,6 +192,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
|
<%namespace file="/wutta-components.mako" import="make_wutta_components" />
|
||||||
<%namespace file="/grids/nav.mako" import="grid_index_nav" />
|
<%namespace file="/grids/nav.mako" import="grid_index_nav" />
|
||||||
<%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" />
|
<%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" />
|
||||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
<%namespace name="base_meta" file="/base_meta.mako" />
|
||||||
<%namespace file="/formposter.mako" import="declare_formposter_mixin" />
|
<%namespace file="/formposter.mako" import="declare_formposter_mixin" />
|
||||||
|
<%namespace file="/grids/filter-components.mako" import="make_grid_filter_components" />
|
||||||
<%namespace name="page_help" file="/page_help.mako" />
|
<%namespace name="page_help" file="/page_help.mako" />
|
||||||
<%namespace name="multi_file_upload" file="/multi_file_upload.mako" />
|
<%namespace name="multi_file_upload" file="/multi_file_upload.mako" />
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
@ -33,17 +35,21 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
${declare_formposter_mixin()}
|
<div id="app" style="height: 100%;">
|
||||||
|
|
||||||
${self.body()}
|
|
||||||
|
|
||||||
<div id="whole-page-app">
|
|
||||||
<whole-page></whole-page>
|
<whole-page></whole-page>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${self.render_whole_page_template()}
|
## TODO: this must come before the self.body() call..but why?
|
||||||
${self.make_whole_page_component()}
|
${declare_formposter_mixin()}
|
||||||
${self.make_whole_page_app()}
|
|
||||||
|
## 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@ -90,7 +96,6 @@
|
||||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.numericinput.js') + '?ver={}'.format(tailbone.__version__))}
|
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.numericinput.js') + '?ver={}'.format(tailbone.__version__))}
|
||||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.oncebutton.js') + '?ver={}'.format(tailbone.__version__))}
|
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.oncebutton.js') + '?ver={}'.format(tailbone.__version__))}
|
||||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + '?ver={}'.format(tailbone.__version__))}
|
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + '?ver={}'.format(tailbone.__version__))}
|
||||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.grid.js') + '?ver={}'.format(tailbone.__version__))}
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
@ -122,16 +127,16 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="vuejs()">
|
<%def name="vuejs()">
|
||||||
${h.javascript_link(h.get_liburl(request, 'vue'))}
|
${h.javascript_link(h.get_liburl(request, 'vue', prefix='tailbone'))}
|
||||||
${h.javascript_link(h.get_liburl(request, 'vue_resource'))}
|
${h.javascript_link(h.get_liburl(request, 'vue_resource', prefix='tailbone'))}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="buefy()">
|
<%def name="buefy()">
|
||||||
${h.javascript_link(h.get_liburl(request, 'buefy'))}
|
${h.javascript_link(h.get_liburl(request, 'buefy', prefix='tailbone'))}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="fontawesome()">
|
<%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>
|
||||||
|
|
||||||
<%def name="extra_javascript()"></%def>
|
<%def name="extra_javascript()"></%def>
|
||||||
|
@ -151,13 +156,18 @@
|
||||||
${h.stylesheet_link(request.static_url('tailbone:static/css/codehilite.css') + '?ver={}'.format(tailbone.__version__))}
|
${h.stylesheet_link(request.static_url('tailbone:static/css/codehilite.css') + '?ver={}'.format(tailbone.__version__))}
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
.filters .filter-fieldname {
|
.filters .filter-fieldname,
|
||||||
|
.filters .filter-fieldname .button {
|
||||||
|
% if filter_fieldname_width is not Undefined:
|
||||||
min-width: ${filter_fieldname_width};
|
min-width: ${filter_fieldname_width};
|
||||||
|
% endif
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
}
|
}
|
||||||
|
% if filter_fieldname_width is not Undefined:
|
||||||
.filters .filter-verb {
|
.filters .filter-verb {
|
||||||
min-width: ${filter_verb_width};
|
min-width: ${filter_verb_width};
|
||||||
}
|
}
|
||||||
|
% endif
|
||||||
</style>
|
</style>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
@ -166,15 +176,17 @@
|
||||||
${h.stylesheet_link(user_css)}
|
${h.stylesheet_link(user_css)}
|
||||||
% else:
|
% else:
|
||||||
## upstream Buefy CSS
|
## upstream Buefy CSS
|
||||||
${h.stylesheet_link(h.get_liburl(request, 'buefy.css'))}
|
${h.stylesheet_link(h.get_liburl(request, 'buefy.css', prefix='tailbone'))}
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="extra_styles()"></%def>
|
<%def name="extra_styles()">
|
||||||
|
${base_meta.extra_styles()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="head_tags()"></%def>
|
<%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">
|
<script type="text/x-template" id="whole-page-template">
|
||||||
<div>
|
<div>
|
||||||
<header>
|
<header>
|
||||||
|
@ -273,7 +285,7 @@
|
||||||
<span class="header-text">
|
<span class="header-text">
|
||||||
${index_title}
|
${index_title}
|
||||||
</span>
|
</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"
|
<once-button type="is-primary"
|
||||||
tag="a" href="${url('{}.create'.format(route_prefix))}"
|
tag="a" href="${url('{}.create'.format(route_prefix))}"
|
||||||
icon-left="plus"
|
icon-left="plus"
|
||||||
|
@ -299,7 +311,7 @@
|
||||||
<span class="header-text">
|
<span class="header-text">
|
||||||
${h.link_to(instance_title, instance_url)}
|
${h.link_to(instance_title, instance_url)}
|
||||||
</span>
|
</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'):
|
% if not request.matched_route.name.endswith('.create'):
|
||||||
<once-button type="is-primary"
|
<once-button type="is-primary"
|
||||||
tag="a" href="${url('{}.create'.format(route_prefix))}"
|
tag="a" href="${url('{}.create'.format(route_prefix))}"
|
||||||
|
@ -398,6 +410,7 @@
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
${h.form(url('change_theme'), method="post", ref='themePickerForm')}
|
${h.form(url('change_theme'), method="post", ref='themePickerForm')}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
|
<input type="hidden" name="referrer" :value="referrer" />
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
<span>Theme:</span>
|
<span>Theme:</span>
|
||||||
<b-select name="theme"
|
<b-select name="theme"
|
||||||
|
@ -510,7 +523,7 @@
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
@click="showFeedback()"
|
@click="showFeedback()"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="fas fa-comment">
|
icon-left="comment">
|
||||||
Feedback
|
Feedback
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -619,9 +632,23 @@
|
||||||
% endif
|
% endif
|
||||||
<div class="navbar-dropdown">
|
<div class="navbar-dropdown">
|
||||||
% if request.is_root:
|
% 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:
|
% 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
|
% endif
|
||||||
% if messaging_enabled:
|
% if messaging_enabled:
|
||||||
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
|
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
|
||||||
|
@ -629,7 +656,11 @@
|
||||||
% if request.is_root or not request.user.prevent_password_change:
|
% if request.is_root or not request.user.prevent_password_change:
|
||||||
${h.link_to("Change Password", url('change_password'), class_='navbar-item')}
|
${h.link_to("Change Password", url('change_password'), class_='navbar-item')}
|
||||||
% endif
|
% 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')}
|
${h.link_to("Logout", url('logout'), class_='navbar-item')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -650,19 +681,19 @@
|
||||||
## TODO: is there a better way to check if viewing parent?
|
## TODO: is there a better way to check if viewing parent?
|
||||||
% if parent_instance is Undefined:
|
% if parent_instance is Undefined:
|
||||||
% if master.editable and instance_editable and master.has_perm('edit'):
|
% 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"
|
icon-left="edit"
|
||||||
text="Edit This">
|
text="Edit This">
|
||||||
</once-button>
|
</once-button>
|
||||||
% endif
|
% endif
|
||||||
% if master.cloneable and master.has_perm('clone'):
|
% if getattr(master, 'cloneable', False) and not master.cloning and master.has_perm('clone'):
|
||||||
<once-button tag="a" href="${action_url('clone', instance)}"
|
<once-button tag="a" href="${master.get_action_url('clone', instance)}"
|
||||||
icon-left="object-ungroup"
|
icon-left="object-ungroup"
|
||||||
text="Clone This">
|
text="Clone This">
|
||||||
</once-button>
|
</once-button>
|
||||||
% endif
|
% endif
|
||||||
% if master.deletable and instance_deletable and master.has_perm('delete'):
|
% 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"
|
type="is-danger"
|
||||||
icon-left="trash"
|
icon-left="trash"
|
||||||
text="Delete This">
|
text="Delete This">
|
||||||
|
@ -671,7 +702,7 @@
|
||||||
% else:
|
% else:
|
||||||
## viewing row
|
## viewing row
|
||||||
% if instance_deletable and master.has_perm('delete_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"
|
type="is-danger"
|
||||||
icon-left="trash"
|
icon-left="trash"
|
||||||
text="Delete This">
|
text="Delete This">
|
||||||
|
@ -680,13 +711,13 @@
|
||||||
% endif
|
% endif
|
||||||
% elif master and master.editing:
|
% elif master and master.editing:
|
||||||
% if master.viewable and master.has_perm('view'):
|
% 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"
|
icon-left="eye"
|
||||||
text="View This">
|
text="View This">
|
||||||
</once-button>
|
</once-button>
|
||||||
% endif
|
% endif
|
||||||
% if master.deletable and instance_deletable and master.has_perm('delete'):
|
% 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"
|
type="is-danger"
|
||||||
icon-left="trash"
|
icon-left="trash"
|
||||||
text="Delete This">
|
text="Delete This">
|
||||||
|
@ -694,13 +725,13 @@
|
||||||
% endif
|
% endif
|
||||||
% elif master and master.deleting:
|
% elif master and master.deleting:
|
||||||
% if master.viewable and master.has_perm('view'):
|
% 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"
|
icon-left="eye"
|
||||||
text="View This">
|
text="View This">
|
||||||
</once-button>
|
</once-button>
|
||||||
% endif
|
% endif
|
||||||
% if master.editable and instance_editable and master.has_perm('edit'):
|
% 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"
|
icon-left="edit"
|
||||||
text="Edit This">
|
text="Edit This">
|
||||||
</once-button>
|
</once-button>
|
||||||
|
@ -741,11 +772,8 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="declare_whole_page_vars()">
|
<%def name="render_vue_script_whole_page()">
|
||||||
${page_help.declare_vars()}
|
<script>
|
||||||
${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">
|
|
||||||
|
|
||||||
let WholePage = {
|
let WholePage = {
|
||||||
template: '#whole-page-template',
|
template: '#whole-page-template',
|
||||||
|
@ -852,7 +880,8 @@
|
||||||
feedbackMessage: "",
|
feedbackMessage: "",
|
||||||
|
|
||||||
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
|
% 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
|
% endif
|
||||||
|
|
||||||
% if can_edit_help:
|
% if can_edit_help:
|
||||||
|
@ -861,7 +890,7 @@
|
||||||
|
|
||||||
globalSearchActive: false,
|
globalSearchActive: false,
|
||||||
globalSearchTerm: '',
|
globalSearchTerm: '',
|
||||||
globalSearchData: ${json.dumps(global_search_data)|n},
|
globalSearchData: ${json.dumps(global_search_data or [])|n},
|
||||||
|
|
||||||
mountedHooks: [],
|
mountedHooks: [],
|
||||||
}
|
}
|
||||||
|
@ -880,54 +909,6 @@
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%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(six.text_type(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()">
|
|
||||||
${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)">
|
<%def name="wtfield(form, name, **kwargs)">
|
||||||
<div class="field-wrapper${' error' if form[name].errors else ''}">
|
<div class="field-wrapper${' error' if form[name].errors else ''}">
|
||||||
<label for="${name}">${form[name].label}</label>
|
<label for="${name}">${form[name].label}</label>
|
||||||
|
@ -949,3 +930,88 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%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>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="wuttaweb:templates/base_meta.mako" />
|
||||||
|
|
||||||
<%def name="app_title()">${request.rattail_config.node_title(default="Rattail")}</%def>
|
<%def name="app_title()">${app.get_node_title()}</%def>
|
||||||
|
|
||||||
<%def name="global_title()">${"[STAGE] " if not request.rattail_config.production() else ''}${self.app_title()}</%def>
|
|
||||||
|
|
||||||
<%def name="favicon()">
|
<%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'))}" />
|
<link rel="icon" type="image/x-icon" href="${request.rattail_config.get('tailbone', 'favicon_url', default=request.static_url('tailbone:static/img/rattail.ico'))}" />
|
||||||
|
@ -11,9 +10,3 @@
|
||||||
<%def name="header_logo()">
|
<%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;")}
|
${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>
|
||||||
|
|
||||||
<%def name="footer()">
|
|
||||||
<p class="has-text-centered">
|
|
||||||
powered by ${h.link_to("Rattail", url('about'))}
|
|
||||||
</p>
|
|
||||||
</%def>
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
:disabled="refreshResultsButtonDisabled"
|
:disabled="refreshResultsButtonDisabled"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="fas fa-redo"
|
icon-left="redo"
|
||||||
@click="refreshResults()">
|
@click="refreshResults()">
|
||||||
{{ refreshResultsButtonText }}
|
{{ refreshResultsButtonText }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
<br />
|
<br />
|
||||||
<div class="form-wrapper">
|
<div class="form-wrapper">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<${execute_form.component} ref="executeResultsForm"></${execute_form.component}>
|
${execute_form.render_vue_tag(ref='executeResultsForm')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -64,10 +64,17 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="render_vue_templates()">
|
||||||
${parent.modify_this_page_vars()}
|
${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'):
|
% if master.results_refreshable and master.has_perm('refresh'):
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
TailboneGridData.refreshResultsButtonText = "Refresh Results"
|
TailboneGridData.refreshResultsButtonText = "Refresh Results"
|
||||||
TailboneGridData.refreshResultsButtonDisabled = false
|
TailboneGridData.refreshResultsButtonDisabled = false
|
||||||
|
@ -81,9 +88,9 @@
|
||||||
</script>
|
</script>
|
||||||
% endif
|
% endif
|
||||||
% if master.results_executable and master.has_perm('execute_multiple'):
|
% 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()
|
this.$refs.actualExecuteForm.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,25 +125,9 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="make_this_page_component()">
|
<%def name="make_vue_components()">
|
||||||
${parent.make_this_page_component()}
|
${parent.make_vue_components()}
|
||||||
% if master.results_executable and master.has_perm('execute_multiple'):
|
% if master.results_executable and master.has_perm('execute_multiple'):
|
||||||
<script type="text/javascript">
|
${execute_form.render_vue_finalize()}
|
||||||
|
|
||||||
${execute_form.component_studly}.data = function() { return ${execute_form.component_studly}Data }
|
|
||||||
|
|
||||||
Vue.component('${execute_form.component}', ${execute_form.component_studly})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%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()}
|
|
||||||
|
|
|
@ -147,7 +147,7 @@
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
let ${form.component_studly} = {
|
let ${form.vue_component} = {
|
||||||
template: '#${form.component}-template',
|
template: '#${form.component}-template',
|
||||||
mixins: [SimpleRequestMixin],
|
mixins: [SimpleRequestMixin],
|
||||||
|
|
||||||
|
@ -278,7 +278,7 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
let ${form.component_studly}Data = {
|
let ${form.vue_component}Data = {
|
||||||
submitting: false,
|
submitting: false,
|
||||||
|
|
||||||
productUPC: null,
|
productUPC: null,
|
||||||
|
@ -297,14 +297,9 @@
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
ThisPageData.toggleCompleteSubmitting = false
|
ThisPageData.toggleCompleteSubmitting = false
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/batch/view.mako" />
|
<%inherit file="/batch/view.mako" />
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
${form.vue_component}Data.taxesData = ${json.dumps(taxes_data)|n}
|
||||||
${form.component_studly}Data.taxesData = ${json.dumps(taxes_data)|n}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -39,14 +39,9 @@
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
ThisPageData.catalogParsers = ${json.dumps(catalog_parsers_data)|n}
|
ThisPageData.catalogParsers = ${json.dumps(catalog_parsers_data)|n}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/batch/create.mako" />
|
<%inherit file="/batch/create.mako" />
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<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.vue_component}Data.vendorName = null
|
||||||
${form.component_studly}Data.vendorNameReplacement = 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]
|
let parser = this.parsers[val]
|
||||||
if (parser.vendor_uuid) {
|
if (parser.vendor_uuid) {
|
||||||
if (this.field_model_vendor_uuid != 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
|
this.vendorNameReplacement = label
|
||||||
}
|
}
|
||||||
|
|
||||||
${form.component_studly}.methods.vendorChanged = function(uuid) {
|
${form.vue_component}.methods.vendorChanged = function(uuid) {
|
||||||
if (uuid) {
|
if (uuid) {
|
||||||
this.vendorName = this.vendorNameReplacement
|
this.vendorName = this.vendorNameReplacement
|
||||||
this.vendorNameReplacement = null
|
this.vendorNameReplacement = null
|
||||||
|
@ -37,6 +37,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -50,12 +50,12 @@
|
||||||
<b-button tag="a"
|
<b-button tag="a"
|
||||||
href="${master.get_action_url('download_worksheet', batch)}"
|
href="${master.get_action_url('download_worksheet', batch)}"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="fas fa-download">
|
icon-left="download">
|
||||||
Download Worksheet
|
Download Worksheet
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="fas fa-upload"
|
icon-left="upload"
|
||||||
@click="$emit('show-upload')">
|
@click="$emit('show-upload')">
|
||||||
Upload Worksheet
|
Upload Worksheet
|
||||||
</b-button>
|
</b-button>
|
||||||
|
@ -68,28 +68,28 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_status_breakdown()">
|
<%def name="render_status_breakdown()">
|
||||||
<div class="object-helper">
|
<nav class="panel">
|
||||||
<h3>Row Status Breakdown</h3>
|
<p class="panel-heading">Row Status</p>
|
||||||
<div class="object-helper-content">
|
<div class="panel-block">
|
||||||
${status_breakdown_grid}
|
<div style="width: 100%;">
|
||||||
|
${status_breakdown_grid}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_execute_helper()">
|
<%def name="render_execute_helper()">
|
||||||
<div class="object-helper">
|
<nav class="panel">
|
||||||
<h3>Batch Execution</h3>
|
<p class="panel-heading">Execution</p>
|
||||||
<div class="object-helper-content">
|
<div class="panel-block">
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
% if batch.executed:
|
% if batch.executed:
|
||||||
<p>
|
<p>
|
||||||
Batch was executed
|
|
||||||
${h.pretty_datetime(request.rattail_config, batch.executed)}
|
${h.pretty_datetime(request.rattail_config, batch.executed)}
|
||||||
by ${batch.executed_by}
|
by ${batch.executed_by}
|
||||||
</p>
|
</p>
|
||||||
% elif master.handler.executable(batch):
|
% elif master.handler.executable(batch):
|
||||||
% if master.has_perm('execute'):
|
% if master.has_perm('execute'):
|
||||||
<p>Batch has not yet been executed.</p>
|
|
||||||
<br />
|
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
% if not execute_enabled:
|
% if not execute_enabled:
|
||||||
disabled
|
disabled
|
||||||
|
@ -119,8 +119,7 @@
|
||||||
<div class="markdown">
|
<div class="markdown">
|
||||||
${execution_described|n}
|
${execution_described|n}
|
||||||
</div>
|
</div>
|
||||||
<${execute_form.component} ref="executeBatchForm">
|
${execute_form.render_vue_tag(ref='executeBatchForm')}
|
||||||
</${execute_form.component}>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
|
@ -144,14 +143,9 @@
|
||||||
% else:
|
% else:
|
||||||
<p>TODO: batch cannot be executed..?</p>
|
<p>TODO: batch cannot be executed..?</p>
|
||||||
% endif
|
% endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<%def name="render_this_page()">
|
<%def name="render_this_page()">
|
||||||
|
@ -173,8 +167,7 @@
|
||||||
Please be certain to use the right one!
|
Please be certain to use the right one!
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<${upload_worksheet_form.component} ref="uploadForm">
|
${upload_worksheet_form.render_vue_tag(ref='uploadForm')}
|
||||||
</${upload_worksheet_form.component}>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
|
@ -184,7 +177,7 @@
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
@click="submitUpload()"
|
@click="submitUpload()"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="fas fa-upload"
|
icon-left="upload"
|
||||||
:disabled="uploadButtonDisabled">
|
:disabled="uploadButtonDisabled">
|
||||||
{{ uploadButtonText }}
|
{{ uploadButtonText }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
@ -196,16 +189,6 @@
|
||||||
|
|
||||||
</%def>
|
</%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()">
|
<%def name="render_form()">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<${form.component} @show-upload="showUploadDialog = true">
|
<${form.component} @show-upload="showUploadDialog = true">
|
||||||
|
@ -266,9 +249,27 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="render_vue_templates()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.render_vue_templates()}
|
||||||
<script type="text/javascript">
|
% 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}
|
ThisPageData.statusBreakdownData = ${json.dumps(status_breakdown_data)|n}
|
||||||
|
|
||||||
|
@ -284,7 +285,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
% if not batch.executed and master.has_perm('edit'):
|
% if not batch.executed and master.has_perm('edit'):
|
||||||
${form.component_studly}Data.togglingBatchComplete = false
|
${form.vue_component}Data.togglingBatchComplete = false
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'):
|
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'):
|
||||||
|
@ -305,7 +306,7 @@
|
||||||
form.submit()
|
form.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
${upload_worksheet_form.component_studly}.methods.submit = function() {
|
${upload_worksheet_form.vue_component}.methods.submit = function() {
|
||||||
this.$refs.actualUploadForm.submit()
|
this.$refs.actualUploadForm.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,7 +321,7 @@
|
||||||
this.$refs.executeBatchForm.submit()
|
this.$refs.executeBatchForm.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
${execute_form.component_studly}.methods.submit = function() {
|
${execute_form.vue_component}.methods.submit = function() {
|
||||||
this.$refs.actualExecuteForm.submit()
|
this.$refs.actualExecuteForm.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,9 +329,9 @@
|
||||||
|
|
||||||
% if master.rows_bulk_deletable and not batch.executed and master.has_perm('delete_rows'):
|
% 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
|
this.deleteResultsShowDialog = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,28 +340,12 @@
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="make_this_page_component()">
|
<%def name="make_vue_components()">
|
||||||
${parent.make_this_page_component()}
|
${parent.make_vue_components()}
|
||||||
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'):
|
% if master.has_worksheet_file and master.allow_worksheet(batch) and master.has_perm('worksheet'):
|
||||||
<script type="text/javascript">
|
${upload_worksheet_form.render_vue_finalize()}
|
||||||
|
|
||||||
## 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>
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if execute_enabled and master.has_perm('execute'):
|
% if execute_enabled and master.has_perm('execute'):
|
||||||
<script type="text/javascript">
|
${execute_form.render_vue_finalize()}
|
||||||
|
|
||||||
## ExecuteForm
|
|
||||||
${execute_form.component_studly}.data = function() { return ${execute_form.component_studly}Data }
|
|
||||||
Vue.component('${execute_form.component}', ${execute_form.component_studly})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -208,9 +208,9 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
ThisPageData.menuSequence = ${json.dumps([m['key'] for m in menus])|n}
|
ThisPageData.menuSequence = ${json.dumps([m['key'] for m in menus])|n}
|
||||||
|
|
||||||
|
@ -443,6 +443,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
<b-select name="${tmpl['setting_file']}"
|
<b-select name="${tmpl['setting_file']}"
|
||||||
v-model="inputFileTemplateSettings['${tmpl['setting_file']}']"
|
v-model="inputFileTemplateSettings['${tmpl['setting_file']}']"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true">
|
||||||
<option :value="null">-new-</option>
|
<option value="">-new-</option>
|
||||||
<option v-for="option in inputFileTemplateFileOptions['${tmpl['key']}']"
|
<option v-for="option in inputFileTemplateFileOptions['${tmpl['key']}']"
|
||||||
:key="option"
|
:key="option"
|
||||||
:value="option">
|
:value="option">
|
||||||
|
@ -104,22 +104,40 @@
|
||||||
<b-field label="Upload"
|
<b-field label="Upload"
|
||||||
v-show="inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !inputFileTemplateSettings['${tmpl['setting_file']}']">
|
v-show="inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !inputFileTemplateSettings['${tmpl['setting_file']}']">
|
||||||
|
|
||||||
<b-field class="file is-primary"
|
% if request.use_oruga:
|
||||||
:class="{'has-name': !!inputFileTemplateSettings['${tmpl['setting_file']}']}">
|
<o-field class="file">
|
||||||
<b-upload name="${tmpl['setting_file']}.upload"
|
<o-upload name="${tmpl['setting_file']}.upload"
|
||||||
v-model="inputFileTemplateUploads['${tmpl['key']}']"
|
v-model="inputFileTemplateUploads['${tmpl['key']}']"
|
||||||
class="file-label"
|
v-slot="{ onclick }"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true">
|
||||||
<span class="file-cta">
|
<o-button variant="primary"
|
||||||
<b-icon class="file-icon" pack="fas" icon="upload"></b-icon>
|
@click="onclick">
|
||||||
<span class="file-label">Click to upload</span>
|
<o-icon icon="upload" />
|
||||||
</span>
|
<span>Click to upload</span>
|
||||||
</b-upload>
|
</o-button>
|
||||||
<span v-if="inputFileTemplateUploads['${tmpl['key']}']"
|
<span class="file-name" v-if="inputFileTemplateUploads['${tmpl['key']}']">
|
||||||
class="file-name">
|
{{ inputFileTemplateUploads['${tmpl['key']}'].name }}
|
||||||
{{ inputFileTemplateUploads['${tmpl['key']}'].name }}
|
</span>
|
||||||
</span>
|
</o-upload>
|
||||||
</b-field>
|
</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>
|
</b-field>
|
||||||
|
|
||||||
|
@ -143,6 +161,85 @@
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%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="form_content()"></%def>
|
||||||
|
|
||||||
<%def name="page_content()">
|
<%def name="page_content()">
|
||||||
|
@ -183,15 +280,14 @@
|
||||||
<b-button @click="purgeSettingsShowDialog = false">
|
<b-button @click="purgeSettingsShowDialog = false">
|
||||||
Cancel
|
Cancel
|
||||||
</b-button>
|
</b-button>
|
||||||
${h.form(request.current_route_url())}
|
${h.form(request.current_route_url(), **{'@submit': 'purgingSettings = true'})}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
${h.hidden('remove_settings', 'true')}
|
${h.hidden('remove_settings', 'true')}
|
||||||
<b-button type="is-danger"
|
<b-button type="is-danger"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
:disabled="purgingSettings"
|
:disabled="purgingSettings"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="trash"
|
icon-left="trash">
|
||||||
@click="purgingSettings = true">
|
|
||||||
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }}
|
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }}
|
||||||
</b-button>
|
</b-button>
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
|
@ -205,62 +301,42 @@
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
% if simple_settings is not Undefined:
|
% if simple_settings is not Undefined:
|
||||||
ThisPageData.simpleSettings = ${json.dumps(simple_settings)|n}
|
ThisPageData.simpleSettings = ${json.dumps(simple_settings)|n}
|
||||||
% endif
|
% 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.purgeSettingsShowDialog = false
|
||||||
ThisPageData.purgingSettings = false
|
ThisPageData.purgingSettings = false
|
||||||
|
|
||||||
ThisPageData.settingsNeedSaved = false
|
ThisPageData.settingsNeedSaved = false
|
||||||
ThisPageData.undoChanges = false
|
ThisPageData.undoChanges = false
|
||||||
ThisPageData.savingSettings = false
|
ThisPageData.savingSettings = false
|
||||||
|
ThisPageData.validators = []
|
||||||
|
|
||||||
ThisPage.methods.purgeSettingsInit = function() {
|
ThisPage.methods.purgeSettingsInit = function() {
|
||||||
this.purgeSettingsShowDialog = true
|
this.purgeSettingsShowDialog = true
|
||||||
}
|
}
|
||||||
|
|
||||||
% if input_file_template_settings is not Undefined:
|
ThisPage.methods.validateSettings = function() {}
|
||||||
ThisPage.methods.validateInputFileTemplateSettings = function() {
|
|
||||||
% for tmpl in six.itervalues(input_file_templates):
|
|
||||||
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.saveSettings = 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) {
|
if (msg) {
|
||||||
alert(msg)
|
alert(msg)
|
||||||
return
|
return
|
||||||
|
@ -291,8 +367,65 @@
|
||||||
window.addEventListener('beforeunload', this.beforeWindowUnload)
|
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>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -88,9 +88,9 @@
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
ThisPage.methods.getLabelForKey = function(key) {
|
ThisPage.methods.getLabelForKey = function(key) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
|
@ -111,6 +111,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -106,9 +106,9 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
ThisPageData.resolvePersonShowDialog = false
|
ThisPageData.resolvePersonShowDialog = false
|
||||||
ThisPageData.resolvePersonUUID = null
|
ThisPageData.resolvePersonUUID = null
|
||||||
|
@ -139,5 +139,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -16,15 +16,15 @@
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
% if expose_shoppers:
|
% if expose_shoppers:
|
||||||
${form.component_studly}Data.shoppers = ${json.dumps(shoppers_data)|n}
|
${form.vue_component}Data.shoppers = ${json.dumps(shoppers_data)|n}
|
||||||
% endif
|
% endif
|
||||||
% if expose_people:
|
% if expose_people:
|
||||||
${form.component_studly}Data.peopleData = ${json.dumps(people_data)|n}
|
${form.vue_component}Data.peopleData = ${json.dumps(people_data)|n}
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
ThisPage.methods.detachPerson = function(url) {
|
ThisPage.methods.detachPerson = function(url) {
|
||||||
|
@ -36,5 +36,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -24,29 +24,38 @@
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field message="Only applies if user is allowed to choose contact info.">
|
<div v-show="simpleSettings['rattail.custorders.new_orders.allow_contact_info_choice']"
|
||||||
<b-checkbox name="rattail.custorders.new_orders.allow_contact_info_create"
|
style="padding-left: 2rem;">
|
||||||
v-model="simpleSettings['rattail.custorders.new_orders.allow_contact_info_create']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
Allow user to enter new contact info
|
|
||||||
</b-checkbox>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<p class="block">
|
<b-field message="Only applies if user is allowed to choose contact info.">
|
||||||
If you allow users to enter new contact info, the default action
|
<b-checkbox name="rattail.custorders.new_orders.allow_contact_info_create"
|
||||||
when the order is submitted, is to send email with details of
|
v-model="simpleSettings['rattail.custorders.new_orders.allow_contact_info_create']"
|
||||||
the new contact info. Settings for these are at:
|
native-value="true"
|
||||||
</p>
|
@input="settingsNeedSaved = true">
|
||||||
|
Allow user to enter new contact info
|
||||||
|
</b-checkbox>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
<ul class="list">
|
<div v-show="simpleSettings['rattail.custorders.new_orders.allow_contact_info_create']"
|
||||||
<li class="list-item">
|
style="padding-left: 2rem;">
|
||||||
${h.link_to("New Phone Request", url('emailprofiles.view', key='new_phone_requested'))}
|
|
||||||
</li>
|
<p class="block">
|
||||||
<li class="list-item">
|
If you allow users to enter new contact info, the default action
|
||||||
${h.link_to("New Email Request", url('emailprofiles.view', key='new_email_requested'))}
|
when the order is submitted, is to send email with details of
|
||||||
</li>
|
the new contact info. Settings for these are at:
|
||||||
</ul>
|
</p>
|
||||||
|
|
||||||
|
<ul class="list">
|
||||||
|
<li class="list-item">
|
||||||
|
${h.link_to("New Phone Request", url('emailprofiles.view', key='new_phone_requested'))}
|
||||||
|
</li>
|
||||||
|
<li class="list-item">
|
||||||
|
${h.link_to("New Email Request", url('emailprofiles.view', key='new_email_requested'))}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="block is-size-3">Product Handling</h3>
|
<h3 class="block is-size-3">Product Handling</h3>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -291,11 +291,11 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<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'):
|
% if master.has_perm('confirm_price'):
|
||||||
|
|
||||||
|
@ -347,7 +347,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ThisPageData.orderItemStatuses = ${json.dumps(enum.CUSTORDER_ITEM_STATUS)|n}
|
ThisPageData.orderItemStatuses = ${json.dumps(enum.CUSTORDER_ITEM_STATUS)|n}
|
||||||
ThisPageData.orderItemStatusOptions = ${json.dumps([dict(key=k, label=v) for k, v in six.iteritems(enum.CUSTORDER_ITEM_STATUS)])|n}
|
ThisPageData.orderItemStatusOptions = ${json.dumps([dict(key=k, label=v) for k, v in enum.CUSTORDER_ITEM_STATUS.items()])|n}
|
||||||
|
|
||||||
ThisPageData.oldStatusCode = ${instance.status_code}
|
ThisPageData.oldStatusCode = ${instance.status_code}
|
||||||
|
|
||||||
|
@ -392,9 +392,9 @@
|
||||||
this.$refs.changeStatusForm.submit()
|
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
|
this.changeFlaggedSubmitting = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,5 +448,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/master/index.mako" />
|
<%inherit file="/master/index.mako" />
|
||||||
|
|
||||||
<%def name="context_menu_items()">
|
|
||||||
${parent.context_menu_items()}
|
|
||||||
% if request.has_perm('datasync.status'):
|
|
||||||
<li>${h.link_to("View DataSync Status", url('datasync.status'))}</li>
|
|
||||||
% endif
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="grid_tools()">
|
<%def name="grid_tools()">
|
||||||
${parent.grid_tools()}
|
${parent.grid_tools()}
|
||||||
|
|
||||||
|
@ -33,9 +26,9 @@
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
% if request.has_perm('datasync.restart'):
|
% if request.has_perm('datasync.restart'):
|
||||||
TailboneGridData.restartDatasyncFormSubmitting = false
|
TailboneGridData.restartDatasyncFormSubmitting = false
|
||||||
|
@ -57,6 +50,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/configure.mako" />
|
<%inherit file="/configure.mako" />
|
||||||
|
|
||||||
|
<%def name="extra_styles()">
|
||||||
|
${parent.extra_styles()}
|
||||||
|
<style>
|
||||||
|
.invisible-watcher {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="buttons_row()">
|
<%def name="buttons_row()">
|
||||||
<div class="level">
|
<div class="level">
|
||||||
<div class="level-left">
|
<div class="level-left">
|
||||||
|
@ -48,7 +57,12 @@
|
||||||
${h.hidden('profiles', **{':value': 'JSON.stringify(profilesData)'})}
|
${h.hidden('profiles', **{':value': 'JSON.stringify(profilesData)'})}
|
||||||
|
|
||||||
<b-notification type="is-warning"
|
<b-notification type="is-warning"
|
||||||
:active.sync="showConfigFilesNote">
|
% if request.use_oruga:
|
||||||
|
v-model:active="showConfigFilesNote"
|
||||||
|
% else:
|
||||||
|
:active.sync="showConfigFilesNote"
|
||||||
|
% endif
|
||||||
|
>
|
||||||
## TODO: should link to some ratman page here, yes?
|
## TODO: should link to some ratman page here, yes?
|
||||||
<p class="block">
|
<p class="block">
|
||||||
This tool works by modifying settings in the DB. It
|
This tool works by modifying settings in the DB. It
|
||||||
|
@ -69,8 +83,8 @@
|
||||||
</b-notification>
|
</b-notification>
|
||||||
|
|
||||||
<b-field>
|
<b-field>
|
||||||
<b-checkbox name="use_profile_settings"
|
<b-checkbox name="rattail.datasync.use_profile_settings"
|
||||||
v-model="useProfileSettings"
|
v-model="simpleSettings['rattail.datasync.use_profile_settings']"
|
||||||
native-value="true"
|
native-value="true"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true">
|
||||||
Use these Settings to configure watchers and consumers
|
Use these Settings to configure watchers and consumers
|
||||||
|
@ -85,7 +99,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="level-right">
|
<div class="level-right">
|
||||||
<div class="level-item"
|
<div class="level-item"
|
||||||
v-show="useProfileSettings">
|
v-show="simpleSettings['rattail.datasync.use_profile_settings']">
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
@click="newProfile()"
|
@click="newProfile()"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
|
@ -101,75 +115,89 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-table :data="filteredProfilesData"
|
<${b}-table :data="profilesData"
|
||||||
:row-class="(row, i) => row.enabled ? null : 'has-background-warning'">
|
:row-class="getWatcherRowClass">
|
||||||
<b-table-column field="key"
|
<${b}-table-column field="key"
|
||||||
label="Watcher Key"
|
label="Watcher Key"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.key }}
|
{{ props.row.key }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="watcher_spec"
|
<${b}-table-column field="watcher_spec"
|
||||||
label="Watcher Spec"
|
label="Watcher Spec"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.watcher_spec }}
|
{{ props.row.watcher_spec }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="watcher_dbkey"
|
<${b}-table-column field="watcher_dbkey"
|
||||||
label="DB Key"
|
label="DB Key"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.watcher_dbkey }}
|
{{ props.row.watcher_dbkey }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="watcher_delay"
|
<${b}-table-column field="watcher_delay"
|
||||||
label="Loop Delay"
|
label="Loop Delay"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.watcher_delay }} sec
|
{{ props.row.watcher_delay }} sec
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="watcher_retry_attempts"
|
<${b}-table-column field="watcher_retry_attempts"
|
||||||
label="Attempts / Delay"
|
label="Attempts / Delay"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.watcher_retry_attempts }} / {{ props.row.watcher_retry_delay }} sec
|
{{ props.row.watcher_retry_attempts }} / {{ props.row.watcher_retry_delay }} sec
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="watcher_default_runas"
|
<${b}-table-column field="watcher_default_runas"
|
||||||
label="Default Runas"
|
label="Default Runas"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.watcher_default_runas }}
|
{{ props.row.watcher_default_runas }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column label="Consumers"
|
<${b}-table-column label="Consumers"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ consumerShortList(props.row) }}
|
{{ consumerShortList(props.row) }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
## <b-table-column field="notes" label="Notes">
|
## <${b}-table-column field="notes" label="Notes">
|
||||||
## TODO
|
## TODO
|
||||||
## ## {{ props.row.notes }}
|
## ## {{ props.row.notes }}
|
||||||
## </b-table-column>
|
## </${b}-table-column>
|
||||||
<b-table-column field="enabled"
|
<${b}-table-column field="enabled"
|
||||||
label="Enabled"
|
label="Enabled"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.enabled ? "Yes" : "No" }}
|
{{ props.row.enabled ? "Yes" : "No" }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column label="Actions"
|
<${b}-table-column label="Actions"
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
v-if="useProfileSettings">
|
v-if="simpleSettings['rattail.datasync.use_profile_settings']">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
class="grid-action"
|
class="grid-action"
|
||||||
@click.prevent="editProfile(props.row)">
|
@click.prevent="editProfile(props.row)">
|
||||||
<i class="fas fa-edit"></i>
|
% if request.use_oruga:
|
||||||
Edit
|
<span class="icon-text">
|
||||||
|
<o-icon icon="edit" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</span>
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
Edit
|
||||||
|
% endif
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="#"
|
<a href="#"
|
||||||
class="grid-action has-text-danger"
|
class="grid-action has-text-danger"
|
||||||
@click.prevent="deleteProfile(props.row)">
|
@click.prevent="deleteProfile(props.row)">
|
||||||
<i class="fas fa-trash"></i>
|
% if request.use_oruga:
|
||||||
Delete
|
<span class="icon-text">
|
||||||
|
<o-icon icon="trash" />
|
||||||
|
<span>Delete</span>
|
||||||
|
</span>
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Delete
|
||||||
|
% endif
|
||||||
</a>
|
</a>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<template slot="empty">
|
<template #empty>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="content has-text-grey has-text-centered">
|
<div class="content has-text-grey has-text-centered">
|
||||||
<p>
|
<p>
|
||||||
<b-icon
|
<b-icon
|
||||||
pack="fas"
|
pack="fas"
|
||||||
icon="fas fa-sad-tear"
|
icon="sad-tear"
|
||||||
size="is-large">
|
size="is-large">
|
||||||
</b-icon>
|
</b-icon>
|
||||||
</p>
|
</p>
|
||||||
|
@ -177,7 +205,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
||||||
<b-modal :active.sync="editProfileShowDialog">
|
<b-modal :active.sync="editProfileShowDialog">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -199,12 +227,12 @@
|
||||||
|
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field grouped>
|
<b-field grouped expanded>
|
||||||
|
|
||||||
<b-field label="Watcher Spec"
|
<b-field label="Watcher Spec"
|
||||||
:type="editingProfileWatcherSpec ? null : 'is-danger'"
|
:type="editingProfileWatcherSpec ? null : 'is-danger'"
|
||||||
expanded>
|
expanded>
|
||||||
<b-input v-model="editingProfileWatcherSpec">
|
<b-input v-model="editingProfileWatcherSpec" expanded>
|
||||||
</b-input>
|
</b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
@ -293,40 +321,54 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<b-table :data="editingProfilePendingWatcherKwargs"
|
<${b}-table :data="editingProfilePendingWatcherKwargs"
|
||||||
style="margin-left: 1rem;">
|
style="margin-left: 1rem;">
|
||||||
<b-table-column field="key"
|
<${b}-table-column field="key"
|
||||||
label="Key"
|
label="Key"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.key }}
|
{{ props.row.key }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="value"
|
<${b}-table-column field="value"
|
||||||
label="Value"
|
label="Value"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.value }}
|
{{ props.row.value }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column label="Actions"
|
<${b}-table-column label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
@click.prevent="editProfileWatcherKwarg(props.row)">
|
@click.prevent="editProfileWatcherKwarg(props.row)">
|
||||||
<i class="fas fa-edit"></i>
|
% if request.use_oruga:
|
||||||
Edit
|
<span class="icon-text">
|
||||||
|
<o-icon icon="edit" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</span>
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
Edit
|
||||||
|
% endif
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="#"
|
<a href="#"
|
||||||
class="has-text-danger"
|
class="has-text-danger"
|
||||||
@click.prevent="deleteProfileWatcherKwarg(props.row)">
|
@click.prevent="deleteProfileWatcherKwarg(props.row)">
|
||||||
<i class="fas fa-trash"></i>
|
% if request.use_oruga:
|
||||||
Delete
|
<span class="icon-text">
|
||||||
|
<o-icon icon="trash" />
|
||||||
|
<span>Delete</span>
|
||||||
|
</span>
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Delete
|
||||||
|
% endif
|
||||||
</a>
|
</a>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<template slot="empty">
|
<template #empty>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="content has-text-grey has-text-centered">
|
<div class="content has-text-grey has-text-centered">
|
||||||
<p>
|
<p>
|
||||||
<b-icon
|
<b-icon
|
||||||
pack="fas"
|
pack="fas"
|
||||||
icon="fas fa-sad-tear"
|
icon="sad-tear"
|
||||||
size="is-large">
|
size="is-large">
|
||||||
</b-icon>
|
</b-icon>
|
||||||
</p>
|
</p>
|
||||||
|
@ -334,7 +376,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -350,41 +392,55 @@
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-table :data="editingProfilePendingConsumers"
|
<${b}-table :data="editingProfilePendingConsumers"
|
||||||
v-if="!editingProfileWatcherConsumesSelf"
|
v-if="!editingProfileWatcherConsumesSelf"
|
||||||
:row-class="(row, i) => row.enabled ? null : 'has-background-warning'">
|
:row-class="(row, i) => row.enabled ? null : 'has-background-warning'">
|
||||||
<b-table-column field="key"
|
<${b}-table-column field="key"
|
||||||
label="Consumer"
|
label="Consumer"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.key }}
|
{{ props.row.key }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column style="white-space: nowrap;"
|
<${b}-table-column style="white-space: nowrap;"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.consumer_delay }} / {{ props.row.consumer_retry_attempts }} / {{ props.row.consumer_retry_delay }}
|
{{ props.row.consumer_delay }} / {{ props.row.consumer_retry_attempts }} / {{ props.row.consumer_retry_delay }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column label="Actions"
|
<${b}-table-column label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
class="grid-action"
|
class="grid-action"
|
||||||
@click.prevent="editProfileConsumer(props.row)">
|
@click.prevent="editProfileConsumer(props.row)">
|
||||||
<i class="fas fa-edit"></i>
|
% if request.use_oruga:
|
||||||
Edit
|
<span class="icon-text">
|
||||||
|
<o-icon icon="edit" />
|
||||||
|
<span>Edit</span>
|
||||||
|
</span>
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
Edit
|
||||||
|
% endif
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="#"
|
<a href="#"
|
||||||
class="grid-action has-text-danger"
|
class="grid-action has-text-danger"
|
||||||
@click.prevent="deleteProfileConsumer(props.row)">
|
@click.prevent="deleteProfileConsumer(props.row)">
|
||||||
<i class="fas fa-trash"></i>
|
% if request.use_oruga:
|
||||||
Delete
|
<span class="icon-text">
|
||||||
|
<o-icon icon="trash" />
|
||||||
|
<span>Delete</span>
|
||||||
|
</span>
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Delete
|
||||||
|
% endif
|
||||||
</a>
|
</a>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<template slot="empty">
|
<template #empty>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="content has-text-grey has-text-centered">
|
<div class="content has-text-grey has-text-centered">
|
||||||
<p>
|
<p>
|
||||||
<b-icon
|
<b-icon
|
||||||
pack="fas"
|
pack="fas"
|
||||||
icon="fas fa-sad-tear"
|
icon="sad-tear"
|
||||||
size="is-large">
|
size="is-large">
|
||||||
</b-icon>
|
</b-icon>
|
||||||
</p>
|
</p>
|
||||||
|
@ -392,7 +448,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -524,31 +580,41 @@
|
||||||
<b-field label="Supervisor Process Name"
|
<b-field label="Supervisor Process Name"
|
||||||
message="This should be the complete name, including group - e.g. poser:poser_datasync"
|
message="This should be the complete name, including group - e.g. poser:poser_datasync"
|
||||||
expanded>
|
expanded>
|
||||||
<b-input name="supervisor_process_name"
|
<b-input name="rattail.datasync.supervisor_process_name"
|
||||||
v-model="supervisorProcessName"
|
v-model="simpleSettings['rattail.datasync.supervisor_process_name']"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true"
|
||||||
|
expanded>
|
||||||
</b-input>
|
</b-input>
|
||||||
</b-field>
|
</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"
|
<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"
|
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>
|
expanded>
|
||||||
<b-input name="restart_command"
|
<b-input name="tailbone.datasync.restart"
|
||||||
v-model="restartCommand"
|
v-model="simpleSettings['tailbone.datasync.restart']"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true"
|
||||||
|
expanded>
|
||||||
</b-input>
|
</b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
ThisPageData.showConfigFilesNote = false
|
ThisPageData.showConfigFilesNote = false
|
||||||
ThisPageData.profilesData = ${json.dumps(profiles_data)|n}
|
ThisPageData.profilesData = ${json.dumps(profiles_data)|n}
|
||||||
ThisPageData.showDisabledProfiles = false
|
ThisPageData.showDisabledProfiles = false
|
||||||
ThisPageData.useProfileSettings = ${json.dumps(use_profile_settings)|n}
|
|
||||||
|
|
||||||
ThisPageData.editProfileShowDialog = false
|
ThisPageData.editProfileShowDialog = false
|
||||||
ThisPageData.editingProfile = null
|
ThisPageData.editingProfile = null
|
||||||
|
@ -573,22 +639,6 @@
|
||||||
ThisPageData.editingConsumerRunas = null
|
ThisPageData.editingConsumerRunas = null
|
||||||
ThisPageData.editingConsumerEnabled = true
|
ThisPageData.editingConsumerEnabled = true
|
||||||
|
|
||||||
ThisPageData.supervisorProcessName = ${json.dumps(supervisor_process_name)|n}
|
|
||||||
ThisPageData.restartCommand = ${json.dumps(restart_command)|n}
|
|
||||||
|
|
||||||
ThisPage.computed.filteredProfilesData = function() {
|
|
||||||
if (this.showDisabledProfiles) {
|
|
||||||
return this.profilesData
|
|
||||||
}
|
|
||||||
let data = []
|
|
||||||
for (let row of this.profilesData) {
|
|
||||||
if (row.enabled) {
|
|
||||||
data.push(row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.computed.updateConsumerDisabled = function() {
|
ThisPage.computed.updateConsumerDisabled = function() {
|
||||||
if (!this.editingConsumerKey) {
|
if (!this.editingConsumerKey) {
|
||||||
return true
|
return true
|
||||||
|
@ -616,6 +666,15 @@
|
||||||
this.showDisabledProfiles = !this.showDisabledProfiles
|
this.showDisabledProfiles = !this.showDisabledProfiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.getWatcherRowClass = function(row, i) {
|
||||||
|
if (!row.enabled) {
|
||||||
|
if (!this.showDisabledProfiles) {
|
||||||
|
return 'invisible-watcher'
|
||||||
|
}
|
||||||
|
return 'has-background-warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ThisPage.methods.consumerShortList = function(row) {
|
ThisPage.methods.consumerShortList = function(row) {
|
||||||
let keys = []
|
let keys = []
|
||||||
if (row.watcher_consumes_self) {
|
if (row.watcher_consumes_self) {
|
||||||
|
@ -680,16 +739,9 @@
|
||||||
|
|
||||||
this.editingProfilePendingConsumers = []
|
this.editingProfilePendingConsumers = []
|
||||||
for (let consumer of row.consumers_data) {
|
for (let consumer of row.consumers_data) {
|
||||||
let pending = {
|
const pending = {
|
||||||
|
...consumer,
|
||||||
original_key: consumer.key,
|
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)
|
this.editingProfilePendingConsumers.push(pending)
|
||||||
}
|
}
|
||||||
|
@ -737,8 +789,8 @@
|
||||||
this.editingProfilePendingWatcherKwargs.splice(i, 1)
|
this.editingProfilePendingWatcherKwargs.splice(i, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
ThisPage.methods.findOriginalConsumer = function(key) {
|
ThisPage.methods.findConsumer = function(profileConsumers, key) {
|
||||||
for (let consumer of this.editingProfile.consumers_data) {
|
for (const consumer of profileConsumers) {
|
||||||
if (consumer.key == key) {
|
if (consumer.key == key) {
|
||||||
return consumer
|
return consumer
|
||||||
}
|
}
|
||||||
|
@ -746,11 +798,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ThisPage.methods.updateProfile = function() {
|
ThisPage.methods.updateProfile = function() {
|
||||||
let row = this.editingProfile
|
const row = this.editingProfile
|
||||||
|
|
||||||
if (!row.key) {
|
const newRow = !row.key
|
||||||
|
let originalProfile = null
|
||||||
|
if (newRow) {
|
||||||
row.consumers_data = []
|
row.consumers_data = []
|
||||||
this.profilesData.push(row)
|
this.profilesData.push(row)
|
||||||
|
} else {
|
||||||
|
originalProfile = this.findProfile(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
row.key = this.editingProfileKey
|
row.key = this.editingProfileKey
|
||||||
|
@ -798,7 +854,8 @@
|
||||||
for (let pending of this.editingProfilePendingConsumers) {
|
for (let pending of this.editingProfilePendingConsumers) {
|
||||||
persistentConsumers.push(pending.key)
|
persistentConsumers.push(pending.key)
|
||||||
if (pending.original_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.key = pending.key
|
||||||
consumer.consumer_spec = pending.consumer_spec
|
consumer.consumer_spec = pending.consumer_spec
|
||||||
consumer.consumer_dbkey = pending.consumer_dbkey
|
consumer.consumer_dbkey = pending.consumer_dbkey
|
||||||
|
@ -825,10 +882,31 @@
|
||||||
row.consumers_data.splice(i, 1)
|
row.consumers_data.splice(i, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!newRow) {
|
||||||
|
|
||||||
|
// nb. must explicitly update the original data row;
|
||||||
|
// otherwise (with vue3) it will remain stale and
|
||||||
|
// submitting the form will keep same settings!
|
||||||
|
// TODO: this probably means i am doing something
|
||||||
|
// sloppy, but at least this hack fixes for now.
|
||||||
|
const profile = this.findProfile(row)
|
||||||
|
for (const key of Object.keys(row)) {
|
||||||
|
profile[key] = row[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.settingsNeedSaved = true
|
this.settingsNeedSaved = true
|
||||||
this.editProfileShowDialog = false
|
this.editProfileShowDialog = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.findProfile = function(row) {
|
||||||
|
for (const profile of this.profilesData) {
|
||||||
|
if (profile.key == row.key) {
|
||||||
|
return profile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ThisPage.methods.deleteProfile = function(row) {
|
ThisPage.methods.deleteProfile = function(row) {
|
||||||
if (confirm("Are you sure you want to delete the '" + row.key + "' profile?")) {
|
if (confirm("Are you sure you want to delete the '" + row.key + "' profile?")) {
|
||||||
let i = this.profilesData.indexOf(row)
|
let i = this.profilesData.indexOf(row)
|
||||||
|
@ -865,8 +943,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ThisPage.methods.updateConsumer = function() {
|
ThisPage.methods.updateConsumer = function() {
|
||||||
let pending = this.editingConsumer
|
const pending = this.findConsumer(
|
||||||
let isNew = !pending.key
|
this.editingProfilePendingConsumers,
|
||||||
|
this.editingConsumer.key)
|
||||||
|
const isNew = !pending.key
|
||||||
|
|
||||||
pending.key = this.editingConsumerKey
|
pending.key = this.editingConsumerKey
|
||||||
pending.consumer_spec = this.editingConsumerSpec
|
pending.consumer_spec = this.editingConsumerSpec
|
||||||
|
@ -907,6 +987,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -5,13 +5,6 @@
|
||||||
|
|
||||||
<%def name="content_title()"></%def>
|
<%def name="content_title()"></%def>
|
||||||
|
|
||||||
<%def name="context_menu_items()">
|
|
||||||
${parent.context_menu_items()}
|
|
||||||
% if request.has_perm('datasync_changes.list'):
|
|
||||||
<li>${h.link_to("View DataSync Changes", url('datasyncchanges'))}</li>
|
|
||||||
% endif
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="page_content()">
|
<%def name="page_content()">
|
||||||
% if expose_websockets and not supervisor_error:
|
% if expose_websockets and not supervisor_error:
|
||||||
<b-notification type="is-warning"
|
<b-notification type="is-warning"
|
||||||
|
@ -47,83 +40,84 @@
|
||||||
</div>
|
</div>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Watcher Status">
|
<h3 class="is-size-3">Watcher Status</h3>
|
||||||
<b-table :data="watchers">
|
|
||||||
<b-table-column field="key"
|
<${b}-table :data="watchers">
|
||||||
|
<${b}-table-column field="key"
|
||||||
label="Watcher"
|
label="Watcher"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.key }}
|
{{ props.row.key }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="spec"
|
<${b}-table-column field="spec"
|
||||||
label="Spec"
|
label="Spec"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.spec }}
|
{{ props.row.spec }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="dbkey"
|
<${b}-table-column field="dbkey"
|
||||||
label="DB Key"
|
label="DB Key"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.dbkey }}
|
{{ props.row.dbkey }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="delay"
|
<${b}-table-column field="delay"
|
||||||
label="Delay"
|
label="Delay"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.delay }} second(s)
|
{{ props.row.delay }} second(s)
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="lastrun"
|
<${b}-table-column field="lastrun"
|
||||||
label="Last Watched"
|
label="Last Watched"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<span v-html="props.row.lastrun"></span>
|
<span v-html="props.row.lastrun"></span>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="status"
|
<${b}-table-column field="status"
|
||||||
label="Status"
|
label="Status"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<span :class="props.row.status == 'okay' ? 'has-background-success' : 'has-background-warning'">
|
<span :class="props.row.status == 'okay' ? 'has-background-success' : 'has-background-warning'">
|
||||||
{{ props.row.status }}
|
{{ props.row.status }}
|
||||||
</span>
|
</span>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
</b-table>
|
</${b}-table>
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Consumer Status">
|
<h3 class="is-size-3">Consumer Status</h3>
|
||||||
<b-table :data="consumers">
|
|
||||||
<b-table-column field="key"
|
<${b}-table :data="consumers">
|
||||||
|
<${b}-table-column field="key"
|
||||||
label="Consumer"
|
label="Consumer"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.key }}
|
{{ props.row.key }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="spec"
|
<${b}-table-column field="spec"
|
||||||
label="Spec"
|
label="Spec"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.spec }}
|
{{ props.row.spec }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="dbkey"
|
<${b}-table-column field="dbkey"
|
||||||
label="DB Key"
|
label="DB Key"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.dbkey }}
|
{{ props.row.dbkey }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="delay"
|
<${b}-table-column field="delay"
|
||||||
label="Delay"
|
label="Delay"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.delay }} second(s)
|
{{ props.row.delay }} second(s)
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="changes"
|
<${b}-table-column field="changes"
|
||||||
label="Pending Changes"
|
label="Pending Changes"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.changes }}
|
{{ props.row.changes }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="status"
|
<${b}-table-column field="status"
|
||||||
label="Status"
|
label="Status"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<span :class="props.row.status == 'okay' ? 'has-background-success' : 'has-background-warning'">
|
<span :class="props.row.status == 'okay' ? 'has-background-success' : 'has-background-warning'">
|
||||||
{{ props.row.status }}
|
{{ props.row.status }}
|
||||||
</span>
|
</span>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
</b-table>
|
</${b}-table>
|
||||||
</b-field>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
<script type="text/javascript">
|
${parent.modify_vue_vars()}
|
||||||
|
<script>
|
||||||
|
|
||||||
ThisPageData.processInfo = ${json.dumps(process_info)|n}
|
ThisPageData.processInfo = ${json.dumps(process_info)|n}
|
||||||
|
|
||||||
|
@ -178,6 +172,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<div i18n:domain="deform" tal:omit-tag=""
|
<div i18n:domain="deform" tal:omit-tag=""
|
||||||
tal:define="oid oid|field.oid;
|
tal:define="oid oid|field.oid;
|
||||||
name name|field.name;
|
name name|field.name;
|
||||||
|
vmodel vmodel|'field_model_' + name;
|
||||||
css_class css_class|field.widget.css_class;
|
css_class css_class|field.widget.css_class;
|
||||||
style style|field.widget.style;">
|
style style|field.widget.style;">
|
||||||
|
|
||||||
|
@ -8,7 +9,7 @@
|
||||||
${field.start_mapping()}
|
${field.start_mapping()}
|
||||||
<b-input type="password"
|
<b-input type="password"
|
||||||
name="${name}"
|
name="${name}"
|
||||||
value="${field.widget.redisplay and cstruct or ''}"
|
v-model="${vmodel}"
|
||||||
tal:attributes="class string: form-control ${css_class or ''};
|
tal:attributes="class string: form-control ${css_class or ''};
|
||||||
style style;
|
style style;
|
||||||
attributes|field.widget.attributes|{};"
|
attributes|field.widget.attributes|{};"
|
||||||
|
@ -18,7 +19,6 @@
|
||||||
</b-input>
|
</b-input>
|
||||||
<b-input type="password"
|
<b-input type="password"
|
||||||
name="${name}-confirm"
|
name="${name}-confirm"
|
||||||
value="${field.widget.redisplay and confirm or ''}"
|
|
||||||
tal:attributes="class string: form-control ${css_class or ''};
|
tal:attributes="class string: form-control ${css_class or ''};
|
||||||
style style;
|
style style;
|
||||||
confirm_attributes|field.widget.confirm_attributes|{};"
|
confirm_attributes|field.widget.confirm_attributes|{};"
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
<tal:block tal:define="oid oid|field.oid;
|
<tal:block tal:define="oid oid|field.oid;
|
||||||
css_class css_class|field.widget.css_class;
|
css_class css_class|field.widget.css_class;
|
||||||
style style|field.widget.style;
|
style style|field.widget.style;
|
||||||
field_name field_name|field.name;">
|
field_name field_name|field.name;
|
||||||
|
use_oruga use_oruga;">
|
||||||
|
|
||||||
<div tal:define="vmodel vmodel|'field_model_' + field_name;">
|
<div tal:define="vmodel vmodel|'field_model_' + field_name;">
|
||||||
${field.start_mapping()}
|
${field.start_mapping()}
|
||||||
<b-field class="file">
|
|
||||||
|
<b-field class="file"
|
||||||
|
tal:condition="not use_oruga">
|
||||||
<b-upload name="upload"
|
<b-upload name="upload"
|
||||||
v-model="${vmodel}">
|
v-model="${vmodel}">
|
||||||
<a class="button is-primary">
|
<a class="button is-primary">
|
||||||
|
@ -18,6 +21,23 @@
|
||||||
{{ ${vmodel}.name }}
|
{{ ${vmodel}.name }}
|
||||||
</span>
|
</span>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
<o-field class="file"
|
||||||
|
tal:condition="use_oruga">
|
||||||
|
<o-upload name="upload"
|
||||||
|
v-slot="{ onclick }"
|
||||||
|
v-model="${vmodel}">
|
||||||
|
<o-button variant="primary"
|
||||||
|
@click="onclick">
|
||||||
|
<o-icon icon="upload" />
|
||||||
|
<span>Click to upload</span>
|
||||||
|
</o-button>
|
||||||
|
</o-upload>
|
||||||
|
<span class="file-name" v-if="${vmodel}">
|
||||||
|
{{ ${vmodel}.name }}
|
||||||
|
</span>
|
||||||
|
</o-field>
|
||||||
|
|
||||||
${field.end_mapping()}
|
${field.end_mapping()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/master/view.mako" />
|
<%inherit file="/master/view.mako" />
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
${form.vue_component}Data.employeesData = ${json.dumps(employees_data)|n}
|
||||||
${form.component_studly}Data.employeesData = ${json.dumps(employees_data)|n}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -6,20 +6,63 @@
|
||||||
<%def name="render_form_buttons()"></%def>
|
<%def name="render_form_buttons()"></%def>
|
||||||
|
|
||||||
<%def name="render_form_template()">
|
<%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>
|
||||||
|
|
||||||
<%def name="render_form()">
|
<%def name="render_form()">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
${form.render_vuejs_component()}
|
${form.render_vue_tag()}
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="page_content()">
|
<%def name="page_content()">
|
||||||
<div class="form-wrapper">
|
% if main_form_collapsible:
|
||||||
<br />
|
<${b}-collapse class="panel"
|
||||||
${self.render_form()}
|
% if request.use_oruga:
|
||||||
</div>
|
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>
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<%def name="render_this_page()">
|
<%def name="render_this_page()">
|
||||||
|
@ -47,25 +90,25 @@
|
||||||
|
|
||||||
<%def name="before_object_helpers()"></%def>
|
<%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:
|
% if form is not Undefined:
|
||||||
${self.render_form_template()}
|
${self.render_form_template()}
|
||||||
% endif
|
% endif
|
||||||
${parent.render_this_page_template()}
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="finalize_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.finalize_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
% if form is not Undefined:
|
% if main_form_collapsible:
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
ThisPageData.mainFormPanelOpen = ${'false' if main_form_autocollapse else 'true'}
|
||||||
${form.component_studly}.data = function() { return ${form.component_studly}Data }
|
|
||||||
|
|
||||||
Vue.component('${form.component}', ${form.component_studly})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_vue_components()">
|
||||||
${parent.body()}
|
${parent.make_vue_components()}
|
||||||
|
% if form is not Undefined:
|
||||||
|
${form.render_vue_finalize()}
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
|
|
||||||
simplePOST(action, params, success, failure) {
|
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 = {
|
let headers = {
|
||||||
'${csrf_header_name}': csrftoken,
|
'${csrf_header_name}': csrftoken,
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
|
|
||||||
<script type="text/x-template" id="${form.component}-template">
|
<% request.register_component(form.vue_tagname, form.vue_component) %>
|
||||||
|
|
||||||
|
<script type="text/x-template" id="${form.vue_tagname}-template">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
% if not form.readonly:
|
% 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)}
|
${h.csrf_token(request)}
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
% if form_body is not Undefined and form_body:
|
% if form_body is not Undefined and form_body:
|
||||||
${form_body|n}
|
${form_body|n}
|
||||||
% elif form.grouping:
|
% elif getattr(form, 'grouping', None):
|
||||||
% for group in form.grouping:
|
% for group in form.grouping:
|
||||||
<nav class="panel">
|
<nav class="panel">
|
||||||
<p class="panel-heading">${group}</p>
|
<p class="panel-heading">${group}</p>
|
||||||
|
@ -25,8 +27,8 @@
|
||||||
</nav>
|
</nav>
|
||||||
% endfor
|
% endfor
|
||||||
% else:
|
% else:
|
||||||
% for field in form.fields:
|
% for fieldname in form.fields:
|
||||||
${form.render_field_complete(field)}
|
${form.render_vue_field(fieldname, session=session)}
|
||||||
% endfor
|
% endfor
|
||||||
% endif
|
% endif
|
||||||
</section>
|
</section>
|
||||||
|
@ -52,16 +54,20 @@
|
||||||
<input type="reset" value="Reset" class="button" />
|
<input type="reset" value="Reset" class="button" />
|
||||||
% endif
|
% endif
|
||||||
## TODO: deprecate / remove the latter option here
|
## 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"
|
<b-button type="is-primary"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
:disabled="${form.component_studly}Submitting">
|
:disabled="${form.vue_component}Submitting"
|
||||||
{{ ${form.component_studly}ButtonText }}
|
icon-pack="fas"
|
||||||
|
icon-left="${form.button_icon_submit}">
|
||||||
|
{{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }}
|
||||||
</b-button>
|
</b-button>
|
||||||
% else:
|
% else:
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
native-type="submit">
|
native-type="submit"
|
||||||
${getattr(form, 'submit_label', getattr(form, 'save_label', "Submit"))}
|
icon-pack="fas"
|
||||||
|
icon-left="save">
|
||||||
|
${form.button_label_submit}
|
||||||
</b-button>
|
</b-button>
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
|
@ -116,8 +122,8 @@
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
let ${form.component_studly} = {
|
let ${form.vue_component} = {
|
||||||
template: '#${form.component}-template',
|
template: '#${form.vue_tagname}-template',
|
||||||
mixins: [FormPosterMixin],
|
mixins: [FormPosterMixin],
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
|
@ -130,10 +136,9 @@
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
## TODO: deprecate / remove the latter option here
|
## 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:
|
||||||
submit${form.component_studly}() {
|
submit${form.vue_component}() {
|
||||||
this.${form.component_studly}Submitting = true
|
this.${form.vue_component}Submitting = true
|
||||||
this.${form.component_studly}ButtonText = "Working, please wait..."
|
|
||||||
},
|
},
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
@ -172,10 +177,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ${form.component_studly}Data = {
|
let ${form.vue_component}Data = {
|
||||||
|
|
||||||
## TODO: should find a better way to handle CSRF token
|
## 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:
|
% if can_edit_help:
|
||||||
fieldLabels: ${json.dumps(field_labels)|n},
|
fieldLabels: ${json.dumps(field_labels)|n},
|
||||||
|
@ -192,16 +197,14 @@
|
||||||
% if not form.readonly:
|
% if not form.readonly:
|
||||||
% for field in form.fields:
|
% for field in form.fields:
|
||||||
% if field in dform:
|
% if field in dform:
|
||||||
<% field = dform[field] %>
|
field_model_${field}: ${json.dumps(form.get_vue_field_value(field))|n},
|
||||||
field_model_${field.name}: ${form.get_vuejs_model_value(field)|n},
|
|
||||||
% endif
|
% endif
|
||||||
% endfor
|
% endfor
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## TODO: deprecate / remove the latter option here
|
## 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:
|
||||||
${form.component_studly}Submitting: false,
|
${form.vue_component}Submitting: false,
|
||||||
${form.component_studly}ButtonText: ${json.dumps(getattr(form, 'submit_label', getattr(form, 'save_label', "Submit")))|n},
|
|
||||||
% endif
|
% endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
tailbone/templates/forms/vue_template.mako
Normal file
3
tailbone/templates/forms/vue_template.mako
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/forms/deform.mako" />
|
||||||
|
${parent.body()}
|
|
@ -87,7 +87,7 @@
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="fas fa-plus"
|
icon-left="plus"
|
||||||
@click="addColumn()">
|
@click="addColumn()">
|
||||||
New Column
|
New Column
|
||||||
</b-button>
|
</b-button>
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<b-button type="is-danger"
|
<b-button type="is-danger"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="fas fa-trash"
|
icon-left="trash"
|
||||||
@click="new_table.columns = []"
|
@click="new_table.columns = []"
|
||||||
:disabled="!new_table.columns.length">
|
:disabled="!new_table.columns.length">
|
||||||
Delete All
|
Delete All
|
||||||
|
@ -106,55 +106,68 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-table
|
<${b}-table
|
||||||
:data="new_table.columns">
|
:data="new_table.columns">
|
||||||
|
|
||||||
<b-table-column field="name"
|
<${b}-table-column field="name"
|
||||||
label="Name"
|
label="Name"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.name }}
|
{{ props.row.name }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
|
|
||||||
<b-table-column field="data_type"
|
<${b}-table-column field="data_type"
|
||||||
label="Data Type"
|
label="Data Type"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.data_type }}
|
{{ props.row.data_type }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
|
|
||||||
<b-table-column field="nullable"
|
<${b}-table-column field="nullable"
|
||||||
label="Nullable"
|
label="Nullable"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.nullable }}
|
{{ props.row.nullable }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
|
|
||||||
<b-table-column field="description"
|
<${b}-table-column field="description"
|
||||||
label="Description"
|
label="Description"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.description }}
|
{{ props.row.description }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
|
|
||||||
<b-table-column field="actions"
|
<${b}-table-column field="actions"
|
||||||
label="Actions"
|
label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<a href="#" class="grid-action"
|
<a href="#" class="grid-action"
|
||||||
@click.prevent="editColumnRow(props.row)">
|
@click.prevent="editColumnRow(props)">
|
||||||
<i class="fas fa-edit"></i>
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="edit" />
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
% endif
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<a href="#" class="grid-action has-text-danger"
|
<a href="#" class="grid-action has-text-danger"
|
||||||
@click.prevent="deleteColumn(props.index)">
|
@click.prevent="deleteColumn(props.index)">
|
||||||
<i class="fas fa-trash"></i>
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="trash" />
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
% endif
|
||||||
Delete
|
Delete
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
|
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
||||||
<b-modal has-modal-card
|
<${b}-modal has-modal-card
|
||||||
:active.sync="showingEditColumn">
|
% if request.use_oruga:
|
||||||
|
v-model:active="showingEditColumn"
|
||||||
|
% else:
|
||||||
|
:active.sync="showingEditColumn"
|
||||||
|
% endif
|
||||||
|
>
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
|
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
|
@ -164,11 +177,13 @@
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
|
|
||||||
<b-field label="Name">
|
<b-field label="Name">
|
||||||
<b-input v-model="editingColumnName"></b-input>
|
<b-input v-model="editingColumnName"
|
||||||
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Data Type">
|
<b-field label="Data Type">
|
||||||
<b-input v-model="editingColumnDataType"></b-input>
|
<b-input v-model="editingColumnDataType"
|
||||||
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Nullable">
|
<b-field label="Nullable">
|
||||||
|
@ -179,7 +194,8 @@
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Description">
|
<b-field label="Description">
|
||||||
<b-input v-model="editingColumnDescription"></b-input>
|
<b-input v-model="editingColumnDescription"
|
||||||
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
@ -194,7 +210,7 @@
|
||||||
</b-button>
|
</b-button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</${b}-modal>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
@ -260,9 +276,9 @@
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
ThisPageData.featureType = ${json.dumps(feature_type)|n}
|
ThisPageData.featureType = ${json.dumps(feature_type)|n}
|
||||||
ThisPageData.resultGenerated = ${json.dumps(bool(result))|n}
|
ThisPageData.resultGenerated = ${json.dumps(bool(result))|n}
|
||||||
|
@ -280,7 +296,7 @@
|
||||||
% endfor
|
% endfor
|
||||||
}
|
}
|
||||||
|
|
||||||
% for key, form in six.iteritems(feature_forms):
|
% for key, form in feature_forms.items():
|
||||||
<% safekey = key.replace('-', '_') %>
|
<% safekey = key.replace('-', '_') %>
|
||||||
ThisPageData.${safekey} = {
|
ThisPageData.${safekey} = {
|
||||||
<% dform = feature_forms[key].make_deform_form() %>
|
<% dform = feature_forms[key].make_deform_form() %>
|
||||||
|
@ -315,6 +331,7 @@
|
||||||
|
|
||||||
ThisPageData.showingEditColumn = false
|
ThisPageData.showingEditColumn = false
|
||||||
ThisPageData.editingColumn = null
|
ThisPageData.editingColumn = null
|
||||||
|
ThisPageData.editingColumnIndex = null
|
||||||
ThisPageData.editingColumnName = null
|
ThisPageData.editingColumnName = null
|
||||||
ThisPageData.editingColumnDataType = null
|
ThisPageData.editingColumnDataType = null
|
||||||
ThisPageData.editingColumnNullable = null
|
ThisPageData.editingColumnNullable = null
|
||||||
|
@ -322,6 +339,7 @@
|
||||||
|
|
||||||
ThisPage.methods.addColumn = function(column) {
|
ThisPage.methods.addColumn = function(column) {
|
||||||
this.editingColumn = null
|
this.editingColumn = null
|
||||||
|
this.editingColumnIndex = null
|
||||||
this.editingColumnName = null
|
this.editingColumnName = null
|
||||||
this.editingColumnDataType = null
|
this.editingColumnDataType = null
|
||||||
this.editingColumnNullable = true
|
this.editingColumnNullable = true
|
||||||
|
@ -329,8 +347,10 @@
|
||||||
this.showingEditColumn = true
|
this.showingEditColumn = true
|
||||||
}
|
}
|
||||||
|
|
||||||
ThisPage.methods.editColumnRow = function(column) {
|
ThisPage.methods.editColumnRow = function(props) {
|
||||||
|
const column = props.row
|
||||||
this.editingColumn = column
|
this.editingColumn = column
|
||||||
|
this.editingColumnIndex = props.index
|
||||||
this.editingColumnName = column.name
|
this.editingColumnName = column.name
|
||||||
this.editingColumnDataType = column.data_type
|
this.editingColumnDataType = column.data_type
|
||||||
this.editingColumnNullable = column.nullable
|
this.editingColumnNullable = column.nullable
|
||||||
|
@ -340,7 +360,7 @@
|
||||||
|
|
||||||
ThisPage.methods.saveColumn = function() {
|
ThisPage.methods.saveColumn = function() {
|
||||||
if (this.editingColumn) {
|
if (this.editingColumn) {
|
||||||
column = this.editingColumn
|
column = this.new_table.columns[this.editingColumnIndex]
|
||||||
} else {
|
} else {
|
||||||
column = {}
|
column = {}
|
||||||
this.new_table.columns.push(column)
|
this.new_table.columns.push(column)
|
||||||
|
@ -365,6 +385,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
<%def name="page_content()">
|
<%def name="page_content()">
|
||||||
% if project_type:
|
% if project_type:
|
||||||
<b-field grouped>
|
<b-field grouped>
|
||||||
<b-field horizontal expanded label="Project Type">
|
<b-field horizontal expanded label="Project Type"
|
||||||
|
class="is-expanded">
|
||||||
${project_type}
|
${project_type}
|
||||||
</b-field>
|
</b-field>
|
||||||
<once-button type="is-primary"
|
<once-button type="is-primary"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<b-table
|
<${b}-table
|
||||||
:data="${data_prop}"
|
:data="${data_prop}"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
striped
|
striped
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
>
|
>
|
||||||
|
|
||||||
% for i, column in enumerate(grid_columns):
|
% for i, column in enumerate(grid_columns):
|
||||||
<b-table-column field="${column['field']}"
|
<${b}-table-column field="${column['field']}"
|
||||||
% if not empty_labels:
|
% if not empty_labels:
|
||||||
label="${column['label']}"
|
label="${column['label']}"
|
||||||
% elif i > 0:
|
% elif i > 0:
|
||||||
|
@ -50,14 +50,14 @@
|
||||||
% else:
|
% else:
|
||||||
<span v-html="props.row.${column['field']}"></span>
|
<span v-html="props.row.${column['field']}"></span>
|
||||||
% endif
|
% endif
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
% endfor
|
% endfor
|
||||||
|
|
||||||
% if grid.main_actions or grid.more_actions:
|
% if grid.actions:
|
||||||
<b-table-column field="actions"
|
<${b}-table-column field="actions"
|
||||||
label="Actions"
|
label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
% for action in grid.main_actions:
|
% for action in grid.actions:
|
||||||
<a :href="props.row._action_url_${action.key}"
|
<a :href="props.row._action_url_${action.key}"
|
||||||
% if action.link_class:
|
% if action.link_class:
|
||||||
class="${action.link_class}"
|
class="${action.link_class}"
|
||||||
|
@ -68,20 +68,19 @@
|
||||||
@click.prevent="${action.click_handler}"
|
@click.prevent="${action.click_handler}"
|
||||||
% endif
|
% endif
|
||||||
>
|
>
|
||||||
<i class="fas fa-${action.icon}"></i>
|
${action.render_icon_and_label()}
|
||||||
${action.label}
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
% endfor
|
% endfor
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
<template slot="empty">
|
<template #empty>
|
||||||
<div class="content has-text-grey has-text-centered">
|
<div class="content has-text-grey has-text-centered">
|
||||||
<p>
|
<p>
|
||||||
<b-icon
|
<b-icon
|
||||||
pack="fas"
|
pack="fas"
|
||||||
icon="fas fa-sad-tear"
|
icon="sad-tear"
|
||||||
size="is-large">
|
size="is-large">
|
||||||
</b-icon>
|
</b-icon>
|
||||||
</p>
|
</p>
|
||||||
|
@ -99,4 +98,4 @@
|
||||||
</template>
|
</template>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
|
@ -1,142 +1,79 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
|
|
||||||
<script type="text/x-template" id="grid-filter-numeric-value-template">
|
<% request.register_component(grid.vue_tagname, grid.vue_component) %>
|
||||||
<div class="level">
|
|
||||||
<div class="level-left">
|
|
||||||
<div class="level-item">
|
|
||||||
<b-input v-model="startValue"
|
|
||||||
ref="startValue"
|
|
||||||
@input="startValueChanged">
|
|
||||||
</b-input>
|
|
||||||
</div>
|
|
||||||
<div v-show="wantsRange"
|
|
||||||
class="level-item">
|
|
||||||
and
|
|
||||||
</div>
|
|
||||||
<div v-show="wantsRange"
|
|
||||||
class="level-item">
|
|
||||||
<b-input v-model="endValue"
|
|
||||||
ref="endValue"
|
|
||||||
@input="endValueChanged">
|
|
||||||
</b-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="text/x-template" id="grid-filter-date-value-template">
|
<script type="text/x-template" id="${grid.vue_tagname}-template">
|
||||||
<div class="level">
|
|
||||||
<div class="level-left">
|
|
||||||
<div class="level-item">
|
|
||||||
<tailbone-datepicker v-model="startDate"
|
|
||||||
ref="startDate"
|
|
||||||
@input="startDateChanged">
|
|
||||||
</tailbone-datepicker>
|
|
||||||
</div>
|
|
||||||
<div v-show="dateRange"
|
|
||||||
class="level-item">
|
|
||||||
and
|
|
||||||
</div>
|
|
||||||
<div v-show="dateRange"
|
|
||||||
class="level-item">
|
|
||||||
<tailbone-datepicker v-model="endDate"
|
|
||||||
ref="endDate"
|
|
||||||
@input="endDateChanged">
|
|
||||||
</tailbone-datepicker>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="text/x-template" id="grid-filter-template">
|
|
||||||
|
|
||||||
<div class="level filter" v-show="filter.visible">
|
|
||||||
<div class="level-left"
|
|
||||||
style="align-items: start;">
|
|
||||||
|
|
||||||
<div class="level-item filter-fieldname">
|
|
||||||
|
|
||||||
<b-field>
|
|
||||||
<b-checkbox-button v-model="filter.active" native-value="IGNORED">
|
|
||||||
<b-icon pack="fas" icon="check" v-show="filter.active"></b-icon>
|
|
||||||
<span>{{ filter.label }}</span>
|
|
||||||
</b-checkbox-button>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<b-field grouped v-show="filter.active"
|
|
||||||
class="level-item"
|
|
||||||
style="align-items: start;">
|
|
||||||
|
|
||||||
<b-select v-model="filter.verb"
|
|
||||||
@input="focusValue()"
|
|
||||||
class="filter-verb">
|
|
||||||
<option v-for="verb in filter.verbs"
|
|
||||||
:key="verb"
|
|
||||||
:value="verb">
|
|
||||||
{{ filter.verb_labels[verb] }}
|
|
||||||
</option>
|
|
||||||
</b-select>
|
|
||||||
|
|
||||||
## only one of the following "value input" elements will be rendered
|
|
||||||
|
|
||||||
<grid-filter-date-value v-if="filter.data_type == 'date'"
|
|
||||||
v-model="filter.value"
|
|
||||||
v-show="valuedVerb()"
|
|
||||||
:date-range="filter.verb == 'between'"
|
|
||||||
ref="valueInput">
|
|
||||||
</grid-filter-date-value>
|
|
||||||
|
|
||||||
<b-select v-if="filter.data_type == 'choice'"
|
|
||||||
v-model="filter.value"
|
|
||||||
v-show="valuedVerb()"
|
|
||||||
ref="valueInput">
|
|
||||||
<option v-for="choice in filter.choices"
|
|
||||||
:key="choice"
|
|
||||||
:value="choice">
|
|
||||||
{{ filter.choice_labels[choice] || choice }}
|
|
||||||
</option>
|
|
||||||
</b-select>
|
|
||||||
|
|
||||||
<grid-filter-numeric-value v-if="filter.data_type == 'number'"
|
|
||||||
v-model="filter.value"
|
|
||||||
v-show="valuedVerb()"
|
|
||||||
:wants-range="filter.verb == 'between'"
|
|
||||||
ref="valueInput">
|
|
||||||
</grid-filter-numeric-value>
|
|
||||||
|
|
||||||
<b-input v-if="filter.data_type == 'string' && !multiValuedVerb()"
|
|
||||||
v-model="filter.value"
|
|
||||||
v-show="valuedVerb()"
|
|
||||||
ref="valueInput">
|
|
||||||
</b-input>
|
|
||||||
|
|
||||||
<b-input v-if="filter.data_type == 'string' && multiValuedVerb()"
|
|
||||||
type="textarea"
|
|
||||||
v-model="filter.value"
|
|
||||||
v-show="valuedVerb()"
|
|
||||||
ref="valueInput">
|
|
||||||
</b-input>
|
|
||||||
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</div><!-- level-left -->
|
|
||||||
</div><!-- level -->
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="text/x-template" id="${grid.component}-template">
|
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5em;">
|
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5em;">
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; justify-content: space-between;">
|
<div style="display: flex; flex-direction: column; justify-content: end;">
|
||||||
<div></div>
|
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
% if grid.filterable:
|
% if getattr(grid, 'filterable', False):
|
||||||
## TODO: stop using |n filter
|
<form method="GET" @submit.prevent="applyFilters()">
|
||||||
${grid.render_filters(allow_save_defaults=allow_save_defaults)|n}
|
|
||||||
|
<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
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,7 +91,7 @@
|
||||||
|
|
||||||
<div class="grid-tools-wrapper">
|
<div class="grid-tools-wrapper">
|
||||||
% if tools:
|
% if tools:
|
||||||
<div class="grid-tools field buttons is-grouped is-pulled-right">
|
<div class="grid-tools">
|
||||||
## TODO: stop using |n filter
|
## TODO: stop using |n filter
|
||||||
${tools|n}
|
${tools|n}
|
||||||
</div>
|
</div>
|
||||||
|
@ -165,11 +102,13 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-table
|
<${b}-table
|
||||||
:data="visibleData"
|
:data="visibleData"
|
||||||
## :columns="columns"
|
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:row-class="getRowClass"
|
:row-class="getRowClass"
|
||||||
|
% if request.use_oruga:
|
||||||
|
tr-checked-class="is-checked"
|
||||||
|
% endif
|
||||||
|
|
||||||
% if request.rattail_config.getbool('tailbone', 'sticky_headers'):
|
% if request.rattail_config.getbool('tailbone', 'sticky_headers'):
|
||||||
sticky-header
|
sticky-header
|
||||||
|
@ -178,58 +117,75 @@
|
||||||
|
|
||||||
:checkable="checkable"
|
:checkable="checkable"
|
||||||
|
|
||||||
% if grid.checkboxes:
|
% if getattr(grid, 'checkboxes', False):
|
||||||
:checked-rows.sync="checkedRows"
|
% if request.use_oruga:
|
||||||
% if grid.clicking_row_checks_box:
|
v-model:checked-rows="checkedRows"
|
||||||
@click="rowClick"
|
% else:
|
||||||
% endif
|
:checked-rows.sync="checkedRows"
|
||||||
|
% endif
|
||||||
|
% if grid.clicking_row_checks_box:
|
||||||
|
@click="rowClick"
|
||||||
|
% endif
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if grid.check_handler:
|
% if getattr(grid, 'check_handler', None):
|
||||||
@check="${grid.check_handler}"
|
@check="${grid.check_handler}"
|
||||||
% endif
|
% endif
|
||||||
% if grid.check_all_handler:
|
% if getattr(grid, 'check_all_handler', None):
|
||||||
@check-all="${grid.check_all_handler}"
|
@check-all="${grid.check_all_handler}"
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
% if hasattr(grid, 'checkable'):
|
||||||
% if isinstance(grid.checkable, str):
|
% if isinstance(grid.checkable, str):
|
||||||
:is-row-checkable="${grid.row_checkable}"
|
:is-row-checkable="${grid.row_checkable}"
|
||||||
% elif grid.checkable:
|
% elif grid.checkable:
|
||||||
:is-row-checkable="row => row._checkable"
|
:is-row-checkable="row => row._checkable"
|
||||||
% endif
|
% 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
|
% 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"
|
@cellclick="cellClick"
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
:paginated="paginated"
|
## paging
|
||||||
:per-page="perPage"
|
% if grid.paginated:
|
||||||
:current-page="currentPage"
|
paginated
|
||||||
backend-pagination
|
pagination-size="${'small' if request.use_oruga else 'is-small'}"
|
||||||
:total="total"
|
:per-page="perPage"
|
||||||
@page-change="onPageChange"
|
: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?
|
## TODO: should let grid (or master view) decide how to set these?
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
|
@ -238,17 +194,15 @@
|
||||||
:hoverable="true"
|
:hoverable="true"
|
||||||
:narrowed="true">
|
:narrowed="true">
|
||||||
|
|
||||||
% for column in grid_columns:
|
% for column in grid.get_vue_columns():
|
||||||
<b-table-column field="${column['field']}"
|
<${b}-table-column field="${column['field']}"
|
||||||
label="${column['label']}"
|
label="${column['label']}"
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
:sortable="${json.dumps(column['sortable'])}"
|
:sortable="${json.dumps(column.get('sortable', False))|n}"
|
||||||
% if grid.is_searchable(column['field']):
|
:searchable="${json.dumps(column.get('searchable', False))|n}"
|
||||||
searchable
|
|
||||||
% endif
|
|
||||||
cell-class="c_${column['field']}"
|
cell-class="c_${column['field']}"
|
||||||
:visible="${json.dumps(column['visible'])}">
|
:visible="${json.dumps(column.get('visible', True))}">
|
||||||
% if column['field'] in grid.raw_renderers:
|
% if hasattr(grid, 'raw_renderers') and column['field'] in grid.raw_renderers:
|
||||||
${grid.raw_renderers[column['field']]()}
|
${grid.raw_renderers[column['field']]()}
|
||||||
% elif grid.is_linked(column['field']):
|
% elif grid.is_linked(column['field']):
|
||||||
<a :href="props.row._action_url_view"
|
<a :href="props.row._action_url_view"
|
||||||
|
@ -260,32 +214,31 @@
|
||||||
% else:
|
% else:
|
||||||
<span v-html="props.row.${column['field']}"></span>
|
<span v-html="props.row.${column['field']}"></span>
|
||||||
% endif
|
% endif
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
% endfor
|
% endfor
|
||||||
|
|
||||||
% if grid.main_actions or grid.more_actions:
|
% if grid.actions:
|
||||||
<b-table-column field="actions"
|
<${b}-table-column field="actions"
|
||||||
label="Actions"
|
label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
## TODO: we do not currently differentiate for "main vs. more"
|
## TODO: we do not currently differentiate for "main vs. more"
|
||||||
## here, but ideally we would tuck "more" away in a drawer etc.
|
## 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}"
|
<a v-if="props.row._action_url_${action.key}"
|
||||||
:href="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 ''}"
|
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}"
|
@click.prevent="${action.click_handler}"
|
||||||
% endif
|
% endif
|
||||||
% if action.target:
|
% if getattr(action, 'target', None):
|
||||||
target="${action.target}"
|
target="${action.target}"
|
||||||
% endif
|
% endif
|
||||||
>
|
>
|
||||||
${action.render_icon()|n}
|
${action.render_icon_and_label()}
|
||||||
${action.render_label()|n}
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
% endfor
|
% endfor
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
<template #empty>
|
<template #empty>
|
||||||
|
@ -294,7 +247,7 @@
|
||||||
<p>
|
<p>
|
||||||
<b-icon
|
<b-icon
|
||||||
pack="fas"
|
pack="fas"
|
||||||
icon="fas fa-sad-tear"
|
icon="sad-tear"
|
||||||
size="is-large">
|
size="is-large">
|
||||||
</b-icon>
|
</b-icon>
|
||||||
</p>
|
</p>
|
||||||
|
@ -306,43 +259,50 @@
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div style="display: flex; justify-content: space-between;">
|
<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"
|
<b-button type="is-primary"
|
||||||
size="is-small"
|
size="is-small"
|
||||||
@click="copyDirectLink()"
|
@click="copyDirectLink()"
|
||||||
title="Copy link to clipboard">
|
title="Copy link to clipboard">
|
||||||
<span><i class="fa fa-share-alt"></i></span>
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="share-alt" />
|
||||||
|
% else:
|
||||||
|
<span><i class="fa fa-share-alt"></i></span>
|
||||||
|
% endif
|
||||||
</b-button>
|
</b-button>
|
||||||
% else:
|
% else:
|
||||||
<div></div>
|
<div></div>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if grid.pageable:
|
% if grid.paginated:
|
||||||
<b-field grouped
|
<div v-if="pagerStats.first_item"
|
||||||
v-if="firstItem">
|
style="display: flex; gap: 0.5rem; align-items: center;">
|
||||||
<span class="control">
|
<span>
|
||||||
showing {{ firstItem.toLocaleString('en') }} - {{ lastItem.toLocaleString('en') }} of {{ total.toLocaleString('en') }} results;
|
showing
|
||||||
|
{{ renderNumber(pagerStats.first_item) }}
|
||||||
|
- {{ renderNumber(pagerStats.last_item) }}
|
||||||
|
of {{ renderNumber(pagerStats.item_count) }} results;
|
||||||
</span>
|
</span>
|
||||||
<b-select v-model="perPage"
|
<b-select v-model="perPage"
|
||||||
size="is-small"
|
size="is-small"
|
||||||
@input="loadAsyncData()">
|
@input="perPageUpdated">
|
||||||
% for value in grid.get_pagesize_options():
|
% for value in grid.get_pagesize_options():
|
||||||
<option value="${value}">${value}</option>
|
<option value="${value}">${value}</option>
|
||||||
% endfor
|
% endfor
|
||||||
</b-select>
|
</b-select>
|
||||||
<span class="control">
|
<span>
|
||||||
per page
|
per page
|
||||||
</span>
|
</span>
|
||||||
</b-field>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
||||||
## dummy input field needed for sharing links on *insecure* sites
|
## 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>
|
<b-input v-model="shareLink" ref="shareLink" v-show="shareLink"></b-input>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
@ -351,63 +311,72 @@
|
||||||
|
|
||||||
<script type="text/javascript">
|
<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,
|
loading: false,
|
||||||
ajaxDataUrl: ${json.dumps(grid.ajax_data_url)|n},
|
ajaxDataUrl: ${json.dumps(getattr(grid, 'ajax_data_url', request.path_url))|n},
|
||||||
|
|
||||||
data: ${grid.component_studly}CurrentData,
|
## nb. this tracks whether grid.fetchFirstData() happened
|
||||||
rowStatusMap: ${json.dumps(grid_data['row_status_map'])|n},
|
fetchedFirstData: false,
|
||||||
|
|
||||||
checkable: ${json.dumps(grid.checkboxes)|n},
|
savingDefaults: false,
|
||||||
% if grid.checkboxes:
|
|
||||||
|
data: ${grid.vue_component}CurrentData,
|
||||||
|
rowStatusMap: ${json.dumps(grid_data['row_status_map'] if grid_data is not Undefined else {})|n},
|
||||||
|
|
||||||
|
checkable: ${json.dumps(getattr(grid, 'checkboxes', False))|n},
|
||||||
|
% if getattr(grid, 'checkboxes', False):
|
||||||
checkedRows: ${grid_data['checked_rows_code']|n},
|
checkedRows: ${grid_data['checked_rows_code']|n},
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
paginated: ${json.dumps(grid.pageable)|n},
|
## paging
|
||||||
total: ${len(grid_data['data']) if static_data else grid_data['total_items']},
|
% if grid.paginated:
|
||||||
perPage: ${json.dumps(grid.pagesize if grid.pageable else None)|n},
|
pageSizeOptions: ${json.dumps(grid.pagesize_options)|n},
|
||||||
currentPage: ${json.dumps(grid.page if grid.pageable else None)|n},
|
perPage: ${json.dumps(grid.pagesize)|n},
|
||||||
firstItem: ${json.dumps(grid_data['first_item'] if grid.pageable else None)|n},
|
currentPage: ${json.dumps(grid.page)|n},
|
||||||
lastItem: ${json.dumps(grid_data['last_item'] if grid.pageable else None)|n},
|
% if grid.paginate_on_backend:
|
||||||
|
pagerStats: ${json.dumps(grid.get_vue_pager_stats())|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: [],
|
|
||||||
% endif
|
% 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
|
% endif
|
||||||
|
|
||||||
## filterable: ${json.dumps(grid.filterable)|n},
|
## filterable: ${json.dumps(grid.filterable)|n},
|
||||||
filters: ${json.dumps(filters_data 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 grid.filterable else None)|n},
|
filtersSequence: ${json.dumps(filters_sequence if getattr(grid, 'filterable', False) else None)|n},
|
||||||
addFilterTerm: '',
|
addFilterTerm: '',
|
||||||
addFilterShow: false,
|
addFilterShow: false,
|
||||||
|
|
||||||
## dummy input value needed for sharing links on *insecure* sites
|
## dummy input value needed for sharing links on *insecure* sites
|
||||||
% if request.scheme == 'http':
|
% if getattr(request, 'scheme', None) == 'http':
|
||||||
shareLink: null,
|
shareLink: null,
|
||||||
% endif
|
% endif
|
||||||
}
|
}
|
||||||
|
|
||||||
let ${grid.component_studly} = {
|
let ${grid.vue_component} = {
|
||||||
template: '#${grid.component}-template',
|
template: '#${grid.vue_tagname}-template',
|
||||||
|
|
||||||
mixins: [FormPosterMixin],
|
mixins: [FormPosterMixin],
|
||||||
|
|
||||||
|
@ -417,7 +386,34 @@
|
||||||
|
|
||||||
computed: {
|
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() {
|
addFilterChoices() {
|
||||||
|
// nb. this returns all choices available for "Add Filter" operation
|
||||||
|
|
||||||
// collect all filters, which are *not* already shown
|
// collect all filters, which are *not* already shown
|
||||||
let choices = []
|
let choices = []
|
||||||
|
@ -463,22 +459,40 @@
|
||||||
|
|
||||||
directLink() {
|
directLink() {
|
||||||
let params = new URLSearchParams(this.getAllParams())
|
let params = new URLSearchParams(this.getAllParams())
|
||||||
return `${request.current_route_url(_query=None)}?${'$'}{params}`
|
return `${request.path_url}?${'$'}{params}`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
% 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. so to
|
## TODO: there is a bug (?) which prevents the arrow
|
||||||
## work around that, we *disable* multi-sort until the
|
## from displaying for simple default single-column sort,
|
||||||
## component is mounted. seems to work for now..see also
|
## when multi-column sort is allowed for the table. for
|
||||||
## https://github.com/buefy/buefy/issues/2584
|
## now we work around that by waiting until mount to
|
||||||
this.allowMultiSort = true
|
## enable the multi-column support. see also
|
||||||
},
|
## https://github.com/buefy/buefy/issues/2584
|
||||||
|
mounted() {
|
||||||
|
this.allowMultiSort = true
|
||||||
|
},
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
% if grid.click_handlers:
|
renderNumber(value) {
|
||||||
|
if (value != undefined) {
|
||||||
|
return value.toLocaleString('en')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatAddFilterItem(filtr) {
|
||||||
|
if (!filtr.key) {
|
||||||
|
filtr = this.filters[filtr]
|
||||||
|
}
|
||||||
|
return filtr.label || filtr.key
|
||||||
|
},
|
||||||
|
|
||||||
|
% if getattr(grid, 'click_handlers', None):
|
||||||
cellClick(row, column, rowIndex, columnIndex) {
|
cellClick(row, column, rowIndex, columnIndex) {
|
||||||
% for key in grid.click_handlers:
|
% for key in grid.click_handlers:
|
||||||
if (column._props.field == '${key}') {
|
if (column._props.field == '${key}') {
|
||||||
|
@ -534,17 +548,18 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
getBasicParams() {
|
getBasicParams() {
|
||||||
let params = {}
|
const params = {
|
||||||
% if grid.sortable:
|
% if grid.paginated and grid.paginate_on_backend:
|
||||||
for (let i = 1; i <= this.backendSorters.length; i++) {
|
pagesize: this.perPage,
|
||||||
params['sort'+i+'key'] = this.backendSorters[i-1].field
|
page: this.currentPage,
|
||||||
params['sort'+i+'dir'] = this.backendSorters[i-1].order
|
% 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
|
% endif
|
||||||
% if grid.pageable:
|
|
||||||
params.pagesize = this.perPage
|
|
||||||
params.page = this.currentPage
|
|
||||||
% endif
|
|
||||||
return params
|
return params
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -568,6 +583,17 @@
|
||||||
...this.getFilterParams()}
|
...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,
|
## TODO: i noticed buefy docs show using `async` keyword here,
|
||||||
## so now i am too. knowing nothing at all of if/how this is
|
## so now i am too. knowing nothing at all of if/how this is
|
||||||
## supposed to improve anything. we shall see i guess
|
## supposed to improve anything. we shall see i guess
|
||||||
|
@ -575,40 +601,50 @@
|
||||||
|
|
||||||
if (params === undefined || params === null) {
|
if (params === undefined || params === null) {
|
||||||
params = new URLSearchParams(this.getBasicParams())
|
params = new URLSearchParams(this.getBasicParams())
|
||||||
params.append('partial', true)
|
} else {
|
||||||
params = params.toString()
|
params = new URLSearchParams(params)
|
||||||
}
|
}
|
||||||
|
if (!params.has('partial')) {
|
||||||
|
params.append('partial', true)
|
||||||
|
}
|
||||||
|
params = params.toString()
|
||||||
|
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(({ data }) => {
|
this.$http.get(`${'$'}{this.ajaxDataUrl}?${'$'}{params}`).then(response => {
|
||||||
if (!data.error) {
|
if (!response.data.error) {
|
||||||
${grid.component_studly}CurrentData = data.data
|
${grid.vue_component}CurrentData = response.data.data
|
||||||
this.data = ${grid.component_studly}CurrentData
|
this.data = ${grid.vue_component}CurrentData
|
||||||
this.rowStatusMap = data.row_status_map
|
% if grid.paginated and grid.paginate_on_backend:
|
||||||
this.total = data.total_items
|
this.pagerStats = response.data.pager_stats
|
||||||
this.firstItem = data.first_item
|
% endif
|
||||||
this.lastItem = data.last_item
|
this.rowStatusMap = response.data.row_status_map || {}
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.checkedRows = this.locateCheckedRows(data.checked_rows)
|
this.savingDefaults = false
|
||||||
|
this.checkedRows = this.locateCheckedRows(response.data.checked_rows || [])
|
||||||
if (success) {
|
if (success) {
|
||||||
success()
|
success()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$buefy.toast.open({
|
this.$buefy.toast.open({
|
||||||
message: data.error,
|
message: response.data.error,
|
||||||
type: 'is-danger',
|
type: 'is-danger',
|
||||||
duration: 2000, // 4 seconds
|
duration: 2000, // 4 seconds
|
||||||
})
|
})
|
||||||
this.loading = false
|
this.loading = false
|
||||||
|
this.savingDefaults = false
|
||||||
if (failure) {
|
if (failure) {
|
||||||
failure()
|
failure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
${grid.vue_component}CurrentData = []
|
||||||
this.data = []
|
this.data = []
|
||||||
this.total = 0
|
% if grid.paginated and grid.paginate_on_backend:
|
||||||
|
this.pagerStats = {}
|
||||||
|
% endif
|
||||||
this.loading = false
|
this.loading = false
|
||||||
|
this.savingDefaults = false
|
||||||
if (failure) {
|
if (failure) {
|
||||||
failure()
|
failure()
|
||||||
}
|
}
|
||||||
|
@ -633,50 +669,84 @@
|
||||||
this.loadAsyncData()
|
this.loadAsyncData()
|
||||||
},
|
},
|
||||||
|
|
||||||
onSort(field, order, event) {
|
perPageUpdated(value) {
|
||||||
|
|
||||||
if (event.ctrlKey) {
|
// nb. buefy passes value, oruga passes event
|
||||||
|
if (value.target) {
|
||||||
// engage or enhance multi-column sorting
|
value = event.target.value
|
||||||
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
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// sort by single column only
|
|
||||||
this.backendSorters = [{field, order}]
|
|
||||||
this.sortingPriority = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// always reset to first page when changing sort options
|
this.loadAsyncData({
|
||||||
// TODO: i mean..right? would we ever not want that?
|
pagesize: value,
|
||||||
this.currentPage = 1
|
})
|
||||||
this.loadAsyncData()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
sortingPriorityRemoved(field) {
|
% if grid.sortable and grid.sort_on_backend:
|
||||||
|
|
||||||
// prune field from active sorters
|
onSort(field, order, event) {
|
||||||
this.backendSorters = this.backendSorters.filter(
|
|
||||||
(sorter) => sorter.field !== field)
|
|
||||||
|
|
||||||
// nb. must keep active sorter list "as-is" even if
|
## nb. buefy passes field name; oruga passes field object
|
||||||
// there is only one sorter; buefy seems to expect it
|
% if request.use_oruga:
|
||||||
this.sortingPriority = this.backendSorters
|
field = field.field
|
||||||
|
% endif
|
||||||
|
|
||||||
this.loadAsyncData()
|
% if grid.sort_multiple:
|
||||||
},
|
|
||||||
|
// did user ctrl-click the column header?
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
|
||||||
|
// toggle direction for existing, or add new sorter
|
||||||
|
const sorter = this.sorters.filter(s => s.field === field)[0]
|
||||||
|
if (sorter) {
|
||||||
|
sorter.order = sorter.order === 'desc' ? 'asc' : 'desc'
|
||||||
|
} else {
|
||||||
|
this.sorters.push({field, order})
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply multi-column sorting
|
||||||
|
this.sortingPriority = this.sorters
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
|
// sort by single column only
|
||||||
|
this.sorters = [{field, order}]
|
||||||
|
|
||||||
|
% if grid.sort_multiple:
|
||||||
|
// multi-column sort not engaged
|
||||||
|
this.sortingPriority = []
|
||||||
|
}
|
||||||
|
% endif
|
||||||
|
|
||||||
|
// nb. always reset to first page when sorting changes
|
||||||
|
this.currentPage = 1
|
||||||
|
this.loadAsyncData()
|
||||||
|
},
|
||||||
|
|
||||||
|
% if grid.sort_multiple:
|
||||||
|
|
||||||
|
sortingPriorityRemoved(field) {
|
||||||
|
|
||||||
|
// 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() {
|
resetView() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
// use current url proper, plus reset param
|
// 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
|
// add current hash, to preserve that in redirect
|
||||||
if (location.hash) {
|
if (location.hash) {
|
||||||
|
@ -686,26 +756,34 @@
|
||||||
location.href = url
|
location.href = url
|
||||||
},
|
},
|
||||||
|
|
||||||
addFilterButton(event) {
|
addFilterInit() {
|
||||||
this.addFilterShow = true
|
this.addFilterShow = true
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
const input = this.$refs.addFilterAutocomplete.$el.querySelector('input')
|
||||||
|
input.addEventListener('keydown', this.addFilterKeydown)
|
||||||
this.$refs.addFilterAutocomplete.focus()
|
this.$refs.addFilterAutocomplete.focus()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addFilterHide() {
|
||||||
|
const input = this.$refs.addFilterAutocomplete.$el.querySelector('input')
|
||||||
|
input.removeEventListener('keydown', this.addFilterKeydown)
|
||||||
|
this.addFilterTerm = ''
|
||||||
|
this.addFilterShow = false
|
||||||
|
},
|
||||||
|
|
||||||
addFilterKeydown(event) {
|
addFilterKeydown(event) {
|
||||||
|
|
||||||
// ESC will clear searchbox
|
// ESC will clear searchbox
|
||||||
if (event.which == 27) {
|
if (event.which == 27) {
|
||||||
this.addFilterTerm = ''
|
this.addFilterHide()
|
||||||
this.addFilterShow = false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addFilterSelect(filtr) {
|
addFilterSelect(filtr) {
|
||||||
this.addFilter(filtr.key)
|
this.addFilter(filtr.key)
|
||||||
this.addFilterTerm = ''
|
this.addFilterHide()
|
||||||
this.addFilterShow = false
|
|
||||||
},
|
},
|
||||||
|
|
||||||
addFilter(filter_key) {
|
addFilter(filter_key) {
|
||||||
|
@ -805,6 +883,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
saveDefaults() {
|
saveDefaults() {
|
||||||
|
this.savingDefaults = true
|
||||||
|
|
||||||
// apply current filters as normal, but add special directive
|
// apply current filters as normal, but add special directive
|
||||||
this.applyFilters({'save-current-filters-as-defaults': true})
|
this.applyFilters({'save-current-filters-as-defaults': true})
|
||||||
|
@ -841,7 +920,7 @@
|
||||||
} else {
|
} else {
|
||||||
this.checkedRows.push(row)
|
this.checkedRows.push(row)
|
||||||
}
|
}
|
||||||
% if grid.check_handler:
|
% if getattr(grid, 'check_handler', None):
|
||||||
this.${grid.check_handler}(this.checkedRows, row)
|
this.${grid.check_handler}(this.checkedRows, row)
|
||||||
% endif
|
% endif
|
||||||
},
|
},
|
||||||
|
|
350
tailbone/templates/grids/filter-components.mako
Normal file
350
tailbone/templates/grids/filter-components.mako
Normal file
|
@ -0,0 +1,350 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
|
||||||
|
<%def name="make_grid_filter_components()">
|
||||||
|
${self.make_grid_filter_numeric_value_component()}
|
||||||
|
${self.make_grid_filter_date_value_component()}
|
||||||
|
${self.make_grid_filter_component()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_grid_filter_numeric_value_component()">
|
||||||
|
<% request.register_component('grid-filter-numeric-value', 'GridFilterNumericValue') %>
|
||||||
|
<script type="text/x-template" id="grid-filter-numeric-value-template">
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item">
|
||||||
|
<b-input v-model="startValue"
|
||||||
|
ref="startValue"
|
||||||
|
@input="startValueChanged">
|
||||||
|
</b-input>
|
||||||
|
</div>
|
||||||
|
<div v-show="wantsRange"
|
||||||
|
class="level-item">
|
||||||
|
and
|
||||||
|
</div>
|
||||||
|
<div v-show="wantsRange"
|
||||||
|
class="level-item">
|
||||||
|
<b-input v-model="endValue"
|
||||||
|
ref="endValue"
|
||||||
|
@input="endValueChanged">
|
||||||
|
</b-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const GridFilterNumericValue = {
|
||||||
|
template: '#grid-filter-numeric-value-template',
|
||||||
|
props: {
|
||||||
|
${'modelValue' if request.use_oruga else 'value'}: String,
|
||||||
|
wantsRange: Boolean,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const value = this.${'modelValue' if request.use_oruga else 'value'}
|
||||||
|
const {startValue, endValue} = this.parseValue(value)
|
||||||
|
return {
|
||||||
|
startValue,
|
||||||
|
endValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// when changing from e.g. 'equal' to 'between' filter verbs,
|
||||||
|
// must proclaim new filter value, to reflect (lack of) range
|
||||||
|
wantsRange(val) {
|
||||||
|
if (val) {
|
||||||
|
this.$emit('input', this.startValue + '|' + this.endValue)
|
||||||
|
} else {
|
||||||
|
this.$emit('input', this.startValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
${'modelValue' if request.use_oruga else 'value'}(to, from) {
|
||||||
|
const parsed = this.parseValue(to)
|
||||||
|
this.startValue = parsed.startValue
|
||||||
|
this.endValue = parsed.endValue
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.startValue.focus()
|
||||||
|
},
|
||||||
|
startValueChanged(value) {
|
||||||
|
if (this.wantsRange) {
|
||||||
|
value += '|' + this.endValue
|
||||||
|
}
|
||||||
|
this.$emit("${'update:modelValue' if request.use_oruga else 'input'}", value)
|
||||||
|
},
|
||||||
|
endValueChanged(value) {
|
||||||
|
value = this.startValue + '|' + value
|
||||||
|
this.$emit("${'update:modelValue' if request.use_oruga else 'input'}", value)
|
||||||
|
},
|
||||||
|
|
||||||
|
parseValue(value) {
|
||||||
|
let startValue = null
|
||||||
|
let endValue = null
|
||||||
|
if (this.wantsRange) {
|
||||||
|
if (value.includes('|')) {
|
||||||
|
let values = value.split('|')
|
||||||
|
if (values.length == 2) {
|
||||||
|
startValue = values[0]
|
||||||
|
endValue = values[1]
|
||||||
|
} else {
|
||||||
|
startValue = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startValue = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startValue = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
startValue,
|
||||||
|
endValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.component('grid-filter-numeric-value', GridFilterNumericValue)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_grid_filter_date_value_component()">
|
||||||
|
<% request.register_component('grid-filter-date-value', 'GridFilterDateValue') %>
|
||||||
|
<script type="text/x-template" id="grid-filter-date-value-template">
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item">
|
||||||
|
<tailbone-datepicker v-model="startDate"
|
||||||
|
ref="startDate"
|
||||||
|
@${'update:model-value' if request.use_oruga else 'input'}="startDateChanged">
|
||||||
|
</tailbone-datepicker>
|
||||||
|
</div>
|
||||||
|
<div v-show="dateRange"
|
||||||
|
class="level-item">
|
||||||
|
and
|
||||||
|
</div>
|
||||||
|
<div v-show="dateRange"
|
||||||
|
class="level-item">
|
||||||
|
<tailbone-datepicker v-model="endDate"
|
||||||
|
ref="endDate"
|
||||||
|
@${'update:model-value' if request.use_oruga else 'input'}="endDateChanged">
|
||||||
|
</tailbone-datepicker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const GridFilterDateValue = {
|
||||||
|
template: '#grid-filter-date-value-template',
|
||||||
|
props: {
|
||||||
|
${'modelValue' if request.use_oruga else 'value'}: String,
|
||||||
|
dateRange: Boolean,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
let startDate = null
|
||||||
|
let endDate = null
|
||||||
|
let value = this.${'modelValue' if request.use_oruga else 'value'}
|
||||||
|
if (value) {
|
||||||
|
|
||||||
|
if (this.dateRange) {
|
||||||
|
let values = value.split('|')
|
||||||
|
if (values.length == 2) {
|
||||||
|
startDate = this.parseDate(values[0])
|
||||||
|
endDate = this.parseDate(values[1])
|
||||||
|
} else { // no end date specified?
|
||||||
|
startDate = this.parseDate(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else { // not a range, so start date only
|
||||||
|
startDate = this.parseDate(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
this.$refs.startDate.focus()
|
||||||
|
},
|
||||||
|
formatDate(date) {
|
||||||
|
if (date === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (typeof(date) == 'string') {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
// just need to convert to simple ISO date format here, seems
|
||||||
|
// like there should be a more obvious way to do that?
|
||||||
|
var year = date.getFullYear()
|
||||||
|
var month = date.getMonth() + 1
|
||||||
|
var day = date.getDate()
|
||||||
|
month = month < 10 ? '0' + month : month
|
||||||
|
day = day < 10 ? '0' + day : day
|
||||||
|
return year + '-' + month + '-' + day
|
||||||
|
},
|
||||||
|
parseDate(value) {
|
||||||
|
if (value) {
|
||||||
|
// note, this assumes classic YYYY-MM-DD (i.e. ISO?) format
|
||||||
|
const parts = value.split('-')
|
||||||
|
return new Date(parts[0], parseInt(parts[1]) - 1, parts[2])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startDateChanged(value) {
|
||||||
|
value = this.formatDate(value)
|
||||||
|
if (this.dateRange) {
|
||||||
|
value += '|' + this.formatDate(this.endDate)
|
||||||
|
}
|
||||||
|
this.$emit("${'update:modelValue' if request.use_oruga else 'input'}", value)
|
||||||
|
},
|
||||||
|
endDateChanged(value) {
|
||||||
|
value = this.formatDate(this.startDate) + '|' + this.formatDate(value)
|
||||||
|
this.$emit("${'update:modelValue' if request.use_oruga else 'input'}", value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.component('grid-filter-date-value', GridFilterDateValue)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_grid_filter_component()">
|
||||||
|
<% request.register_component('grid-filter', 'GridFilter') %>
|
||||||
|
<script type="text/x-template" id="grid-filter-template">
|
||||||
|
<div class="filter"
|
||||||
|
v-show="filter.visible"
|
||||||
|
style="display: flex; gap: 0.5rem;">
|
||||||
|
|
||||||
|
<div class="filter-fieldname">
|
||||||
|
<b-button @click="filter.active = !filter.active"
|
||||||
|
icon-pack="fas"
|
||||||
|
:icon-left="filter.active ? 'check' : null">
|
||||||
|
{{ filter.label }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="filter.active"
|
||||||
|
style="display: flex; gap: 0.5rem;">
|
||||||
|
|
||||||
|
<b-select v-model="filter.verb"
|
||||||
|
@input="focusValue()"
|
||||||
|
class="filter-verb">
|
||||||
|
<option v-for="verb in filter.verbs"
|
||||||
|
:key="verb"
|
||||||
|
:value="verb">
|
||||||
|
{{ filter.verb_labels[verb] }}
|
||||||
|
</option>
|
||||||
|
</b-select>
|
||||||
|
|
||||||
|
## only one of the following "value input" elements will be rendered
|
||||||
|
|
||||||
|
<grid-filter-date-value v-if="filter.data_type == 'date'"
|
||||||
|
v-model="filter.value"
|
||||||
|
v-show="valuedVerb()"
|
||||||
|
:date-range="filter.verb == 'between'"
|
||||||
|
ref="valueInput">
|
||||||
|
</grid-filter-date-value>
|
||||||
|
|
||||||
|
<b-select v-if="filter.data_type == 'choice'"
|
||||||
|
v-model="filter.value"
|
||||||
|
v-show="valuedVerb()"
|
||||||
|
ref="valueInput">
|
||||||
|
<option v-for="choice in filter.choices"
|
||||||
|
:key="choice"
|
||||||
|
:value="choice">
|
||||||
|
{{ filter.choice_labels[choice] || choice }}
|
||||||
|
</option>
|
||||||
|
</b-select>
|
||||||
|
|
||||||
|
<grid-filter-numeric-value v-if="filter.data_type == 'number'"
|
||||||
|
v-model="filter.value"
|
||||||
|
v-show="valuedVerb()"
|
||||||
|
:wants-range="filter.verb == 'between'"
|
||||||
|
ref="valueInput">
|
||||||
|
</grid-filter-numeric-value>
|
||||||
|
|
||||||
|
<b-input v-if="filter.data_type == 'string' && !multiValuedVerb()"
|
||||||
|
v-model="filter.value"
|
||||||
|
v-show="valuedVerb()"
|
||||||
|
ref="valueInput">
|
||||||
|
</b-input>
|
||||||
|
|
||||||
|
<b-input v-if="filter.data_type == 'string' && multiValuedVerb()"
|
||||||
|
type="textarea"
|
||||||
|
v-model="filter.value"
|
||||||
|
v-show="valuedVerb()"
|
||||||
|
ref="valueInput">
|
||||||
|
</b-input>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const GridFilter = {
|
||||||
|
template: '#grid-filter-template',
|
||||||
|
props: {
|
||||||
|
filter: Object
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
changeVerb() {
|
||||||
|
// set focus to value input, "as quickly as we can"
|
||||||
|
this.$nextTick(function() {
|
||||||
|
this.focusValue()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
valuedVerb() {
|
||||||
|
/* this returns true if the filter's current verb should expose value input(s) */
|
||||||
|
|
||||||
|
// if filter has no "valueless" verbs, then all verbs should expose value inputs
|
||||||
|
if (!this.filter.valueless_verbs) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if filter *does* have valueless verbs, check if "current" verb is valueless
|
||||||
|
if (this.filter.valueless_verbs.includes(this.filter.verb)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// current verb is *not* valueless
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
multiValuedVerb() {
|
||||||
|
/* this returns true if the filter's current verb should expose a multi-value input */
|
||||||
|
|
||||||
|
// if filter has no "multi-value" verbs then we safely assume false
|
||||||
|
if (!this.filter.multiple_value_verbs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if filter *does* have multi-value verbs, see if "current" is one
|
||||||
|
if (this.filter.multiple_value_verbs.includes(this.filter.verb)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// current verb is not multi-value
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
|
focusValue: function() {
|
||||||
|
this.$refs.valueInput.focus()
|
||||||
|
// this.$refs.valueInput.select()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.component('grid-filter', GridFilter)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</%def>
|
|
@ -1,70 +0,0 @@
|
||||||
## -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
<form action="${form.action_url}" method="GET" @submit.prevent="applyFilters()">
|
|
||||||
|
|
||||||
<grid-filter v-for="key in filtersSequence"
|
|
||||||
:key="key"
|
|
||||||
:filter="filters[key]"
|
|
||||||
ref="gridFilters">
|
|
||||||
</grid-filter>
|
|
||||||
|
|
||||||
<b-field grouped>
|
|
||||||
|
|
||||||
<b-button type="is-primary"
|
|
||||||
native-type="submit"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="check"
|
|
||||||
class="control">
|
|
||||||
Apply Filters
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<b-button v-if="!addFilterShow"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="plus"
|
|
||||||
class="control"
|
|
||||||
@click="addFilterButton">
|
|
||||||
Add Filter
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<b-autocomplete v-if="addFilterShow"
|
|
||||||
ref="addFilterAutocomplete"
|
|
||||||
:data="addFilterChoices"
|
|
||||||
v-model="addFilterTerm"
|
|
||||||
placeholder="Add Filter"
|
|
||||||
field="key"
|
|
||||||
:custom-formatter="filtr => filtr.label"
|
|
||||||
open-on-focus
|
|
||||||
keep-first
|
|
||||||
icon-pack="fas"
|
|
||||||
clearable
|
|
||||||
clear-on-select
|
|
||||||
@select="addFilterSelect"
|
|
||||||
@keydown.native="addFilterKeydown">
|
|
||||||
</b-autocomplete>
|
|
||||||
|
|
||||||
<b-button @click="resetView()"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="home"
|
|
||||||
class="control">
|
|
||||||
Default View
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<b-button @click="clearFilters()"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="trash"
|
|
||||||
class="control">
|
|
||||||
No Filters
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
% if allow_save_defaults and request.user:
|
|
||||||
<b-button @click="saveDefaults()"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="save"
|
|
||||||
class="control">
|
|
||||||
Save Defaults
|
|
||||||
</b-button>
|
|
||||||
% endif
|
|
||||||
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</form>
|
|
3
tailbone/templates/grids/vue_template.mako
Normal file
3
tailbone/templates/grids/vue_template.mako
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/grids/complete.mako" />
|
||||||
|
${parent.body()}
|
|
@ -1,33 +1,7 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/page.mako" />
|
<%inherit file="wuttaweb:templates/home.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>
|
|
||||||
|
|
||||||
|
## DEPRECATED; remains for back-compat
|
||||||
<%def name="render_this_page()">
|
<%def name="render_this_page()">
|
||||||
${self.page_content()}
|
${self.page_content()}
|
||||||
</%def>
|
</%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()}
|
|
||||||
|
|
|
@ -6,61 +6,65 @@
|
||||||
|
|
||||||
<h3 class="is-size-3">Designated Handlers</h3>
|
<h3 class="is-size-3">Designated Handlers</h3>
|
||||||
|
|
||||||
<b-table :data="handlersData"
|
<${b}-table :data="handlersData"
|
||||||
narrowed
|
narrowed
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
:default-sort="['host_title', 'asc']">
|
:default-sort="['host_title', 'asc']">
|
||||||
<b-table-column field="host_title"
|
<${b}-table-column field="host_title"
|
||||||
label="Data Source"
|
label="Data Source"
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
sortable>
|
sortable>
|
||||||
{{ props.row.host_title }}
|
{{ props.row.host_title }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="local_title"
|
<${b}-table-column field="local_title"
|
||||||
label="Data Target"
|
label="Data Target"
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
sortable>
|
sortable>
|
||||||
{{ props.row.local_title }}
|
{{ props.row.local_title }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="direction"
|
<${b}-table-column field="direction"
|
||||||
label="Direction"
|
label="Direction"
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
sortable>
|
sortable>
|
||||||
{{ props.row.direction_display }}
|
{{ props.row.direction_display }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="handler_spec"
|
<${b}-table-column field="handler_spec"
|
||||||
label="Handler Spec"
|
label="Handler Spec"
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
sortable>
|
sortable>
|
||||||
{{ props.row.handler_spec }}
|
{{ props.row.handler_spec }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="cmd"
|
<${b}-table-column field="cmd"
|
||||||
label="Command"
|
label="Command"
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
sortable>
|
sortable>
|
||||||
{{ props.row.command }} {{ props.row.subcommand }}
|
{{ props.row.command }} {{ props.row.subcommand }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="runas"
|
<${b}-table-column field="runas"
|
||||||
label="Default Runas"
|
label="Default Runas"
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
sortable>
|
sortable>
|
||||||
{{ props.row.default_runas }}
|
{{ props.row.default_runas }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column label="Actions"
|
<${b}-table-column label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<a href="#" class="grid-action"
|
<a href="#" class="grid-action"
|
||||||
@click.prevent="editHandler(props.row)">
|
@click.prevent="editHandler(props.row)">
|
||||||
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="edit" />
|
||||||
|
% else:
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
|
% endif
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<template slot="empty">
|
<template #empty>
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="content has-text-grey has-text-centered">
|
<div class="content has-text-grey has-text-centered">
|
||||||
<p>
|
<p>
|
||||||
<b-icon
|
<b-icon
|
||||||
pack="fas"
|
pack="fas"
|
||||||
icon="fas fa-sad-tear"
|
icon="sad-tear"
|
||||||
size="is-large">
|
size="is-large">
|
||||||
</b-icon>
|
</b-icon>
|
||||||
</p>
|
</p>
|
||||||
|
@ -68,7 +72,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
||||||
<b-modal :active.sync="editHandlerShowDialog">
|
<b-modal :active.sync="editHandlerShowDialog">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -140,9 +144,9 @@
|
||||||
</b-modal>
|
</b-modal>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
ThisPageData.handlersData = ${json.dumps(handlers_data)|n}
|
ThisPageData.handlersData = ${json.dumps(handlers_data)|n}
|
||||||
|
|
||||||
|
@ -199,6 +203,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -63,28 +63,26 @@
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
${form.component_studly}Data.submittingRun = false
|
${form.vue_component}Data.submittingRun = false
|
||||||
${form.component_studly}Data.submittingExplain = false
|
${form.vue_component}Data.submittingExplain = false
|
||||||
${form.component_studly}Data.runJob = false
|
${form.vue_component}Data.runJob = false
|
||||||
|
|
||||||
${form.component_studly}.methods.submitRun = function() {
|
${form.vue_component}.methods.submitRun = function() {
|
||||||
this.submittingRun = true
|
this.submittingRun = true
|
||||||
this.runJob = true
|
this.runJob = true
|
||||||
this.$nextTick(() => {
|
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.submittingExplain = true
|
||||||
this.$refs.${form.component_studly}.submit()
|
this.$refs.${form.vue_component}.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -1,78 +1,17 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/form.mako" />
|
<%inherit file="wuttaweb:templates/auth/login.mako" />
|
||||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
|
||||||
|
|
||||||
<%def name="title()">Login</%def>
|
|
||||||
|
|
||||||
|
## TODO: this will not be needed with wuttaform
|
||||||
<%def name="extra_styles()">
|
<%def name="extra_styles()">
|
||||||
${parent.extra_styles()}
|
${parent.extra_styles()}
|
||||||
<style type="text/css">
|
<style>
|
||||||
.logo img {
|
.card-content .buttons {
|
||||||
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 {
|
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="logo()">
|
## DEPRECATED; remains for back-compat
|
||||||
${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>
|
|
||||||
|
|
||||||
<%def name="render_this_page()">
|
<%def name="render_this_page()">
|
||||||
${self.page_content()}
|
${self.page_content()}
|
||||||
</%def>
|
</%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">
|
|
||||||
|
|
||||||
TailboneForm.mounted = function() {
|
|
||||||
this.$refs.username.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
TailboneForm.methods.usernameKeydown = function(event) {
|
|
||||||
if (event.which == 13) {
|
|
||||||
event.preventDefault()
|
|
||||||
this.$refs.password.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -22,48 +22,56 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="block" style="padding-left: 2rem; display: flex;">
|
<div class="block" style="padding-left: 2rem; display: flex;">
|
||||||
|
|
||||||
<b-table :data="overnightTasks">
|
<${b}-table :data="overnightTasks">
|
||||||
<!-- <b-table-column field="key" -->
|
<!-- <${b}-table-column field="key" -->
|
||||||
<!-- label="Key" -->
|
<!-- label="Key" -->
|
||||||
<!-- sortable> -->
|
<!-- sortable> -->
|
||||||
<!-- {{ props.row.key }} -->
|
<!-- {{ props.row.key }} -->
|
||||||
<!-- </b-table-column> -->
|
<!-- </${b}-table-column> -->
|
||||||
<b-table-column field="key"
|
<${b}-table-column field="key"
|
||||||
label="Key"
|
label="Key"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.key }}
|
{{ props.row.key }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="description"
|
<${b}-table-column field="description"
|
||||||
label="Description"
|
label="Description"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.description }}
|
{{ props.row.description }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="class_name"
|
<${b}-table-column field="class_name"
|
||||||
label="Class Name"
|
label="Class Name"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.class_name }}
|
{{ props.row.class_name }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="script"
|
<${b}-table-column field="script"
|
||||||
label="Script"
|
label="Script"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.script }}
|
{{ props.row.script }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column label="Actions"
|
<${b}-table-column label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
@click.prevent="overnightTaskEdit(props.row)">
|
@click.prevent="overnightTaskEdit(props.row)">
|
||||||
<i class="fas fa-edit"></i>
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="edit" />
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
% endif
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="#"
|
<a href="#"
|
||||||
class="has-text-danger"
|
class="has-text-danger"
|
||||||
@click.prevent="overnightTaskDelete(props.row)">
|
@click.prevent="overnightTaskDelete(props.row)">
|
||||||
<i class="fas fa-trash"></i>
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="trash" />
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
% endif
|
||||||
Delete
|
Delete
|
||||||
</a>
|
</a>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
||||||
<b-modal has-modal-card
|
<b-modal has-modal-card
|
||||||
:active.sync="overnightTaskShowDialog">
|
:active.sync="overnightTaskShowDialog">
|
||||||
|
@ -77,31 +85,31 @@
|
||||||
<b-field label="Key"
|
<b-field label="Key"
|
||||||
:type="overnightTaskKey ? null : 'is-danger'">
|
:type="overnightTaskKey ? null : 'is-danger'">
|
||||||
<b-input v-model.trim="overnightTaskKey"
|
<b-input v-model.trim="overnightTaskKey"
|
||||||
ref="overnightTaskKey">
|
ref="overnightTaskKey"
|
||||||
</b-input>
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field label="Description"
|
<b-field label="Description"
|
||||||
:type="overnightTaskDescription ? null : 'is-danger'">
|
:type="overnightTaskDescription ? null : 'is-danger'">
|
||||||
<b-input v-model.trim="overnightTaskDescription"
|
<b-input v-model.trim="overnightTaskDescription"
|
||||||
ref="overnightTaskDescription">
|
ref="overnightTaskDescription"
|
||||||
</b-input>
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field label="Module">
|
<b-field label="Module">
|
||||||
<b-input v-model.trim="overnightTaskModule">
|
<b-input v-model.trim="overnightTaskModule"
|
||||||
</b-input>
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field label="Class Name">
|
<b-field label="Class Name">
|
||||||
<b-input v-model.trim="overnightTaskClass">
|
<b-input v-model.trim="overnightTaskClass"
|
||||||
</b-input>
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field label="Script">
|
<b-field label="Script">
|
||||||
<b-input v-model.trim="overnightTaskScript">
|
<b-input v-model.trim="overnightTaskScript"
|
||||||
</b-input>
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field label="Notes">
|
<b-field label="Notes">
|
||||||
<b-input v-model.trim="overnightTaskNotes"
|
<b-input v-model.trim="overnightTaskNotes"
|
||||||
type="textarea">
|
type="textarea"
|
||||||
</b-input>
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -139,48 +147,56 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="block" style="padding-left: 2rem; display: flex;">
|
<div class="block" style="padding-left: 2rem; display: flex;">
|
||||||
|
|
||||||
<b-table :data="backfillTasks">
|
<${b}-table :data="backfillTasks">
|
||||||
<b-table-column field="key"
|
<${b}-table-column field="key"
|
||||||
label="Key"
|
label="Key"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.key }}
|
{{ props.row.key }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="description"
|
<${b}-table-column field="description"
|
||||||
label="Description"
|
label="Description"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.description }}
|
{{ props.row.description }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="script"
|
<${b}-table-column field="script"
|
||||||
label="Script"
|
label="Script"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.script }}
|
{{ props.row.script }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="forward"
|
<${b}-table-column field="forward"
|
||||||
label="Orientation"
|
label="Orientation"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.forward ? "Forward" : "Backward" }}
|
{{ props.row.forward ? "Forward" : "Backward" }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="target_date"
|
<${b}-table-column field="target_date"
|
||||||
label="Target Date"
|
label="Target Date"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.target_date }}
|
{{ props.row.target_date }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column label="Actions"
|
<${b}-table-column label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
@click.prevent="backfillTaskEdit(props.row)">
|
@click.prevent="backfillTaskEdit(props.row)">
|
||||||
<i class="fas fa-edit"></i>
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="edit" />
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
% endif
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="#"
|
<a href="#"
|
||||||
class="has-text-danger"
|
class="has-text-danger"
|
||||||
@click.prevent="backfillTaskDelete(props.row)">
|
@click.prevent="backfillTaskDelete(props.row)">
|
||||||
<i class="fas fa-trash"></i>
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="trash" />
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
% endif
|
||||||
Delete
|
Delete
|
||||||
</a>
|
</a>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
||||||
<b-modal has-modal-card
|
<b-modal has-modal-card
|
||||||
:active.sync="backfillTaskShowDialog">
|
:active.sync="backfillTaskShowDialog">
|
||||||
|
@ -194,19 +210,19 @@
|
||||||
<b-field label="Key"
|
<b-field label="Key"
|
||||||
:type="backfillTaskKey ? null : 'is-danger'">
|
:type="backfillTaskKey ? null : 'is-danger'">
|
||||||
<b-input v-model.trim="backfillTaskKey"
|
<b-input v-model.trim="backfillTaskKey"
|
||||||
ref="backfillTaskKey">
|
ref="backfillTaskKey"
|
||||||
</b-input>
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field label="Description"
|
<b-field label="Description"
|
||||||
:type="backfillTaskDescription ? null : 'is-danger'">
|
:type="backfillTaskDescription ? null : 'is-danger'">
|
||||||
<b-input v-model.trim="backfillTaskDescription"
|
<b-input v-model.trim="backfillTaskDescription"
|
||||||
ref="backfillTaskDescription">
|
ref="backfillTaskDescription"
|
||||||
</b-input>
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field label="Script"
|
<b-field label="Script"
|
||||||
:type="backfillTaskScript ? null : 'is-danger'">
|
:type="backfillTaskScript ? null : 'is-danger'">
|
||||||
<b-input v-model.trim="backfillTaskScript">
|
<b-input v-model.trim="backfillTaskScript"
|
||||||
</b-input>
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field grouped>
|
<b-field grouped>
|
||||||
<b-field label="Orientation">
|
<b-field label="Orientation">
|
||||||
|
@ -222,8 +238,8 @@
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field label="Notes">
|
<b-field label="Notes">
|
||||||
<b-input v-model.trim="backfillTaskNotes"
|
<b-input v-model.trim="backfillTaskNotes"
|
||||||
type="textarea">
|
type="textarea"
|
||||||
</b-input>
|
expanded />
|
||||||
</b-field>
|
</b-field>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -252,7 +268,8 @@
|
||||||
expanded>
|
expanded>
|
||||||
<b-input name="rattail.luigi.url"
|
<b-input name="rattail.luigi.url"
|
||||||
v-model="simpleSettings['rattail.luigi.url']"
|
v-model="simpleSettings['rattail.luigi.url']"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true"
|
||||||
|
expanded>
|
||||||
</b-input>
|
</b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
@ -261,7 +278,8 @@
|
||||||
expanded>
|
expanded>
|
||||||
<b-input name="rattail.luigi.scheduler.supervisor_process_name"
|
<b-input name="rattail.luigi.scheduler.supervisor_process_name"
|
||||||
v-model="simpleSettings['rattail.luigi.scheduler.supervisor_process_name']"
|
v-model="simpleSettings['rattail.luigi.scheduler.supervisor_process_name']"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true"
|
||||||
|
expanded>
|
||||||
</b-input>
|
</b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
@ -270,7 +288,8 @@
|
||||||
expanded>
|
expanded>
|
||||||
<b-input name="rattail.luigi.scheduler.restart_command"
|
<b-input name="rattail.luigi.scheduler.restart_command"
|
||||||
v-model="simpleSettings['rattail.luigi.scheduler.restart_command']"
|
v-model="simpleSettings['rattail.luigi.scheduler.restart_command']"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true"
|
||||||
|
expanded>
|
||||||
</b-input>
|
</b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
@ -278,9 +297,9 @@
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
ThisPageData.overnightTasks = ${json.dumps(overnight_tasks)|n}
|
ThisPageData.overnightTasks = ${json.dumps(overnight_tasks)|n}
|
||||||
ThisPageData.overnightTaskShowDialog = false
|
ThisPageData.overnightTaskShowDialog = false
|
||||||
|
@ -406,6 +425,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -53,25 +53,25 @@
|
||||||
|
|
||||||
<h3 class="block is-size-3">Overnight Tasks</h3>
|
<h3 class="block is-size-3">Overnight Tasks</h3>
|
||||||
|
|
||||||
<b-table :data="overnightTasks" hoverable>
|
<${b}-table :data="overnightTasks" hoverable>
|
||||||
<b-table-column field="description"
|
<${b}-table-column field="description"
|
||||||
label="Description"
|
label="Description"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.description }}
|
{{ props.row.description }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="script"
|
<${b}-table-column field="script"
|
||||||
label="Command"
|
label="Command"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.script || props.row.class_name }}
|
{{ props.row.script || props.row.class_name }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="last_date"
|
<${b}-table-column field="last_date"
|
||||||
label="Last Date"
|
label="Last Date"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<span :class="overnightTextClass(props.row)">
|
<span :class="overnightTextClass(props.row)">
|
||||||
{{ props.row.last_date || "never!" }}
|
{{ props.row.last_date || "never!" }}
|
||||||
</span>
|
</span>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column label="Actions"
|
<${b}-table-column label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
|
@ -79,8 +79,13 @@
|
||||||
@click="overnightTaskLaunchInit(props.row)">
|
@click="overnightTaskLaunchInit(props.row)">
|
||||||
Launch
|
Launch
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-modal has-modal-card
|
<${b}-modal has-modal-card
|
||||||
:active.sync="overnightTaskShowLaunchDialog">
|
% if request.use_oruga:
|
||||||
|
v-model:active="overnightTaskShowLaunchDialog"
|
||||||
|
% else:
|
||||||
|
:active.sync="overnightTaskShowLaunchDialog"
|
||||||
|
% endif
|
||||||
|
>
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
|
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
|
@ -127,12 +132,12 @@
|
||||||
</b-button>
|
</b-button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</${b}-modal>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<p class="block">No tasks defined.</p>
|
<p class="block">No tasks defined.</p>
|
||||||
</template>
|
</template>
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
@ -140,35 +145,35 @@
|
||||||
|
|
||||||
<h3 class="block is-size-3">Backfill Tasks</h3>
|
<h3 class="block is-size-3">Backfill Tasks</h3>
|
||||||
|
|
||||||
<b-table :data="backfillTasks" hoverable>
|
<${b}-table :data="backfillTasks" hoverable>
|
||||||
<b-table-column field="description"
|
<${b}-table-column field="description"
|
||||||
label="Description"
|
label="Description"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.description }}
|
{{ props.row.description }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="script"
|
<${b}-table-column field="script"
|
||||||
label="Script"
|
label="Script"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.script }}
|
{{ props.row.script }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="forward"
|
<${b}-table-column field="forward"
|
||||||
label="Orientation"
|
label="Orientation"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.forward ? "Forward" : "Backward" }}
|
{{ props.row.forward ? "Forward" : "Backward" }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="last_date"
|
<${b}-table-column field="last_date"
|
||||||
label="Last Date"
|
label="Last Date"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<span :class="backfillTextClass(props.row)">
|
<span :class="backfillTextClass(props.row)">
|
||||||
{{ props.row.last_date }}
|
{{ props.row.last_date }}
|
||||||
</span>
|
</span>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column field="target_date"
|
<${b}-table-column field="target_date"
|
||||||
label="Target Date"
|
label="Target Date"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.target_date }}
|
{{ props.row.target_date }}
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<b-table-column label="Actions"
|
<${b}-table-column label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
|
@ -176,14 +181,19 @@
|
||||||
@click="backfillTaskLaunch(props.row)">
|
@click="backfillTaskLaunch(props.row)">
|
||||||
Launch
|
Launch
|
||||||
</b-button>
|
</b-button>
|
||||||
</b-table-column>
|
</${b}-table-column>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<p class="block">No tasks defined.</p>
|
<p class="block">No tasks defined.</p>
|
||||||
</template>
|
</template>
|
||||||
</b-table>
|
</${b}-table>
|
||||||
|
|
||||||
<b-modal has-modal-card
|
<${b}-modal has-modal-card
|
||||||
:active.sync="backfillTaskShowLaunchDialog">
|
% if request.use_oruga:
|
||||||
|
v-model:active="backfillTaskShowLaunchDialog"
|
||||||
|
% else:
|
||||||
|
:active.sync="backfillTaskShowLaunchDialog"
|
||||||
|
% endif
|
||||||
|
>
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
|
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
|
@ -238,16 +248,16 @@
|
||||||
</b-button>
|
</b-button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</${b}-modal>
|
||||||
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
% if master.has_perm('restart_scheduler'):
|
% if master.has_perm('restart_scheduler'):
|
||||||
|
|
||||||
|
@ -364,6 +374,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -34,9 +34,9 @@
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
TailboneFormData.formSubmitting = false
|
TailboneFormData.formSubmitting = false
|
||||||
TailboneFormData.submitButtonText = "Yes, please clone away"
|
TailboneFormData.submitButtonText = "Yes, please clone away"
|
||||||
|
@ -48,6 +48,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/form.mako" />
|
<%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()}
|
${parent.body()}
|
||||||
|
|
|
@ -27,26 +27,21 @@
|
||||||
<b-button type="is-primary is-danger"
|
<b-button type="is-primary is-danger"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
:disabled="formSubmitting">
|
:disabled="formSubmitting">
|
||||||
{{ formButtonText }}
|
{{ formSubmitting ? "Working, please wait..." : "${form.button_label_submit}" }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
TailboneFormData.formSubmitting = false
|
${form.vue_component}Data.formSubmitting = false
|
||||||
TailboneFormData.formButtonText = "Yes, please DELETE this data forever!"
|
|
||||||
|
|
||||||
TailboneForm.methods.submitForm = function() {
|
${form.vue_component}.methods.submitForm = function() {
|
||||||
this.formSubmitting = true
|
this.formSubmitting = true
|
||||||
this.formButtonText = "Working, please wait..."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/form.mako" />
|
<%inherit file="/form.mako" />
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
% if master.deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple':
|
<script>
|
||||||
<script type="text/javascript">
|
|
||||||
|
## declare extra data needed by form
|
||||||
|
% if form is not Undefined and getattr(form, 'json_data', None):
|
||||||
|
% for key, value in form.json_data.items():
|
||||||
|
${form.vue_component}Data.${key} = ${json.dumps(value)|n}
|
||||||
|
% endfor
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if master.deletable and instance_deletable and master.has_perm('delete') and getattr(master, 'delete_confirm', 'full') == 'simple':
|
||||||
|
|
||||||
ThisPage.methods.deleteObject = function() {
|
ThisPage.methods.deleteObject = function() {
|
||||||
if (confirm("Are you sure you wish to delete this ${model_title}?")) {
|
if (confirm("Are you sure you wish to delete this ${model_title}?")) {
|
||||||
|
@ -12,9 +20,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
% endif
|
||||||
|
</script>
|
||||||
|
|
||||||
|
% if form is not Undefined and hasattr(form, 'render_included_templates'):
|
||||||
|
${form.render_included_templates()}
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
|
@ -12,187 +12,178 @@
|
||||||
|
|
||||||
<%def name="content_title()"></%def>
|
<%def name="content_title()"></%def>
|
||||||
|
|
||||||
<%def name="context_menu_items()">
|
|
||||||
% if master.results_downloadable_csv and request.has_perm('{}.results_csv'.format(permission_prefix)):
|
|
||||||
<li>${h.link_to("Download results as CSV", url('{}.results_csv'.format(route_prefix)))}</li>
|
|
||||||
% endif
|
|
||||||
% if master.results_downloadable_xlsx and request.has_perm('{}.results_xlsx'.format(permission_prefix)):
|
|
||||||
<li>${h.link_to("Download results as XLSX", url('{}.results_xlsx'.format(route_prefix)))}</li>
|
|
||||||
% endif
|
|
||||||
% if master.has_input_file_templates and master.has_perm('create'):
|
|
||||||
% for template in six.itervalues(input_file_templates):
|
|
||||||
<li>${h.link_to("Download {} Template".format(template['label']), template['effective_url'])}</li>
|
|
||||||
% endfor
|
|
||||||
% endif
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="grid_tools()">
|
<%def name="grid_tools()">
|
||||||
|
|
||||||
## grid totals
|
## grid totals
|
||||||
% if master.supports_grid_totals:
|
% if getattr(master, 'supports_grid_totals', False):
|
||||||
<b-button v-if="gridTotalsDisplay == null"
|
<div style="display: flex; align-items: center;">
|
||||||
:disabled="gridTotalsFetching"
|
<b-button v-if="gridTotalsDisplay == null"
|
||||||
@click="gridTotalsFetch()">
|
:disabled="gridTotalsFetching"
|
||||||
{{ gridTotalsFetching ? "Working, please wait..." : "Show Totals" }}
|
@click="gridTotalsFetch()">
|
||||||
</b-button>
|
{{ gridTotalsFetching ? "Working, please wait..." : "Show Totals" }}
|
||||||
<div v-if="gridTotalsDisplay != null"
|
</b-button>
|
||||||
class="control">
|
<div v-if="gridTotalsDisplay != null"
|
||||||
Totals: {{ gridTotalsDisplay }}
|
class="control">
|
||||||
|
Totals: {{ gridTotalsDisplay }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## download search results
|
## 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'):
|
||||||
<b-button type="is-primary"
|
<div>
|
||||||
icon-pack="fas"
|
<b-button type="is-primary"
|
||||||
icon-left="fas fa-download"
|
icon-pack="fas"
|
||||||
@click="showDownloadResultsDialog = true"
|
icon-left="download"
|
||||||
:disabled="!total">
|
@click="showDownloadResultsDialog = true"
|
||||||
Download Results
|
:disabled="!total">
|
||||||
</b-button>
|
Download Results
|
||||||
|
</b-button>
|
||||||
|
|
||||||
${h.form(url('{}.download_results'.format(route_prefix)), ref='download_results_form')}
|
${h.form(url('{}.download_results'.format(route_prefix)), ref='download_results_form')}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
<input type="hidden" name="fmt" :value="downloadResultsFormat" />
|
<input type="hidden" name="fmt" :value="downloadResultsFormat" />
|
||||||
<input type="hidden" name="fields" :value="downloadResultsFieldsIncluded" />
|
<input type="hidden" name="fields" :value="downloadResultsFieldsIncluded" />
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
|
|
||||||
<b-modal :active.sync="showDownloadResultsDialog">
|
<b-modal :active.sync="showDownloadResultsDialog">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>
|
<p>
|
||||||
There are
|
There are
|
||||||
<span class="is-size-4 has-text-weight-bold">
|
<span class="is-size-4 has-text-weight-bold">
|
||||||
{{ total.toLocaleString('en') }} ${model_title_plural}
|
{{ total.toLocaleString('en') }} ${model_title_plural}
|
||||||
</span>
|
</span>
|
||||||
matching your current filters.
|
matching your current filters.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You may download this set as a single data file if you like.
|
You may download this set as a single data file if you like.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<b-notification type="is-warning" :closable="false"
|
<b-notification type="is-warning" :closable="false"
|
||||||
v-if="downloadResultsFormat == 'xlsx' && total >= 1000">
|
v-if="downloadResultsFormat == 'xlsx' && total >= 1000">
|
||||||
Excel downloads for large data sets can take a long time to
|
Excel downloads for large data sets can take a long time to
|
||||||
generate, and bog down the server in the meantime. You are
|
generate, and bog down the server in the meantime. You are
|
||||||
encouraged to choose CSV for a large data set, even though
|
encouraged to choose CSV for a large data set, even though
|
||||||
the end result (file size) may be larger with CSV.
|
the end result (file size) may be larger with CSV.
|
||||||
</b-notification>
|
</b-notification>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between">
|
<div style="display: flex; justify-content: space-between">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<b-field horizontal label="Format">
|
<b-field label="Format">
|
||||||
<b-select v-model="downloadResultsFormat">
|
<b-select v-model="downloadResultsFormat">
|
||||||
% for key, label in six.iteritems(master.download_results_supported_formats()):
|
% for key, label in master.download_results_supported_formats().items():
|
||||||
<option value="${key}">${label}</option>
|
<option value="${key}">${label}</option>
|
||||||
% endfor
|
% endfor
|
||||||
</b-select>
|
</b-select>
|
||||||
</b-field>
|
</b-field>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<div v-show="downloadResultsFieldsMode != 'choose'"
|
|
||||||
class="has-text-right">
|
|
||||||
<p v-if="downloadResultsFieldsMode == 'default'">
|
|
||||||
Will use DEFAULT fields.
|
|
||||||
</p>
|
|
||||||
<p v-if="downloadResultsFieldsMode == 'all'">
|
|
||||||
Will use ALL fields.
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="buttons is-right">
|
<div>
|
||||||
<b-button type="is-primary"
|
|
||||||
v-show="downloadResultsFieldsMode != 'default'"
|
|
||||||
@click="downloadResultsUseDefaultFields()">
|
|
||||||
Use Default Fields
|
|
||||||
</b-button>
|
|
||||||
<b-button type="is-primary"
|
|
||||||
v-show="downloadResultsFieldsMode != 'all'"
|
|
||||||
@click="downloadResultsUseAllFields()">
|
|
||||||
Use All Fields
|
|
||||||
</b-button>
|
|
||||||
<b-button type="is-primary"
|
|
||||||
v-show="downloadResultsFieldsMode != 'choose'"
|
|
||||||
@click="downloadResultsFieldsMode = 'choose'">
|
|
||||||
Choose Fields
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="downloadResultsFieldsMode == 'choose'">
|
<div v-show="downloadResultsFieldsMode != 'choose'"
|
||||||
<div style="display: flex;">
|
class="has-text-right">
|
||||||
<div>
|
<p v-if="downloadResultsFieldsMode == 'default'">
|
||||||
<b-field label="Excluded Fields">
|
Will use DEFAULT fields.
|
||||||
<b-select multiple native-size="8"
|
</p>
|
||||||
expanded
|
<p v-if="downloadResultsFieldsMode == 'all'">
|
||||||
ref="downloadResultsExcludedFields">
|
Will use ALL fields.
|
||||||
<option v-for="field in downloadResultsFieldsAvailable"
|
</p>
|
||||||
v-if="!downloadResultsFieldsIncluded.includes(field)"
|
<br />
|
||||||
:key="field"
|
</div>
|
||||||
:value="field">
|
|
||||||
{{ field }}
|
<div class="buttons is-right">
|
||||||
</option>
|
<b-button type="is-primary"
|
||||||
</b-select>
|
v-show="downloadResultsFieldsMode != 'default'"
|
||||||
</b-field>
|
@click="downloadResultsUseDefaultFields()">
|
||||||
</div>
|
Use Default Fields
|
||||||
<div>
|
</b-button>
|
||||||
<br /><br />
|
<b-button type="is-primary"
|
||||||
<b-button style="margin: 0.5rem;"
|
v-show="downloadResultsFieldsMode != 'all'"
|
||||||
@click="downloadResultsExcludeFields()">
|
@click="downloadResultsUseAllFields()">
|
||||||
<
|
Use All Fields
|
||||||
</b-button>
|
</b-button>
|
||||||
<br />
|
<b-button type="is-primary"
|
||||||
<b-button style="margin: 0.5rem;"
|
v-show="downloadResultsFieldsMode != 'choose'"
|
||||||
@click="downloadResultsIncludeFields()">
|
@click="downloadResultsFieldsMode = 'choose'">
|
||||||
>
|
Choose Fields
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<b-field label="Included Fields">
|
<div v-show="downloadResultsFieldsMode == 'choose'">
|
||||||
<b-select multiple native-size="8"
|
<div style="display: flex;">
|
||||||
expanded
|
<div>
|
||||||
ref="downloadResultsIncludedFields">
|
<b-field label="Excluded Fields">
|
||||||
<option v-for="field in downloadResultsFieldsIncluded"
|
<b-select multiple native-size="8"
|
||||||
:key="field"
|
expanded
|
||||||
:value="field">
|
v-model="downloadResultsExcludedFieldsSelected"
|
||||||
{{ field }}
|
ref="downloadResultsExcludedFields">
|
||||||
</option>
|
<option v-for="field in downloadResultsFieldsExcluded"
|
||||||
</b-select>
|
:key="field"
|
||||||
</b-field>
|
:value="field">
|
||||||
|
{{ field }}
|
||||||
|
</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<br /><br />
|
||||||
|
<b-button style="margin: 0.5rem;"
|
||||||
|
@click="downloadResultsExcludeFields()">
|
||||||
|
<
|
||||||
|
</b-button>
|
||||||
|
<br />
|
||||||
|
<b-button style="margin: 0.5rem;"
|
||||||
|
@click="downloadResultsIncludeFields()">
|
||||||
|
>
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b-field label="Included Fields">
|
||||||
|
<b-select multiple native-size="8"
|
||||||
|
expanded
|
||||||
|
v-model="downloadResultsIncludedFieldsSelected"
|
||||||
|
ref="downloadResultsIncludedFields">
|
||||||
|
<option v-for="field in downloadResultsFieldsIncluded"
|
||||||
|
:key="field"
|
||||||
|
:value="field">
|
||||||
|
{{ field }}
|
||||||
|
</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> <!-- card-content -->
|
||||||
</div> <!-- card-content -->
|
|
||||||
|
|
||||||
<footer class="modal-card-foot">
|
<footer class="modal-card-foot">
|
||||||
<b-button @click="showDownloadResultsDialog = false">
|
<b-button @click="showDownloadResultsDialog = false">
|
||||||
Cancel
|
Cancel
|
||||||
</b-button>
|
</b-button>
|
||||||
<once-button type="is-primary"
|
<once-button type="is-primary"
|
||||||
@click="downloadResultsSubmit()"
|
@click="downloadResultsSubmit()"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="fas fa-download"
|
icon-left="download"
|
||||||
:disabled="!downloadResultsFieldsIncluded.length"
|
:disabled="!downloadResultsFieldsIncluded.length"
|
||||||
text="Download Results">
|
text="Download Results">
|
||||||
</once-button>
|
</once-button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## download rows for search results
|
## 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"
|
<b-button type="is-primary"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="fas fa-download"
|
icon-left="download"
|
||||||
@click="downloadResultsRows()"
|
@click="downloadResultsRows()"
|
||||||
:disabled="downloadResultsRowsButtonDisabled">
|
:disabled="downloadResultsRowsButtonDisabled">
|
||||||
{{ downloadResultsRowsButtonText }}
|
{{ downloadResultsRowsButtonText }}
|
||||||
|
@ -203,7 +194,7 @@
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## merge 2 objects
|
## 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.form(url('{}.merge'.format(route_prefix)), class_='control', **{'@submit': 'submitMergeForm'})}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
|
@ -221,7 +212,7 @@
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## enable / disable selected objects
|
## 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.form(url('{}.enable_set'.format(route_prefix)), class_='control', ref='enable_selected_form')}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
|
@ -243,7 +234,7 @@
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## delete selected objects
|
## 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.form(url('{}.delete_set'.format(route_prefix)), ref='delete_selected_form', class_='control')}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
${h.hidden('uuids', v_model='selected_uuids')}
|
${h.hidden('uuids', v_model='selected_uuids')}
|
||||||
|
@ -258,7 +249,7 @@
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## delete search results
|
## 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.form(url('{}.bulk_delete'.format(route_prefix)), ref='delete_results_form', class_='control')}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
<b-button type="is-danger"
|
<b-button type="is-danger"
|
||||||
|
@ -274,6 +265,11 @@
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
## DEPRECATED; remains for back-compat
|
||||||
|
<%def name="render_this_page()">
|
||||||
|
${self.page_content()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="page_content()">
|
<%def name="page_content()">
|
||||||
|
|
||||||
% if download_results_path:
|
% if download_results_path:
|
||||||
|
@ -292,7 +288,7 @@
|
||||||
|
|
||||||
${self.render_grid_component()}
|
${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.form('#', ref='deleteObjectForm')}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
|
@ -300,45 +296,34 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_grid_component()">
|
<%def name="render_grid_component()">
|
||||||
<${grid.component} ref="grid" :csrftoken="csrftoken"
|
${grid.render_vue_tag()}
|
||||||
% if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple':
|
|
||||||
@deleteActionClicked="deleteObject"
|
|
||||||
% endif
|
|
||||||
>
|
|
||||||
</${grid.component}>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="make_this_page_component()">
|
##############################
|
||||||
${parent.make_this_page_component()}
|
## vue components
|
||||||
|
##############################
|
||||||
|
|
||||||
|
<%def name="render_vue_templates()">
|
||||||
|
${parent.render_vue_templates()}
|
||||||
|
|
||||||
|
## DEPRECATED; called for back-compat
|
||||||
|
${self.make_grid_component()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
## 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_vue_vars()">
|
||||||
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
${grid.component_studly}.data = function() { return ${grid.component_studly}Data }
|
% if getattr(master, 'supports_grid_totals', False):
|
||||||
|
${grid.vue_component}Data.gridTotalsDisplay = null
|
||||||
|
${grid.vue_component}Data.gridTotalsFetching = false
|
||||||
|
|
||||||
Vue.component('${grid.component}', ${grid.component_studly})
|
${grid.vue_component}.methods.gridTotalsFetch = function() {
|
||||||
|
|
||||||
</script>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="render_this_page()">
|
|
||||||
${self.page_content()}
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="render_this_page_template()">
|
|
||||||
${parent.render_this_page_template()}
|
|
||||||
|
|
||||||
## 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="modify_this_page_vars()">
|
|
||||||
${parent.modify_this_page_vars()}
|
|
||||||
<script type="text/javascript">
|
|
||||||
|
|
||||||
% if master.supports_grid_totals:
|
|
||||||
${grid.component_studly}Data.gridTotalsDisplay = null
|
|
||||||
${grid.component_studly}Data.gridTotalsFetching = false
|
|
||||||
|
|
||||||
${grid.component_studly}.methods.gridTotalsFetch = function() {
|
|
||||||
this.gridTotalsFetching = true
|
this.gridTotalsFetching = true
|
||||||
|
|
||||||
let url = '${url(f'{route_prefix}.fetch_grid_totals')}'
|
let url = '${url(f'{route_prefix}.fetch_grid_totals')}'
|
||||||
|
@ -350,7 +335,7 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}.methods.appliedFiltersHook = function() {
|
${grid.vue_component}.methods.appliedFiltersHook = function() {
|
||||||
this.gridTotalsDisplay = null
|
this.gridTotalsDisplay = null
|
||||||
this.gridTotalsFetching = false
|
this.gridTotalsFetching = false
|
||||||
}
|
}
|
||||||
|
@ -394,7 +379,7 @@
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## delete single object
|
## 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) {
|
ThisPage.methods.deleteObject = function(url) {
|
||||||
if (confirm("Are you sure you wish to delete this ${model_title}?")) {
|
if (confirm("Are you sure you wish to delete this ${model_title}?")) {
|
||||||
let form = this.$refs.deleteObjectForm
|
let form = this.$refs.deleteObjectForm
|
||||||
|
@ -405,16 +390,19 @@
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## download results
|
## 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.vue_component}Data.downloadResultsFormat = '${master.download_results_default_format()}'
|
||||||
${grid.component_studly}Data.showDownloadResultsDialog = false
|
${grid.vue_component}Data.showDownloadResultsDialog = false
|
||||||
${grid.component_studly}Data.downloadResultsFieldsMode = 'default'
|
${grid.vue_component}Data.downloadResultsFieldsMode = 'default'
|
||||||
${grid.component_studly}Data.downloadResultsFieldsAvailable = ${json.dumps(download_results_fields_available)|n}
|
${grid.vue_component}Data.downloadResultsFieldsAvailable = ${json.dumps(download_results_fields_available)|n}
|
||||||
${grid.component_studly}Data.downloadResultsFieldsDefault = ${json.dumps(download_results_fields_default)|n}
|
${grid.vue_component}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.downloadResultsFieldsIncluded = ${json.dumps(download_results_fields_default)|n}
|
||||||
|
|
||||||
${grid.component_studly}.computed.downloadResultsFieldsExcluded = function() {
|
${grid.vue_component}Data.downloadResultsExcludedFieldsSelected = []
|
||||||
|
${grid.vue_component}Data.downloadResultsIncludedFieldsSelected = []
|
||||||
|
|
||||||
|
${grid.vue_component}.computed.downloadResultsFieldsExcluded = function() {
|
||||||
let excluded = []
|
let excluded = []
|
||||||
this.downloadResultsFieldsAvailable.forEach(field => {
|
this.downloadResultsFieldsAvailable.forEach(field => {
|
||||||
if (!this.downloadResultsFieldsIncluded.includes(field)) {
|
if (!this.downloadResultsFieldsIncluded.includes(field)) {
|
||||||
|
@ -424,70 +412,73 @@
|
||||||
return excluded
|
return excluded
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}.methods.downloadResultsExcludeFields = function() {
|
${grid.vue_component}.methods.downloadResultsExcludeFields = function() {
|
||||||
let selected = this.$refs.downloadResultsIncludedFields.selected
|
const selected = Array.from(this.downloadResultsIncludedFieldsSelected)
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selected = Array.from(selected)
|
|
||||||
selected.forEach(field => {
|
|
||||||
|
|
||||||
// de-select the entry within "included" field input
|
selected.forEach(field => {
|
||||||
let index = this.$refs.downloadResultsIncludedFields.selected.indexOf(field)
|
let index
|
||||||
if (index > -1) {
|
|
||||||
this.$refs.downloadResultsIncludedFields.selected.splice(index, 1)
|
// remove field from selected
|
||||||
|
index = this.downloadResultsIncludedFieldsSelected.indexOf(field)
|
||||||
|
if (index >= 0) {
|
||||||
|
this.downloadResultsIncludedFieldsSelected.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove field from official "included" list
|
// remove field from included
|
||||||
|
// nb. excluded list will reflect this change too
|
||||||
index = this.downloadResultsFieldsIncluded.indexOf(field)
|
index = this.downloadResultsFieldsIncluded.indexOf(field)
|
||||||
if (index > -1) {
|
if (index >= 0) {
|
||||||
this.downloadResultsFieldsIncluded.splice(index, 1)
|
this.downloadResultsFieldsIncluded.splice(index, 1)
|
||||||
}
|
}
|
||||||
}, this)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}.methods.downloadResultsIncludeFields = function() {
|
${grid.vue_component}.methods.downloadResultsIncludeFields = function() {
|
||||||
let selected = this.$refs.downloadResultsExcludedFields.selected
|
const selected = Array.from(this.downloadResultsExcludedFieldsSelected)
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selected = Array.from(selected)
|
|
||||||
selected.forEach(field => {
|
|
||||||
|
|
||||||
// de-select the entry within "excluded" field input
|
selected.forEach(field => {
|
||||||
let index = this.$refs.downloadResultsExcludedFields.selected.indexOf(field)
|
let index
|
||||||
if (index > -1) {
|
|
||||||
this.$refs.downloadResultsExcludedFields.selected.splice(index, 1)
|
// remove field from selected
|
||||||
|
index = this.downloadResultsExcludedFieldsSelected.indexOf(field)
|
||||||
|
if (index >= 0) {
|
||||||
|
this.downloadResultsExcludedFieldsSelected.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// add field to official "included" list
|
// add field to included
|
||||||
|
// nb. excluded list will reflect this change too
|
||||||
this.downloadResultsFieldsIncluded.push(field)
|
this.downloadResultsFieldsIncluded.push(field)
|
||||||
|
})
|
||||||
}, this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}.methods.downloadResultsUseDefaultFields = function() {
|
${grid.vue_component}.methods.downloadResultsUseDefaultFields = function() {
|
||||||
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsDefault)
|
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsDefault)
|
||||||
this.downloadResultsFieldsMode = 'default'
|
this.downloadResultsFieldsMode = 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}.methods.downloadResultsUseAllFields = function() {
|
${grid.vue_component}.methods.downloadResultsUseAllFields = function() {
|
||||||
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsAvailable)
|
this.downloadResultsFieldsIncluded = Array.from(this.downloadResultsFieldsAvailable)
|
||||||
this.downloadResultsFieldsMode = 'all'
|
this.downloadResultsFieldsMode = 'all'
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}.methods.downloadResultsSubmit = function() {
|
${grid.vue_component}.methods.downloadResultsSubmit = function() {
|
||||||
this.$refs.download_results_form.submit()
|
this.$refs.download_results_form.submit()
|
||||||
}
|
}
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## download rows for results
|
## 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.vue_component}Data.downloadResultsRowsButtonDisabled = false
|
||||||
${grid.component_studly}Data.downloadResultsRowsButtonText = "Download Rows for Results"
|
${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 "
|
if (confirm("This will generate an Excel file which contains "
|
||||||
+ "not the results themselves, but the *rows* for "
|
+ "not the results themselves, but the *rows* for "
|
||||||
+ "each.\n\nAre you sure you want this?")) {
|
+ "each.\n\nAre you sure you want this?")) {
|
||||||
|
@ -499,12 +490,12 @@
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## enable / disable selected objects
|
## 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.vue_component}Data.enableSelectedSubmitting = false
|
||||||
${grid.component_studly}Data.enableSelectedText = "Enable Selected"
|
${grid.vue_component}Data.enableSelectedText = "Enable Selected"
|
||||||
|
|
||||||
${grid.component_studly}.computed.enableSelectedDisabled = function() {
|
${grid.vue_component}.computed.enableSelectedDisabled = function() {
|
||||||
if (this.enableSelectedSubmitting) {
|
if (this.enableSelectedSubmitting) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -514,7 +505,7 @@
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}.methods.enableSelectedSubmit = function() {
|
${grid.vue_component}.methods.enableSelectedSubmit = function() {
|
||||||
let uuids = this.checkedRowUUIDs()
|
let uuids = this.checkedRowUUIDs()
|
||||||
if (!uuids.length) {
|
if (!uuids.length) {
|
||||||
alert("You must first select one or more objects to disable.")
|
alert("You must first select one or more objects to disable.")
|
||||||
|
@ -529,10 +520,10 @@
|
||||||
this.$refs.enable_selected_form.submit()
|
this.$refs.enable_selected_form.submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}Data.disableSelectedSubmitting = false
|
${grid.vue_component}Data.disableSelectedSubmitting = false
|
||||||
${grid.component_studly}Data.disableSelectedText = "Disable Selected"
|
${grid.vue_component}Data.disableSelectedText = "Disable Selected"
|
||||||
|
|
||||||
${grid.component_studly}.computed.disableSelectedDisabled = function() {
|
${grid.vue_component}.computed.disableSelectedDisabled = function() {
|
||||||
if (this.disableSelectedSubmitting) {
|
if (this.disableSelectedSubmitting) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -542,7 +533,7 @@
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}.methods.disableSelectedSubmit = function() {
|
${grid.vue_component}.methods.disableSelectedSubmit = function() {
|
||||||
let uuids = this.checkedRowUUIDs()
|
let uuids = this.checkedRowUUIDs()
|
||||||
if (!uuids.length) {
|
if (!uuids.length) {
|
||||||
alert("You must first select one or more objects to disable.")
|
alert("You must first select one or more objects to disable.")
|
||||||
|
@ -560,12 +551,12 @@
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## delete selected objects
|
## 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.vue_component}Data.deleteSelectedSubmitting = false
|
||||||
${grid.component_studly}Data.deleteSelectedText = "Delete Selected"
|
${grid.vue_component}Data.deleteSelectedText = "Delete Selected"
|
||||||
|
|
||||||
${grid.component_studly}.computed.deleteSelectedDisabled = function() {
|
${grid.vue_component}.computed.deleteSelectedDisabled = function() {
|
||||||
if (this.deleteSelectedSubmitting) {
|
if (this.deleteSelectedSubmitting) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -575,7 +566,7 @@
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}.methods.deleteSelectedSubmit = function() {
|
${grid.vue_component}.methods.deleteSelectedSubmit = function() {
|
||||||
let uuids = this.checkedRowUUIDs()
|
let uuids = this.checkedRowUUIDs()
|
||||||
if (!uuids.length) {
|
if (!uuids.length) {
|
||||||
alert("You must first select one or more objects to disable.")
|
alert("You must first select one or more objects to disable.")
|
||||||
|
@ -591,12 +582,12 @@
|
||||||
}
|
}
|
||||||
% endif
|
% 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.vue_component}Data.deleteResultsSubmitting = false
|
||||||
${grid.component_studly}Data.deleteResultsText = "Delete Results"
|
${grid.vue_component}Data.deleteResultsText = "Delete Results"
|
||||||
|
|
||||||
${grid.component_studly}.computed.deleteResultsDisabled = function() {
|
${grid.vue_component}.computed.deleteResultsDisabled = function() {
|
||||||
if (this.deleteResultsSubmitting) {
|
if (this.deleteResultsSubmitting) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -606,7 +597,7 @@
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
${grid.component_studly}.methods.deleteResultsSubmit = function() {
|
${grid.vue_component}.methods.deleteResultsSubmit = function() {
|
||||||
// TODO: show "plural model title" here?
|
// TODO: show "plural model title" here?
|
||||||
if (!confirm("You are about to delete " + this.total.toLocaleString('en') + " objects.\n\nAre you sure?")) {
|
if (!confirm("You are about to delete " + this.total.toLocaleString('en') + " objects.\n\nAre you sure?")) {
|
||||||
return
|
return
|
||||||
|
@ -619,12 +610,12 @@
|
||||||
|
|
||||||
% endif
|
% 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.vue_component}Data.mergeFormButtonText = "Merge 2 ${model_title_plural}"
|
||||||
${grid.component_studly}Data.mergeFormSubmitting = false
|
${grid.vue_component}Data.mergeFormSubmitting = false
|
||||||
|
|
||||||
${grid.component_studly}.methods.submitMergeForm = function() {
|
${grid.vue_component}.methods.submitMergeForm = function() {
|
||||||
this.mergeFormSubmitting = true
|
this.mergeFormSubmitting = true
|
||||||
this.mergeFormButtonText = "Working, please wait..."
|
this.mergeFormButtonText = "Working, please wait..."
|
||||||
}
|
}
|
||||||
|
@ -632,5 +623,10 @@
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_vue_components()">
|
||||||
${parent.body()}
|
${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>
|
||||||
|
|
|
@ -109,8 +109,8 @@
|
||||||
<merge-buttons></merge-buttons>
|
<merge-buttons></merge-buttons>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_this_page_template()">
|
<%def name="render_vue_templates()">
|
||||||
${parent.render_this_page_template()}
|
${parent.render_vue_templates()}
|
||||||
|
|
||||||
<script type="text/x-template" id="merge-buttons-template">
|
<script type="text/x-template" id="merge-buttons-template">
|
||||||
<div class="level" style="margin-top: 2em;">
|
<div class="level" style="margin-top: 2em;">
|
||||||
|
@ -147,11 +147,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
<script>
|
||||||
|
|
||||||
<%def name="make_this_page_component()">
|
|
||||||
${parent.make_this_page_component()}
|
|
||||||
<script type="text/javascript">
|
|
||||||
|
|
||||||
const MergeButtons = {
|
const MergeButtons = {
|
||||||
template: '#merge-buttons-template',
|
template: '#merge-buttons-template',
|
||||||
|
@ -175,10 +171,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.component('merge-buttons', MergeButtons)
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_vue_components()">
|
||||||
${parent.body()}
|
${parent.make_vue_components()}
|
||||||
|
<script>
|
||||||
|
Vue.component('merge-buttons', MergeButtons)
|
||||||
|
<% request.register_component('merge-buttons', 'MergeButtons') %>
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
|
@ -16,27 +16,16 @@
|
||||||
${self.page_content()}
|
${self.page_content()}
|
||||||
</%def>
|
</%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()">
|
<%def name="page_content()">
|
||||||
<tailbone-grid :csrftoken="csrftoken">
|
${grid.render_vue_tag(**{':csrftoken': 'csrftoken'})}
|
||||||
</tailbone-grid>
|
|
||||||
</%def>
|
</%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>
|
||||||
|
|
|
@ -8,12 +8,15 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_instance_header_title_extras()">
|
<%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=""Touch" this record to trigger sync"
|
<b-button title=""Touch" this record to trigger sync"
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="hand-pointer"
|
|
||||||
@click="touchRecord()"
|
@click="touchRecord()"
|
||||||
:disabled="touchSubmitting">
|
:disabled="touchSubmitting">
|
||||||
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="hand-pointer" />
|
||||||
|
% else:
|
||||||
|
<span><i class="fa fa-hand-pointer"></i></span>
|
||||||
|
% endif
|
||||||
</b-button>
|
</b-button>
|
||||||
% endif
|
% endif
|
||||||
% if expose_versions:
|
% if expose_versions:
|
||||||
|
@ -34,7 +37,7 @@
|
||||||
% if xref_buttons or xref_links:
|
% if xref_buttons or xref_links:
|
||||||
<nav class="panel">
|
<nav class="panel">
|
||||||
<p class="panel-heading">Cross-Reference</p>
|
<p class="panel-heading">Cross-Reference</p>
|
||||||
<div class="panel-block buttons">
|
<div class="panel-block">
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||||
% for button in xref_buttons:
|
% for button in xref_buttons:
|
||||||
${button}
|
${button}
|
||||||
|
@ -48,12 +51,6 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="context_menu_items()">
|
|
||||||
## TODO: either make this configurable, or just lose it.
|
|
||||||
## nobody seems to ever find it useful in practice.
|
|
||||||
## <li>${h.link_to("Permalink for this {}".format(model_title), action_url('view', instance))}</li>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="render_row_grid_tools()">
|
<%def name="render_row_grid_tools()">
|
||||||
${rows_grid_tools}
|
${rows_grid_tools}
|
||||||
% if master.rows_downloadable_xlsx and master.has_perm('row_results_xlsx'):
|
% if master.rows_downloadable_xlsx and master.has_perm('row_results_xlsx'):
|
||||||
|
@ -96,7 +93,7 @@
|
||||||
${parent.render_this_page()}
|
${parent.render_this_page()}
|
||||||
|
|
||||||
## render row grid
|
## render row grid
|
||||||
% if master.has_rows:
|
% if getattr(master, 'has_rows', False):
|
||||||
<br />
|
<br />
|
||||||
% if rows_title:
|
% if rows_title:
|
||||||
<h4 class="block is-size-4">${rows_title}</h4>
|
<h4 class="block is-size-4">${rows_title}</h4>
|
||||||
|
@ -113,17 +110,25 @@
|
||||||
<p class="block">
|
<p class="block">
|
||||||
<a href="${master.get_action_url('versions', instance)}"
|
<a href="${master.get_action_url('versions', instance)}"
|
||||||
target="_blank">
|
target="_blank">
|
||||||
<i class="fas fa-external-link-alt"></i>
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="external-link-alt" />
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-external-link-alt"></i>
|
||||||
|
% endif
|
||||||
View as separate page
|
View as separate page
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<versions-grid ref="versionsGrid"
|
${versions_grid.render_vue_tag(ref='versionsGrid', **{'@view-revision': 'viewRevision'})}
|
||||||
@view-revision="viewRevision">
|
|
||||||
</versions-grid>
|
|
||||||
|
|
||||||
<b-modal :active.sync="viewVersionShowDialog" :width="1200">
|
<${b}-modal :width="1200"
|
||||||
|
% if request.use_oruga:
|
||||||
|
v-model:active="viewVersionShowDialog"
|
||||||
|
% else:
|
||||||
|
:active.sync="viewVersionShowDialog"
|
||||||
|
% endif
|
||||||
|
>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
|
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
|
||||||
|
@ -170,7 +175,11 @@
|
||||||
<div>
|
<div>
|
||||||
<a :href="viewVersionData.url"
|
<a :href="viewVersionData.url"
|
||||||
target="_blank">
|
target="_blank">
|
||||||
<i class="fas fa-external-link-alt"></i>
|
% if request.use_oruga:
|
||||||
|
<o-icon icon="external-link-alt" />
|
||||||
|
% else:
|
||||||
|
<i class="fas fa-external-link-alt"></i>
|
||||||
|
% endif
|
||||||
View as separate page
|
View as separate page
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -187,6 +196,7 @@
|
||||||
|
|
||||||
<p class="block has-text-weight-bold">
|
<p class="block has-text-weight-bold">
|
||||||
{{ version.model_title }}
|
{{ version.model_title }}
|
||||||
|
({{ version.operation }})
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table class="diff monospace is-size-7"
|
<table class="diff monospace is-size-7"
|
||||||
|
@ -213,34 +223,50 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<b-loading :active.sync="viewVersionLoading" :is-full-page="false"></b-loading>
|
% if request.use_oruga:
|
||||||
|
<o-loading v-model:active="viewVersionLoading" :is-full-page="false" />
|
||||||
|
% else:
|
||||||
|
<b-loading :active.sync="viewVersionLoading" :is-full-page="false"></b-loading>
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</${b}-modal>
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_row_grid_component()">
|
<%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>
|
||||||
|
|
||||||
<%def name="render_this_page_template()">
|
<%def name="render_vue_templates()">
|
||||||
% if master.has_rows:
|
${parent.render_vue_templates()}
|
||||||
## TODO: stop using |n filter
|
% if getattr(master, 'has_rows', False):
|
||||||
${rows_grid.render_complete(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))|n}
|
${rows_grid.render_vue_template(allow_save_defaults=False, tools=capture(self.render_row_grid_tools))}
|
||||||
% endif
|
% endif
|
||||||
${parent.render_this_page_template()}
|
|
||||||
% if expose_versions:
|
% if expose_versions:
|
||||||
${versions_grid.render_complete()|n}
|
${versions_grid.render_vue_template()}
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
% if expose_versions:
|
<script>
|
||||||
<script type="text/javascript">
|
|
||||||
|
|
||||||
|
% if getattr(master, 'touchable', False) and master.has_perm('touch'):
|
||||||
|
|
||||||
|
WholePageData.touchSubmitting = false
|
||||||
|
|
||||||
|
WholePage.methods.touchRecord = function() {
|
||||||
|
this.touchSubmitting = true
|
||||||
|
location.href = '${master.get_action_url('touch', instance)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if expose_versions:
|
||||||
|
|
||||||
|
WholePageData.viewingHistory = false
|
||||||
ThisPage.props.viewingHistory = Boolean
|
ThisPage.props.viewingHistory = Boolean
|
||||||
|
|
||||||
ThisPageData.gettingRevisions = false
|
ThisPageData.gettingRevisions = false
|
||||||
|
@ -295,48 +321,16 @@
|
||||||
this.viewVersionShowAllFields = !this.viewVersionShowAllFields
|
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
|
% endif
|
||||||
</%def>
|
</%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()}
|
|
||||||
|
|
|
@ -19,48 +19,39 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="page_content()">
|
<%def name="page_content()">
|
||||||
## TODO: this was basically copied from Revel diff template..need to abstract
|
|
||||||
|
|
||||||
<div class="form-wrapper">
|
<div class="form-wrapper" style="margin: 1rem; 0;">
|
||||||
|
<div class="form">
|
||||||
|
|
||||||
<div class="form">
|
<b-field label="Changed" horizontal>
|
||||||
|
<span>${h.pretty_datetime(request.rattail_config, changed)}</span>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Changed by" horizontal>
|
||||||
|
<span>${transaction.user or ''}</span>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="IP Address" horizontal>
|
||||||
|
<span>${transaction.remote_addr}</span>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Comment" horizontal>
|
||||||
|
<span>${transaction.meta.get('comment') or ''}</span>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="TXN ID" horizontal>
|
||||||
|
<span>${transaction.id}</span>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
<div class="field-wrapper">
|
|
||||||
<label>Changed</label>
|
|
||||||
<div class="field">${h.pretty_datetime(request.rattail_config, changed)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-wrapper">
|
|
||||||
<label>Changed by</label>
|
|
||||||
<div class="field">${transaction.user or ''}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-wrapper">
|
|
||||||
<label>IP Address</label>
|
|
||||||
<div class="field">${transaction.remote_addr}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-wrapper">
|
|
||||||
<label>Comment</label>
|
|
||||||
<div class="field">${transaction.meta.get('comment') or ''}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-wrapper">
|
|
||||||
<label>TXN ID</label>
|
|
||||||
<div class="field">${transaction.id}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div><!-- form-wrapper -->
|
<div class="versions-wrapper">
|
||||||
|
% for diff in version_diffs:
|
||||||
<div class="versions-wrapper">
|
<h4 class="is-size-4 block">${diff.title}</h4>
|
||||||
% for diff in version_diffs:
|
${diff.render_html()}
|
||||||
<h4 class="is-size-4 block">${diff.title}</h4>
|
% endfor
|
||||||
${diff.render_html()}
|
</div>
|
||||||
% endfor
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -52,9 +52,9 @@
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_this_page_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_this_page_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
|
|
||||||
ThisPage.methods.getLabelForKey = function(key) {
|
ThisPage.methods.getLabelForKey = function(key) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
|
@ -75,6 +75,3 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
${parent.body()}
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue