Compare commits
79 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 |
61 changed files with 2119 additions and 1677 deletions
164
CHANGELOG.md
164
CHANGELOG.md
|
@ -5,6 +5,170 @@ 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/)
|
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).
|
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)
|
## v0.20.0 (2024-08-20)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
@ -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/
|
|
|
@ -27,10 +27,10 @@ templates_path = ['_templates']
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
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://rattailproject.org/docs/wuttaweb/', None),
|
'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None),
|
||||||
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
|
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# allow todo entries to show up
|
# allow todo entries to show up
|
||||||
|
|
|
@ -6,9 +6,9 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "Tailbone"
|
name = "Tailbone"
|
||||||
version = "0.20.0"
|
version = "0.22.7"
|
||||||
description = "Backoffice Web Application for Rattail"
|
description = "Backoffice Web Application for Rattail"
|
||||||
readme = "README.rst"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||||
license = {text = "GNU GPL v3+"}
|
license = {text = "GNU GPL v3+"}
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
@ -53,13 +53,13 @@ dependencies = [
|
||||||
"pyramid_mako",
|
"pyramid_mako",
|
||||||
"pyramid_retry",
|
"pyramid_retry",
|
||||||
"pyramid_tm",
|
"pyramid_tm",
|
||||||
"rattail[db,bouncer]>=0.18.1",
|
"rattail[db,bouncer]>=0.20.1",
|
||||||
"sa-filters",
|
"sa-filters",
|
||||||
"simplejson",
|
"simplejson",
|
||||||
"transaction",
|
"transaction",
|
||||||
"waitress",
|
"waitress",
|
||||||
"WebHelpers2",
|
"WebHelpers2",
|
||||||
"WuttaWeb>=0.11.0",
|
"WuttaWeb>=0.21.0",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -84,9 +84,9 @@ tailbone = "tailbone.config:ConfigExtension"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://rattailproject.org"
|
Homepage = "https://rattailproject.org"
|
||||||
Repository = "https://kallithea.rattailproject.org/rattail-project/tailbone"
|
Repository = "https://forgejo.wuttaproject.org/rattail/tailbone"
|
||||||
Issues = "https://redmine.rattailproject.org/projects/tailbone/issues"
|
Issues = "https://forgejo.wuttaproject.org/rattail/tailbone/issues"
|
||||||
Changelog = "https://kallithea.rattailproject.org/rattail-project/tailbone/files/master/CHANGELOG.md"
|
Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone/src/branch/master/CHANGELOG.md"
|
||||||
|
|
||||||
|
|
||||||
[tool.commitizen]
|
[tool.commitizen]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -26,7 +26,6 @@ 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
|
||||||
|
@ -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
|
||||||
|
|
|
@ -62,6 +62,17 @@ def make_rattail_config(settings):
|
||||||
# nb. this is for compaibility with wuttaweb
|
# nb. this is for compaibility with wuttaweb
|
||||||
settings['wutta_config'] = rattail_config
|
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, 'appdb_engine'):
|
if hasattr(rattail_config, 'appdb_engine'):
|
||||||
tailbone.db.Session.configure(bind=rattail_config.appdb_engine)
|
tailbone.db.Session.configure(bind=rattail_config.appdb_engine)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -401,6 +401,8 @@ class Form(object):
|
||||||
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)
|
||||||
|
|
||||||
|
@ -1037,9 +1039,9 @@ class Form(object):
|
||||||
|
|
||||||
def render_vue_tag(self, **kwargs):
|
def render_vue_tag(self, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
return self.render_vuejs_component()
|
return self.render_vuejs_component(**kwargs)
|
||||||
|
|
||||||
def render_vuejs_component(self):
|
def render_vuejs_component(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Render the Vue.js component HTML for the form.
|
Render the Vue.js component HTML for the form.
|
||||||
|
|
||||||
|
@ -1050,10 +1052,11 @@ 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.vue_tagname, **kwargs)
|
return HTML.tag(self.vue_tagname, **kw)
|
||||||
|
|
||||||
def set_json_data(self, key, value):
|
def set_json_data(self, key, value):
|
||||||
"""
|
"""
|
||||||
|
@ -1380,7 +1383,11 @@ class Form(object):
|
||||||
return getattr(record, field_name)
|
return getattr(record, field_name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
return record[field_name]
|
|
||||||
|
try:
|
||||||
|
return record[field_name]
|
||||||
|
except TypeError:
|
||||||
|
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:
|
||||||
|
|
|
@ -24,9 +24,10 @@
|
||||||
Core Grid Classes
|
Core Grid Classes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
import inspect
|
||||||
import warnings
|
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
@ -196,11 +197,7 @@ class Grid(WuttaGrid):
|
||||||
raw_renderers={},
|
raw_renderers={},
|
||||||
extra_row_class=None,
|
extra_row_class=None,
|
||||||
url='#',
|
url='#',
|
||||||
joiners={},
|
|
||||||
filterable=False,
|
|
||||||
filters={},
|
|
||||||
use_byte_string_filters=False,
|
use_byte_string_filters=False,
|
||||||
searchable={},
|
|
||||||
checkboxes=False,
|
checkboxes=False,
|
||||||
checked=None,
|
checked=None,
|
||||||
check_handler=None,
|
check_handler=None,
|
||||||
|
@ -238,7 +235,7 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
if 'pageable' in kwargs:
|
if 'pageable' in kwargs:
|
||||||
warnings.warn("pageable param is deprecated for Grid(); "
|
warnings.warn("pageable param is deprecated for Grid(); "
|
||||||
"please use vue_tagname param instead",
|
"please use paginated param instead",
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
kwargs.setdefault('paginated', kwargs.pop('pageable'))
|
kwargs.setdefault('paginated', kwargs.pop('pageable'))
|
||||||
|
|
||||||
|
@ -254,10 +251,21 @@ class Grid(WuttaGrid):
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
kwargs.setdefault('page', kwargs.pop('default_page'))
|
kwargs.setdefault('page', kwargs.pop('default_page'))
|
||||||
|
|
||||||
|
if 'searchable' in kwargs:
|
||||||
|
warnings.warn("searchable param is deprecated for Grid(); "
|
||||||
|
"please use searchable_columns param instead",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
kwargs.setdefault('searchable_columns', kwargs.pop('searchable'))
|
||||||
|
|
||||||
# TODO: this should not be needed once all templates correctly
|
# TODO: this should not be needed once all templates correctly
|
||||||
# reference grid.vue_component etc.
|
# reference grid.vue_component etc.
|
||||||
kwargs.setdefault('vue_tagname', 'tailbone-grid')
|
kwargs.setdefault('vue_tagname', 'tailbone-grid')
|
||||||
|
|
||||||
|
# nb. these must be set before super init, as they are
|
||||||
|
# referenced when constructing filters
|
||||||
|
self.assume_local_times = assume_local_times
|
||||||
|
self.use_byte_string_filters = use_byte_string_filters
|
||||||
|
|
||||||
kwargs['key'] = key
|
kwargs['key'] = key
|
||||||
kwargs['data'] = data
|
kwargs['data'] = data
|
||||||
super().__init__(request, **kwargs)
|
super().__init__(request, **kwargs)
|
||||||
|
@ -275,19 +283,11 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
self.width = width
|
self.width = width
|
||||||
self.enums = enums or {}
|
self.enums = enums or {}
|
||||||
self.assume_local_times = assume_local_times
|
|
||||||
self.renderers = self.make_default_renderers(self.renderers)
|
self.renderers = self.make_default_renderers(self.renderers)
|
||||||
self.raw_renderers = raw_renderers or {}
|
self.raw_renderers = raw_renderers or {}
|
||||||
self.invisible = invisible or []
|
self.invisible = invisible or []
|
||||||
self.extra_row_class = extra_row_class
|
self.extra_row_class = extra_row_class
|
||||||
self.url = url
|
self.url = url
|
||||||
self.joiners = joiners or {}
|
|
||||||
|
|
||||||
self.filterable = filterable
|
|
||||||
self.use_byte_string_filters = use_byte_string_filters
|
|
||||||
self.filters = self.make_filters(filters)
|
|
||||||
|
|
||||||
self.searchable = searchable or {}
|
|
||||||
|
|
||||||
self.checkboxes = checkboxes
|
self.checkboxes = checkboxes
|
||||||
self.checked = checked
|
self.checked = checked
|
||||||
|
@ -443,10 +443,14 @@ class Grid(WuttaGrid):
|
||||||
self.remove(oldfield)
|
self.remove(oldfield)
|
||||||
|
|
||||||
def set_joiner(self, key, joiner):
|
def set_joiner(self, key, joiner):
|
||||||
|
""" """
|
||||||
if joiner is None:
|
if joiner is None:
|
||||||
self.joiners.pop(key, None)
|
warnings.warn("specifying None is deprecated for Grid.set_joiner(); "
|
||||||
|
"please use Grid.remove_joiner() instead",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
self.remove_joiner(key)
|
||||||
else:
|
else:
|
||||||
self.joiners[key] = joiner
|
super().set_joiner(key, joiner)
|
||||||
|
|
||||||
def set_sorter(self, key, *args, **kwargs):
|
def set_sorter(self, key, *args, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
|
@ -474,41 +478,19 @@ class Grid(WuttaGrid):
|
||||||
self.sorters[key] = self.make_sorter(*args, **kwargs)
|
self.sorters[key] = self.make_sorter(*args, **kwargs)
|
||||||
|
|
||||||
def set_filter(self, key, *args, **kwargs):
|
def set_filter(self, key, *args, **kwargs):
|
||||||
if len(args) == 1 and args[0] is None:
|
""" """
|
||||||
self.remove_filter(key)
|
if len(args) == 1:
|
||||||
else:
|
if args[0] is None:
|
||||||
if 'label' not in kwargs and key in self.labels:
|
warnings.warn("specifying None is deprecated for Grid.set_filter(); "
|
||||||
kwargs['label'] = self.labels[key]
|
"please use Grid.remove_filter() instead",
|
||||||
self.filters[key] = self.make_filter(key, *args, **kwargs)
|
DeprecationWarning, stacklevel=2)
|
||||||
|
self.remove_filter(key)
|
||||||
|
return
|
||||||
|
|
||||||
def set_searchable(self, key, searchable=True):
|
# TODO: our make_filter() signature differs from upstream,
|
||||||
if searchable:
|
# so must call it explicitly instead of delegating to super
|
||||||
self.searchable[key] = True
|
kwargs.setdefault('label', self.get_label(key))
|
||||||
else:
|
self.filters[key] = self.make_filter(key, *args, **kwargs)
|
||||||
self.searchable.pop(key, None)
|
|
||||||
|
|
||||||
def is_searchable(self, key):
|
|
||||||
return self.searchable.get(key, False)
|
|
||||||
|
|
||||||
def remove_filter(self, key):
|
|
||||||
self.filters.pop(key, None)
|
|
||||||
|
|
||||||
def set_label(self, key, label, column_only=False):
|
|
||||||
"""
|
|
||||||
Set/override the label for a column.
|
|
||||||
|
|
||||||
This overrides
|
|
||||||
:meth:`~wuttaweb:wuttaweb.grids.base.Grid.set_label()` to add
|
|
||||||
the following params:
|
|
||||||
|
|
||||||
:param column_only: Boolean indicating whether the label
|
|
||||||
should be applied *only* to the column header (if
|
|
||||||
``True``), vs. applying also to the filter (if ``False``).
|
|
||||||
"""
|
|
||||||
super().set_label(key, label)
|
|
||||||
|
|
||||||
if not column_only and key in self.filters:
|
|
||||||
self.filters[key].label = label
|
|
||||||
|
|
||||||
def set_click_handler(self, key, handler):
|
def set_click_handler(self, key, handler):
|
||||||
if handler:
|
if handler:
|
||||||
|
@ -593,7 +575,11 @@ class Grid(WuttaGrid):
|
||||||
return getattr(obj, column_name)
|
return getattr(obj, column_name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
return obj[column_name]
|
|
||||||
|
try:
|
||||||
|
return obj[column_name]
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
def render_currency(self, obj, column_name):
|
def render_currency(self, obj, column_name):
|
||||||
value = self.obtain_value(obj, column_name)
|
value = self.obtain_value(obj, column_name)
|
||||||
|
@ -708,6 +694,14 @@ class Grid(WuttaGrid):
|
||||||
def actions_column_format(self, column_number, row_number, item):
|
def actions_column_format(self, column_number, row_number, item):
|
||||||
return HTML.td(self.render_actions(item, row_number), class_='actions')
|
return HTML.td(self.render_actions(item, row_number), class_='actions')
|
||||||
|
|
||||||
|
# TODO: upstream should handle this..
|
||||||
|
def make_backend_filters(self, filters=None):
|
||||||
|
""" """
|
||||||
|
final = self.get_default_filters()
|
||||||
|
if filters:
|
||||||
|
final.update(filters)
|
||||||
|
return final
|
||||||
|
|
||||||
def get_default_filters(self):
|
def get_default_filters(self):
|
||||||
"""
|
"""
|
||||||
Returns the default set of filters provided by the grid.
|
Returns the default set of filters provided by the grid.
|
||||||
|
@ -732,16 +726,6 @@ class Grid(WuttaGrid):
|
||||||
filters[prop.key] = self.make_filter(prop.key, column)
|
filters[prop.key] = self.make_filter(prop.key, column)
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
def make_filters(self, filters=None):
|
|
||||||
"""
|
|
||||||
Returns an initial set of filters which will be available to the grid.
|
|
||||||
The grid itself may or may not provide some default filters, and the
|
|
||||||
``filters`` kwarg may contain additions and/or overrides.
|
|
||||||
"""
|
|
||||||
if filters:
|
|
||||||
return filters
|
|
||||||
return self.get_default_filters()
|
|
||||||
|
|
||||||
def make_filter(self, key, column, **kwargs):
|
def make_filter(self, key, column, **kwargs):
|
||||||
"""
|
"""
|
||||||
Make a filter suitable for use with the given column.
|
Make a filter suitable for use with the given column.
|
||||||
|
@ -879,9 +863,13 @@ class Grid(WuttaGrid):
|
||||||
settings['page'] = self.page
|
settings['page'] = self.page
|
||||||
if self.filterable:
|
if self.filterable:
|
||||||
for filtr in self.iter_filters():
|
for filtr in self.iter_filters():
|
||||||
settings['filter.{}.active'.format(filtr.key)] = filtr.default_active
|
defaults = self.filter_defaults.get(filtr.key, {})
|
||||||
settings['filter.{}.verb'.format(filtr.key)] = filtr.default_verb
|
settings[f'filter.{filtr.key}.active'] = defaults.get('active',
|
||||||
settings['filter.{}.value'.format(filtr.key)] = filtr.default_value
|
filtr.default_active)
|
||||||
|
settings[f'filter.{filtr.key}.verb'] = defaults.get('verb',
|
||||||
|
filtr.default_verb)
|
||||||
|
settings[f'filter.{filtr.key}.value'] = defaults.get('value',
|
||||||
|
filtr.default_value)
|
||||||
|
|
||||||
# If user has default settings on file, apply those first.
|
# If user has default settings on file, apply those first.
|
||||||
if self.user_has_defaults():
|
if self.user_has_defaults():
|
||||||
|
@ -889,13 +877,13 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
# If request contains instruction to reset to default filters, then we
|
# If request contains instruction to reset to default filters, then we
|
||||||
# can skip the rest of the request/session checks.
|
# can skip the rest of the request/session checks.
|
||||||
if self.request.GET.get('reset-to-default-filters') == 'true':
|
if self.request.GET.get('reset-view'):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# If request has filter settings, grab those, then grab sort/pager
|
# If request has filter settings, grab those, then grab sort/pager
|
||||||
# settings from request or session.
|
# settings from request or session.
|
||||||
elif self.filterable and self.request_has_settings('filter'):
|
elif self.request_has_settings('filter'):
|
||||||
self.update_filter_settings(settings, 'request')
|
self.update_filter_settings(settings, src='request')
|
||||||
if self.request_has_settings('sort'):
|
if self.request_has_settings('sort'):
|
||||||
self.update_sort_settings(settings, src='request')
|
self.update_sort_settings(settings, src='request')
|
||||||
else:
|
else:
|
||||||
|
@ -907,7 +895,7 @@ class Grid(WuttaGrid):
|
||||||
# settings from request or session.
|
# settings from request or session.
|
||||||
elif self.request_has_settings('sort'):
|
elif self.request_has_settings('sort'):
|
||||||
self.update_sort_settings(settings, src='request')
|
self.update_sort_settings(settings, src='request')
|
||||||
self.update_filter_settings(settings, 'session')
|
self.update_filter_settings(settings, src='session')
|
||||||
self.update_page_settings(settings)
|
self.update_page_settings(settings)
|
||||||
|
|
||||||
# NOTE: These next two are functionally equivalent, but are kept
|
# NOTE: These next two are functionally equivalent, but are kept
|
||||||
|
@ -917,12 +905,12 @@ class Grid(WuttaGrid):
|
||||||
# grab those, then grab filter/sort settings from session.
|
# grab those, then grab filter/sort settings from session.
|
||||||
elif self.request_has_settings('page'):
|
elif self.request_has_settings('page'):
|
||||||
self.update_page_settings(settings)
|
self.update_page_settings(settings)
|
||||||
self.update_filter_settings(settings, 'session')
|
self.update_filter_settings(settings, src='session')
|
||||||
self.update_sort_settings(settings, src='session')
|
self.update_sort_settings(settings, src='session')
|
||||||
|
|
||||||
# If request has no settings, grab all from session.
|
# If request has no settings, grab all from session.
|
||||||
elif self.session_has_settings():
|
elif self.session_has_settings():
|
||||||
self.update_filter_settings(settings, 'session')
|
self.update_filter_settings(settings, src='session')
|
||||||
self.update_sort_settings(settings, src='session')
|
self.update_sort_settings(settings, src='session')
|
||||||
self.update_page_settings(settings)
|
self.update_page_settings(settings)
|
||||||
|
|
||||||
|
@ -1062,18 +1050,11 @@ class Grid(WuttaGrid):
|
||||||
merge('page', int)
|
merge('page', int)
|
||||||
|
|
||||||
def request_has_settings(self, type_):
|
def request_has_settings(self, type_):
|
||||||
"""
|
""" """
|
||||||
Determine if the current request (GET query string) contains any
|
if super().request_has_settings(type_):
|
||||||
filter/sort settings for the grid.
|
return True
|
||||||
"""
|
|
||||||
if type_ == 'filter':
|
|
||||||
for filtr in self.iter_filters():
|
|
||||||
if filtr.key in self.request.GET:
|
|
||||||
return True
|
|
||||||
if 'filter' in self.request.GET: # user may be applying empty filters
|
|
||||||
return True
|
|
||||||
|
|
||||||
elif type_ == 'sort':
|
if type_ == 'sort':
|
||||||
|
|
||||||
# TODO: remove this eventually, but some links in the wild
|
# TODO: remove this eventually, but some links in the wild
|
||||||
# may still include these params, so leave it for now
|
# may still include these params, so leave it for now
|
||||||
|
@ -1081,14 +1062,6 @@ class Grid(WuttaGrid):
|
||||||
if key in self.request.GET:
|
if key in self.request.GET:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if 'sort1key' in self.request.GET:
|
|
||||||
return True
|
|
||||||
|
|
||||||
elif type_ == 'page':
|
|
||||||
for key in ['pagesize', 'page']:
|
|
||||||
if key in self.request.GET:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def session_has_settings(self):
|
def session_has_settings(self):
|
||||||
|
@ -1104,72 +1077,6 @@ class Grid(WuttaGrid):
|
||||||
return any([key.startswith(f'{prefix}.filter')
|
return any([key.startswith(f'{prefix}.filter')
|
||||||
for key in self.request.session])
|
for key in self.request.session])
|
||||||
|
|
||||||
def update_filter_settings(self, settings, source):
|
|
||||||
"""
|
|
||||||
Updates a settings dictionary according to filter settings data found
|
|
||||||
in either the GET query string, or session storage.
|
|
||||||
|
|
||||||
:param settings: Dictionary of initial settings, which is to be updated.
|
|
||||||
|
|
||||||
:param source: String identifying the source to consult for settings
|
|
||||||
data. Must be one of: ``('request', 'session')``.
|
|
||||||
"""
|
|
||||||
if not self.filterable:
|
|
||||||
return
|
|
||||||
|
|
||||||
for filtr in self.iter_filters():
|
|
||||||
prefix = 'filter.{}'.format(filtr.key)
|
|
||||||
|
|
||||||
if source == 'request':
|
|
||||||
# consider filter active if query string contains a value for it
|
|
||||||
settings['{}.active'.format(prefix)] = filtr.key in self.request.GET
|
|
||||||
settings['{}.verb'.format(prefix)] = self.get_setting(
|
|
||||||
settings, f'{filtr.key}.verb', src='request', default='')
|
|
||||||
settings['{}.value'.format(prefix)] = self.get_setting(
|
|
||||||
settings, filtr.key, src='request', default='')
|
|
||||||
|
|
||||||
else: # source = session
|
|
||||||
settings['{}.active'.format(prefix)] = self.get_setting(
|
|
||||||
settings, f'{prefix}.active', src='session',
|
|
||||||
normalize=lambda v: str(v).lower() == 'true', default=False)
|
|
||||||
settings['{}.verb'.format(prefix)] = self.get_setting(
|
|
||||||
settings, f'{prefix}.verb', src='session', default='')
|
|
||||||
settings['{}.value'.format(prefix)] = self.get_setting(
|
|
||||||
settings, f'{prefix}.value', src='session', default='')
|
|
||||||
|
|
||||||
def update_page_settings(self, settings):
|
|
||||||
"""
|
|
||||||
Updates a settings dictionary according to pager settings data found in
|
|
||||||
either the GET query string, or session storage.
|
|
||||||
|
|
||||||
Note that due to how the actual pager functions, the effective settings
|
|
||||||
will often come from *both* the request and session. This is so that
|
|
||||||
e.g. the page size will remain constant (coming from the session) while
|
|
||||||
the user jumps between pages (which only provides the single setting).
|
|
||||||
|
|
||||||
:param settings: Dictionary of initial settings, which is to be updated.
|
|
||||||
"""
|
|
||||||
if not self.paginated:
|
|
||||||
return
|
|
||||||
|
|
||||||
pagesize = self.request.GET.get('pagesize')
|
|
||||||
if pagesize is not None:
|
|
||||||
if pagesize.isdigit():
|
|
||||||
settings['pagesize'] = int(pagesize)
|
|
||||||
else:
|
|
||||||
pagesize = self.request.session.get('grid.{}.pagesize'.format(self.key))
|
|
||||||
if pagesize is not None:
|
|
||||||
settings['pagesize'] = pagesize
|
|
||||||
|
|
||||||
page = self.request.GET.get('page')
|
|
||||||
if page is not None:
|
|
||||||
if page.isdigit():
|
|
||||||
settings['page'] = int(page)
|
|
||||||
else:
|
|
||||||
page = self.request.session.get('grid.{}.page'.format(self.key))
|
|
||||||
if page is not None:
|
|
||||||
settings['page'] = int(page)
|
|
||||||
|
|
||||||
def persist_settings(self, settings, dest='session'):
|
def persist_settings(self, settings, dest='session'):
|
||||||
""" """
|
""" """
|
||||||
if dest not in ('defaults', 'session'):
|
if dest not in ('defaults', 'session'):
|
||||||
|
@ -1257,89 +1164,12 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def sort_data(self, data, sorters=None):
|
|
||||||
""" """
|
|
||||||
if sorters is None:
|
|
||||||
sorters = self.active_sorters
|
|
||||||
if not sorters:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# nb. when data is a query, we want to apply sorters in the
|
|
||||||
# requested order, so the final query has order_by() in the
|
|
||||||
# correct "as-is" sequence. however when data is a list we
|
|
||||||
# must do the opposite, applying in the reverse order, so the
|
|
||||||
# final list has the most "important" sort(s) applied last.
|
|
||||||
if not isinstance(data, orm.Query):
|
|
||||||
sorters = reversed(sorters)
|
|
||||||
|
|
||||||
for sorter in sorters:
|
|
||||||
sortkey = sorter['key']
|
|
||||||
sortdir = sorter['dir']
|
|
||||||
|
|
||||||
# cannot sort unless we have a sorter callable
|
|
||||||
sortfunc = self.sorters.get(sortkey)
|
|
||||||
if not sortfunc:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# join appropriate model if needed
|
|
||||||
if sortkey in self.joiners and sortkey not in self.joined:
|
|
||||||
data = self.joiners[sortkey](data)
|
|
||||||
self.joined.add(sortkey)
|
|
||||||
|
|
||||||
# invoke the sorter
|
|
||||||
data = sortfunc(data, sortdir)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def paginate_data(self, data):
|
|
||||||
"""
|
|
||||||
Paginate the given data set according to current settings, and return
|
|
||||||
the result.
|
|
||||||
"""
|
|
||||||
# we of course assume our current page is correct, at first
|
|
||||||
pager = self.make_pager(data)
|
|
||||||
|
|
||||||
# if pager has detected that our current page is outside the valid
|
|
||||||
# range, we must re-orient ourself around the "new" (valid) page
|
|
||||||
if pager.page != self.page:
|
|
||||||
self.page = pager.page
|
|
||||||
self.request.session['grid.{}.page'.format(self.key)] = self.page
|
|
||||||
pager = self.make_pager(data)
|
|
||||||
|
|
||||||
return pager
|
|
||||||
|
|
||||||
def make_pager(self, data):
|
|
||||||
|
|
||||||
# TODO: this seems hacky..normally we expect `data` to be a
|
|
||||||
# query of course, but in some cases it may be a list instead.
|
|
||||||
# if so then we can't use ORM pager
|
|
||||||
if isinstance(data, list):
|
|
||||||
import paginate
|
|
||||||
return paginate.Page(data,
|
|
||||||
items_per_page=self.pagesize,
|
|
||||||
page=self.page)
|
|
||||||
|
|
||||||
return SqlalchemyOrmPage(data,
|
|
||||||
items_per_page=self.pagesize,
|
|
||||||
page=self.page,
|
|
||||||
url_maker=URLMaker(self.request))
|
|
||||||
|
|
||||||
def make_visible_data(self):
|
def make_visible_data(self):
|
||||||
"""
|
""" """
|
||||||
Apply various settings to the raw data set, to produce a final data
|
warnings.warn("grid.make_visible_data() method is deprecated; "
|
||||||
set. This will page / sort / filter as necessary, according to the
|
"please use grid.get_visible_data() instead",
|
||||||
grid's defaults and the current request etc.
|
DeprecationWarning, stacklevel=2)
|
||||||
"""
|
return self.get_visible_data()
|
||||||
self.joined = set()
|
|
||||||
data = self.data
|
|
||||||
if self.filterable:
|
|
||||||
data = self.filter_data(data)
|
|
||||||
if self.sortable:
|
|
||||||
data = self.sort_data(data)
|
|
||||||
if self.paginated:
|
|
||||||
self.pager = self.paginate_data(data)
|
|
||||||
data = self.pager
|
|
||||||
return data
|
|
||||||
|
|
||||||
def render_vue_tag(self, master=None, **kwargs):
|
def render_vue_tag(self, master=None, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
|
@ -1362,7 +1192,7 @@ class Grid(WuttaGrid):
|
||||||
includes the context menu items and grid tools.
|
includes the context menu items and grid tools.
|
||||||
"""
|
"""
|
||||||
if 'grid_columns' not in kwargs:
|
if 'grid_columns' not in kwargs:
|
||||||
kwargs['grid_columns'] = self.get_table_columns()
|
kwargs['grid_columns'] = self.get_vue_columns()
|
||||||
|
|
||||||
if 'grid_data' not in kwargs:
|
if 'grid_data' not in kwargs:
|
||||||
kwargs['grid_data'] = self.get_table_data()
|
kwargs['grid_data'] = self.get_table_data()
|
||||||
|
@ -1385,6 +1215,7 @@ class Grid(WuttaGrid):
|
||||||
return HTML.literal(html)
|
return HTML.literal(html)
|
||||||
|
|
||||||
def render_buefy(self, **kwargs):
|
def render_buefy(self, **kwargs):
|
||||||
|
""" """
|
||||||
warnings.warn("Grid.render_buefy() is deprecated; "
|
warnings.warn("Grid.render_buefy() is deprecated; "
|
||||||
"please use Grid.render_complete() instead",
|
"please use Grid.render_complete() instead",
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
|
@ -1392,6 +1223,7 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
def render_table_element(self, template='/grids/b-table.mako',
|
def render_table_element(self, template='/grids/b-table.mako',
|
||||||
data_prop='gridData', empty_labels=False,
|
data_prop='gridData', empty_labels=False,
|
||||||
|
literal=False,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
This is intended for ad-hoc "small" grids with static data. Renders
|
This is intended for ad-hoc "small" grids with static data. Renders
|
||||||
|
@ -1403,12 +1235,15 @@ class Grid(WuttaGrid):
|
||||||
context['data_prop'] = data_prop
|
context['data_prop'] = data_prop
|
||||||
context['empty_labels'] = empty_labels
|
context['empty_labels'] = empty_labels
|
||||||
if 'grid_columns' not in context:
|
if 'grid_columns' not in context:
|
||||||
context['grid_columns'] = self.get_table_columns()
|
context['grid_columns'] = self.get_vue_columns()
|
||||||
context.setdefault('paginated', False)
|
context.setdefault('paginated', False)
|
||||||
if context['paginated']:
|
if context['paginated']:
|
||||||
context.setdefault('per_page', 20)
|
context.setdefault('per_page', 20)
|
||||||
context['view_click_handler'] = self.get_view_click_handler()
|
context['view_click_handler'] = self.get_view_click_handler()
|
||||||
return render(template, context)
|
result = render(template, context)
|
||||||
|
if literal:
|
||||||
|
result = HTML.literal(result)
|
||||||
|
return result
|
||||||
|
|
||||||
def get_view_click_handler(self):
|
def get_view_click_handler(self):
|
||||||
""" """
|
""" """
|
||||||
|
@ -1417,7 +1252,7 @@ class Grid(WuttaGrid):
|
||||||
view = None
|
view = None
|
||||||
for action in self.actions:
|
for action in self.actions:
|
||||||
if action.key == 'view':
|
if action.key == 'view':
|
||||||
return action.click_handler
|
return getattr(action, 'click_handler', None)
|
||||||
|
|
||||||
def set_filters_sequence(self, filters, only=False):
|
def set_filters_sequence(self, filters, only=False):
|
||||||
"""
|
"""
|
||||||
|
@ -1491,28 +1326,6 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def render_filters(self, template='/grids/filters.mako', **kwargs):
|
|
||||||
"""
|
|
||||||
Render the filters to a Unicode string, using the specified template.
|
|
||||||
Additional kwargs are passed along as context to the template.
|
|
||||||
"""
|
|
||||||
# Provide default data to filters form, so renderer can do some of the
|
|
||||||
# work for us.
|
|
||||||
data = {}
|
|
||||||
for filtr in self.iter_active_filters():
|
|
||||||
data['{}.active'.format(filtr.key)] = filtr.active
|
|
||||||
data['{}.verb'.format(filtr.key)] = filtr.verb
|
|
||||||
data[filtr.key] = filtr.value
|
|
||||||
|
|
||||||
form = gridfilters.GridFiltersForm(self.filters,
|
|
||||||
request=self.request,
|
|
||||||
defaults=data)
|
|
||||||
|
|
||||||
kwargs['request'] = self.request
|
|
||||||
kwargs['grid'] = self
|
|
||||||
kwargs['form'] = form
|
|
||||||
return render(template, kwargs)
|
|
||||||
|
|
||||||
def render_actions(self, row, i): # pragma: no cover
|
def render_actions(self, row, i): # pragma: no cover
|
||||||
""" """
|
""" """
|
||||||
warnings.warn("grid.render_actions() is deprecated!",
|
warnings.warn("grid.render_actions() is deprecated!",
|
||||||
|
@ -1574,22 +1387,19 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
def get_vue_columns(self):
|
def get_vue_columns(self):
|
||||||
""" """
|
""" """
|
||||||
return self.get_table_columns()
|
columns = super().get_vue_columns()
|
||||||
|
|
||||||
|
for column in columns:
|
||||||
|
column['visible'] = column['field'] not in self.invisible
|
||||||
|
|
||||||
|
return columns
|
||||||
|
|
||||||
def get_table_columns(self):
|
def get_table_columns(self):
|
||||||
"""
|
""" """
|
||||||
Return a list of dicts representing all grid columns. Meant
|
warnings.warn("grid.get_table_columns() method is deprecated; "
|
||||||
for use with the client-side JS table.
|
"please use grid.get_vue_columns() instead",
|
||||||
"""
|
DeprecationWarning, stacklevel=2)
|
||||||
columns = []
|
return self.get_vue_columns()
|
||||||
for name in self.columns:
|
|
||||||
columns.append({
|
|
||||||
'field': name,
|
|
||||||
'label': self.get_label(name),
|
|
||||||
'sortable': self.is_sortable(name),
|
|
||||||
'visible': name not in self.invisible,
|
|
||||||
})
|
|
||||||
return columns
|
|
||||||
|
|
||||||
def get_uuid_for_row(self, rowobj):
|
def get_uuid_for_row(self, rowobj):
|
||||||
|
|
||||||
|
@ -1601,6 +1411,10 @@ class Grid(WuttaGrid):
|
||||||
if hasattr(rowobj, 'uuid'):
|
if hasattr(rowobj, 'uuid'):
|
||||||
return rowobj.uuid
|
return rowobj.uuid
|
||||||
|
|
||||||
|
def get_vue_context(self):
|
||||||
|
""" """
|
||||||
|
return self.get_table_data()
|
||||||
|
|
||||||
def get_vue_data(self):
|
def get_vue_data(self):
|
||||||
""" """
|
""" """
|
||||||
table_data = self.get_table_data()
|
table_data = self.get_table_data()
|
||||||
|
@ -1615,7 +1429,7 @@ class Grid(WuttaGrid):
|
||||||
return self._table_data
|
return self._table_data
|
||||||
|
|
||||||
# filter / sort / paginate to get "visible" data
|
# filter / sort / paginate to get "visible" data
|
||||||
raw_data = self.make_visible_data()
|
raw_data = self.get_visible_data()
|
||||||
data = []
|
data = []
|
||||||
status_map = {}
|
status_map = {}
|
||||||
checked = []
|
checked = []
|
||||||
|
@ -1656,10 +1470,22 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
# leverage configured rendering logic where applicable;
|
# leverage configured rendering logic where applicable;
|
||||||
# otherwise use "raw" data value as string
|
# otherwise use "raw" data value as string
|
||||||
|
value = self.obtain_value(rowobj, name)
|
||||||
if self.renderers and name in self.renderers:
|
if self.renderers and name in self.renderers:
|
||||||
value = self.renderers[name](rowobj, name)
|
renderer = self.renderers[name]
|
||||||
else:
|
|
||||||
value = self.obtain_value(rowobj, name)
|
# TODO: legacy renderer callables require 2 args,
|
||||||
|
# but wuttaweb callables require 3 args
|
||||||
|
sig = inspect.signature(renderer)
|
||||||
|
required = [param for param in sig.parameters.values()
|
||||||
|
if param.default == param.empty]
|
||||||
|
|
||||||
|
if len(required) == 2:
|
||||||
|
# TODO: legacy renderer
|
||||||
|
value = renderer(rowobj, name)
|
||||||
|
else: # the future
|
||||||
|
value = renderer(rowobj, name, value)
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
value = ""
|
value = ""
|
||||||
|
|
||||||
|
@ -1692,6 +1518,8 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
results = {
|
results = {
|
||||||
'data': data,
|
'data': data,
|
||||||
|
'row_classes': status_map,
|
||||||
|
# TODO: deprecate / remove this
|
||||||
'row_status_map': status_map,
|
'row_status_map': status_map,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1720,6 +1548,11 @@ class Grid(WuttaGrid):
|
||||||
self._table_data = results
|
self._table_data = results
|
||||||
return self._table_data
|
return self._table_data
|
||||||
|
|
||||||
|
# TODO: remove this when we use upstream GridAction
|
||||||
|
def add_action(self, key, **kwargs):
|
||||||
|
""" """
|
||||||
|
self.actions.append(GridAction(self.request, key, **kwargs))
|
||||||
|
|
||||||
def set_action_urls(self, row, rowobj, i):
|
def set_action_urls(self, row, rowobj, i):
|
||||||
"""
|
"""
|
||||||
Pre-generate all action URLs for the given data row. Meant for use
|
Pre-generate all action URLs for the given data row. Meant for use
|
||||||
|
|
|
@ -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,12 +36,7 @@ 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 wuttaweb.util import get_liburl
|
|
||||||
from tailbone.util import (csrf_token, get_csrf_token,
|
|
||||||
pretty_datetime, raw_datetime,
|
|
||||||
render_markdown,
|
render_markdown,
|
||||||
route_exists)
|
route_exists)
|
||||||
|
|
||||||
|
|
|
@ -394,6 +394,11 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
||||||
'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',
|
||||||
|
@ -451,6 +456,11 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
||||||
'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",
|
||||||
|
@ -703,7 +713,7 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
||||||
},
|
},
|
||||||
{'type': 'sep'},
|
{'type': 'sep'},
|
||||||
{
|
{
|
||||||
'title': "App Details",
|
'title': "App Info",
|
||||||
'route': 'appinfo',
|
'route': 'appinfo',
|
||||||
'perm': 'appinfo.list',
|
'perm': 'appinfo.list',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,247 +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)">
|
|
||||||
% if request.use_oruga:
|
|
||||||
<o-icon icon="edit" />
|
|
||||||
% else:
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
% endif
|
|
||||||
Edit
|
|
||||||
</a>
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
</${b}-table>
|
|
||||||
|
|
||||||
% for weblib in weblibs:
|
|
||||||
${h.hidden('wuttaweb.libver.{}'.format(weblib['key']), **{':value': "simpleSettings['wuttaweb.libver.{}']".format(weblib['key'])})}
|
|
||||||
${h.hidden('wuttaweb.liburl.{}'.format(weblib['key']), **{':value': "simpleSettings['wuttaweb.liburl.{}']".format(weblib['key'])})}
|
|
||||||
% endfor
|
|
||||||
|
|
||||||
<${b}-modal has-modal-card
|
|
||||||
% if request.use_oruga:
|
|
||||||
v-model:active="editWebLibraryShowDialog"
|
|
||||||
% else:
|
|
||||||
:active.sync="editWebLibraryShowDialog"
|
|
||||||
% endif
|
|
||||||
>
|
|
||||||
<div class="modal-card">
|
|
||||||
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<p class="modal-card-title">Web Library: {{ editWebLibraryRecord.title }}</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="modal-card-body">
|
|
||||||
|
|
||||||
<b-field grouped>
|
|
||||||
|
|
||||||
<b-field label="Default Version">
|
|
||||||
<b-input v-model="editWebLibraryRecord.default_version"
|
|
||||||
disabled>
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Override Version">
|
|
||||||
<b-input v-model="editWebLibraryVersion">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Override URL">
|
|
||||||
<b-input v-model="editWebLibraryURL"
|
|
||||||
expanded />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Effective URL (as of last page load)">
|
|
||||||
<b-input v-model="editWebLibraryRecord.live_url"
|
|
||||||
disabled
|
|
||||||
expanded />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<b-button type="is-primary"
|
|
||||||
@click="editWebLibrarySave()"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="save">
|
|
||||||
Save
|
|
||||||
</b-button>
|
|
||||||
<b-button @click="editWebLibraryShowDialog = false">
|
|
||||||
Cancel
|
|
||||||
</b-button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</${b}-modal>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="modify_vue_vars()">
|
|
||||||
${parent.modify_vue_vars()}
|
|
||||||
<script>
|
|
||||||
|
|
||||||
ThisPageData.weblibs = ${json.dumps(weblibs)|n}
|
|
||||||
|
|
||||||
ThisPageData.editWebLibraryShowDialog = false
|
|
||||||
ThisPageData.editWebLibraryRecord = {}
|
|
||||||
ThisPageData.editWebLibraryVersion = null
|
|
||||||
ThisPageData.editWebLibraryURL = null
|
|
||||||
|
|
||||||
ThisPage.methods.editWebLibraryInit = function(row) {
|
|
||||||
this.editWebLibraryRecord = row
|
|
||||||
this.editWebLibraryVersion = row.configured_version
|
|
||||||
this.editWebLibraryURL = row.configured_url
|
|
||||||
this.editWebLibraryShowDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.methods.editWebLibrarySave = function() {
|
|
||||||
this.editWebLibraryRecord.configured_version = this.editWebLibraryVersion
|
|
||||||
this.editWebLibraryRecord.configured_url = this.editWebLibraryURL
|
|
||||||
this.editWebLibraryRecord.modified = true
|
|
||||||
|
|
||||||
this.simpleSettings[`wuttaweb.libver.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryVersion
|
|
||||||
this.simpleSettings[`wuttaweb.liburl.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryURL
|
|
||||||
|
|
||||||
this.settingsNeedSaved = true
|
|
||||||
this.editWebLibraryShowDialog = false
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</%def>
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/master/index.mako" />
|
<%inherit file="wuttaweb:templates/appinfo/index.mako" />
|
||||||
|
|
||||||
<%def name="page_content()">
|
<%def name="page_content()">
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
|
||||||
<once-button type="is-primary"
|
<once-button type="is-primary"
|
||||||
|
@ -28,95 +27,5 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<${b}-collapse class="panel" open>
|
${parent.page_content()}
|
||||||
|
|
||||||
<template #trigger="props">
|
|
||||||
<div class="panel-heading"
|
|
||||||
style="cursor: pointer;"
|
|
||||||
role="button">
|
|
||||||
|
|
||||||
## TODO: for some reason buefy will "reuse" the icon
|
|
||||||
## element in such a way that its display does not
|
|
||||||
## refresh. so to work around that, we use different
|
|
||||||
## structure for the two icons, so buefy is forced to
|
|
||||||
## re-draw
|
|
||||||
|
|
||||||
<b-icon v-if="props.open"
|
|
||||||
pack="fas"
|
|
||||||
icon="angle-down">
|
|
||||||
</b-icon>
|
|
||||||
|
|
||||||
<span v-if="!props.open">
|
|
||||||
<b-icon pack="fas"
|
|
||||||
icon="angle-right">
|
|
||||||
</b-icon>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>Configuration Files</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="panel-block">
|
|
||||||
<div style="width: 100%;">
|
|
||||||
<${b}-table :data="configFiles">
|
|
||||||
|
|
||||||
<${b}-table-column field="priority"
|
|
||||||
label="Priority"
|
|
||||||
v-slot="props">
|
|
||||||
{{ props.row.priority }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column field="path"
|
|
||||||
label="File Path"
|
|
||||||
v-slot="props">
|
|
||||||
{{ props.row.path }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
</${b}-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</${b}-collapse>
|
|
||||||
|
|
||||||
<${b}-collapse class="panel"
|
|
||||||
:open="false">
|
|
||||||
|
|
||||||
<template #trigger="props">
|
|
||||||
<div class="panel-heading"
|
|
||||||
style="cursor: pointer;"
|
|
||||||
role="button">
|
|
||||||
|
|
||||||
## TODO: for some reason buefy will "reuse" the icon
|
|
||||||
## element in such a way that its display does not
|
|
||||||
## refresh. so to work around that, we use different
|
|
||||||
## structure for the two icons, so buefy is forced to
|
|
||||||
## re-draw
|
|
||||||
|
|
||||||
<b-icon v-if="props.open"
|
|
||||||
pack="fas"
|
|
||||||
icon="angle-down">
|
|
||||||
</b-icon>
|
|
||||||
|
|
||||||
<span v-if="!props.open">
|
|
||||||
<b-icon pack="fas"
|
|
||||||
icon="angle-right">
|
|
||||||
</b-icon>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<strong>Installed Packages</strong>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="panel-block">
|
|
||||||
<div style="width: 100%;">
|
|
||||||
${grid.render_vue_tag()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</${b}-collapse>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="modify_vue_vars()">
|
|
||||||
${parent.modify_vue_vars()}
|
|
||||||
<script>
|
|
||||||
ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(request.rattail_config.prioritized_files, 1)])|n}
|
|
||||||
</script>
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
@ -632,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')}
|
||||||
|
@ -642,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>
|
||||||
|
@ -668,7 +686,7 @@
|
||||||
text="Edit This">
|
text="Edit This">
|
||||||
</once-button>
|
</once-button>
|
||||||
% endif
|
% endif
|
||||||
% if getattr(master, 'cloneable', False) and master.has_perm('clone'):
|
% if getattr(master, 'cloneable', False) and not master.cloning and master.has_perm('clone'):
|
||||||
<once-button tag="a" href="${master.get_action_url('clone', instance)}"
|
<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">
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="wuttaweb:templates/base_meta.mako" />
|
||||||
|
|
||||||
<%def name="app_title()">${rattail_app.get_node_title()}</%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="extra_styles()"></%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'))}" />
|
||||||
|
@ -13,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>
|
|
||||||
|
|
|
@ -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()}
|
||||||
|
@ -213,54 +309,34 @@
|
||||||
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 input_file_templates.values():
|
|
||||||
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
|
|
||||||
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
|
|
||||||
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
|
|
||||||
return "You must provide a file to upload for the ${tmpl['label']} template."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
% endfor
|
|
||||||
}
|
|
||||||
% endif
|
|
||||||
|
|
||||||
ThisPage.methods.validateSettings = function() {
|
|
||||||
let msg
|
|
||||||
|
|
||||||
% if input_file_template_settings is not Undefined:
|
|
||||||
msg = this.validateInputFileTemplateSettings()
|
|
||||||
if (msg) {
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
% endif
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.methods.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,5 +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>
|
||||||
|
|
|
@ -83,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
|
||||||
|
@ -99,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"
|
||||||
|
@ -162,7 +162,7 @@
|
||||||
</${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)">
|
||||||
|
@ -580,18 +580,27 @@
|
||||||
<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>
|
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>
|
expanded>
|
||||||
</b-input>
|
</b-input>
|
||||||
|
@ -606,7 +615,6 @@
|
||||||
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
|
||||||
|
@ -631,9 +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.updateConsumerDisabled = function() {
|
ThisPage.computed.updateConsumerDisabled = function() {
|
||||||
if (!this.editingConsumerKey) {
|
if (!this.editingConsumerKey) {
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -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|{};"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
:disabled="${form.vue_component}Submitting"
|
:disabled="${form.vue_component}Submitting"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="save">
|
icon-left="${form.button_icon_submit}">
|
||||||
{{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }}
|
{{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }}
|
||||||
</b-button>
|
</b-button>
|
||||||
% else:
|
% else:
|
||||||
|
@ -180,7 +180,7 @@
|
||||||
let ${form.vue_component}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},
|
||||||
|
|
|
@ -10,8 +10,70 @@
|
||||||
<div style="display: flex; flex-direction: column; justify-content: end;">
|
<div style="display: flex; flex-direction: column; justify-content: end;">
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
% if getattr(grid, 'filterable', False):
|
% 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>
|
||||||
|
@ -136,10 +198,8 @@
|
||||||
<${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.get('sortable', False))}"
|
:sortable="${json.dumps(column.get('sortable', False))|n}"
|
||||||
% if hasattr(grid, 'is_searchable') and 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.get('visible', True))}">
|
:visible="${json.dumps(column.get('visible', True))}">
|
||||||
% if hasattr(grid, 'raw_renderers') and column['field'] in grid.raw_renderers:
|
% if hasattr(grid, 'raw_renderers') and column['field'] in grid.raw_renderers:
|
||||||
|
@ -251,12 +311,16 @@
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
let ${grid.vue_component}CurrentData = ${json.dumps(grid.get_vue_data())|n}
|
const ${grid.vue_component}Context = ${json.dumps(grid.get_vue_context())|n}
|
||||||
|
let ${grid.vue_component}CurrentData = ${grid.vue_component}Context.data
|
||||||
|
|
||||||
let ${grid.vue_component}Data = {
|
let ${grid.vue_component}Data = {
|
||||||
loading: false,
|
loading: false,
|
||||||
ajaxDataUrl: ${json.dumps(getattr(grid, 'ajax_data_url', request.path_url))|n},
|
ajaxDataUrl: ${json.dumps(getattr(grid, 'ajax_data_url', request.path_url))|n},
|
||||||
|
|
||||||
|
## nb. this tracks whether grid.fetchFirstData() happened
|
||||||
|
fetchedFirstData: false,
|
||||||
|
|
||||||
savingDefaults: false,
|
savingDefaults: false,
|
||||||
|
|
||||||
data: ${grid.vue_component}CurrentData,
|
data: ${grid.vue_component}CurrentData,
|
||||||
|
@ -519,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
|
||||||
|
@ -671,7 +746,7 @@
|
||||||
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) {
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
## -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
<form action="${form.action_url}" method="GET" @submit.prevent="applyFilters()">
|
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
||||||
<grid-filter v-for="key in filtersSequence"
|
|
||||||
:key="key"
|
|
||||||
:filter="filters[key]"
|
|
||||||
ref="gridFilters">
|
|
||||||
</grid-filter>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
|
||||||
|
|
||||||
<b-button type="is-primary"
|
|
||||||
native-type="submit"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="check">
|
|
||||||
Apply Filters
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<b-button v-if="!addFilterShow"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="plus"
|
|
||||||
@click="addFilterInit()">
|
|
||||||
Add Filter
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<b-autocomplete v-if="addFilterShow"
|
|
||||||
ref="addFilterAutocomplete"
|
|
||||||
:data="addFilterChoices"
|
|
||||||
v-model="addFilterTerm"
|
|
||||||
placeholder="Add Filter"
|
|
||||||
field="key"
|
|
||||||
:custom-formatter="formatAddFilterItem"
|
|
||||||
open-on-focus
|
|
||||||
keep-first
|
|
||||||
icon-pack="fas"
|
|
||||||
clearable
|
|
||||||
clear-on-select
|
|
||||||
@select="addFilterSelect">
|
|
||||||
</b-autocomplete>
|
|
||||||
|
|
||||||
<b-button @click="resetView()"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="home">
|
|
||||||
Default View
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<b-button @click="clearFilters()"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="trash">
|
|
||||||
No Filters
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
% if allow_save_defaults and request.user:
|
|
||||||
<b-button @click="saveDefaults()"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="save"
|
|
||||||
:disabled="savingDefaults">
|
|
||||||
{{ savingDefaults ? "Working, please wait..." : "Save Defaults" }}
|
|
||||||
</b-button>
|
|
||||||
% endif
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
|
@ -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()}
|
|
||||||
|
|
|
@ -1,84 +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_vue_vars()">
|
|
||||||
${parent.modify_vue_vars()}
|
|
||||||
<script>
|
|
||||||
|
|
||||||
${form.vue_component}Data.usernameInput = null
|
|
||||||
|
|
||||||
${form.vue_component}.mounted = function() {
|
|
||||||
this.$refs.username.focus()
|
|
||||||
this.usernameInput = this.$refs.username.$el.querySelector('input')
|
|
||||||
this.usernameInput.addEventListener('keydown', this.usernameKeydown)
|
|
||||||
}
|
|
||||||
|
|
||||||
${form.vue_component}.beforeDestroy = function() {
|
|
||||||
this.usernameInput.removeEventListener('keydown', this.usernameKeydown)
|
|
||||||
}
|
|
||||||
|
|
||||||
${form.vue_component}.methods.usernameKeydown = function(event) {
|
|
||||||
if (event.which == 13) {
|
|
||||||
event.preventDefault()
|
|
||||||
this.$refs.password.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</%def>
|
|
||||||
|
|
|
@ -196,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"
|
||||||
|
|
74
tailbone/templates/ordering/configure.mako
Normal file
74
tailbone/templates/ordering/configure.mako
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/configure.mako" />
|
||||||
|
|
||||||
|
<%def name="form_content()">
|
||||||
|
|
||||||
|
<h3 class="block is-size-3">Workflows</h3>
|
||||||
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
Users can only choose from the workflows enabled below.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<b-field>
|
||||||
|
<b-checkbox name="rattail.batch.purchase.allow_ordering_from_scratch"
|
||||||
|
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_from_scratch']"
|
||||||
|
native-value="true"
|
||||||
|
@input="settingsNeedSaved = true">
|
||||||
|
From Scratch
|
||||||
|
</b-checkbox>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field>
|
||||||
|
<b-checkbox name="rattail.batch.purchase.allow_ordering_from_file"
|
||||||
|
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_from_file']"
|
||||||
|
native-value="true"
|
||||||
|
@input="settingsNeedSaved = true">
|
||||||
|
From Order File
|
||||||
|
</b-checkbox>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="block is-size-3">Vendors</h3>
|
||||||
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
|
<b-field message="If not set, user must choose a "supported" vendor.">
|
||||||
|
<b-checkbox name="rattail.batch.purchase.allow_ordering_any_vendor"
|
||||||
|
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_any_vendor']"
|
||||||
|
native-value="true"
|
||||||
|
@input="settingsNeedSaved = true">
|
||||||
|
Allow ordering for <span class="has-text-weight-bold">any</span> vendor
|
||||||
|
</b-checkbox>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="block is-size-3">Order Parsers</h3>
|
||||||
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
Only the selected file parsers will be exposed to users.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
% for Parser in order_parsers:
|
||||||
|
<b-field message="${Parser.key}">
|
||||||
|
<b-checkbox name="order_parser_${Parser.key}"
|
||||||
|
v-model="orderParsers['${Parser.key}']"
|
||||||
|
native-value="true"
|
||||||
|
@input="settingsNeedSaved = true">
|
||||||
|
${Parser.title}
|
||||||
|
</b-checkbox>
|
||||||
|
</b-field>
|
||||||
|
% endfor
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="modify_vue_vars()">
|
||||||
|
${parent.modify_vue_vars()}
|
||||||
|
<script>
|
||||||
|
ThisPageData.orderParsers = ${json.dumps(order_parsers_data)|n}
|
||||||
|
</script>
|
||||||
|
</%def>
|
|
@ -204,7 +204,7 @@
|
||||||
saving: false,
|
saving: false,
|
||||||
|
|
||||||
## 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},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -250,7 +250,7 @@
|
||||||
submitting: false,
|
submitting: false,
|
||||||
|
|
||||||
## 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},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
|
|
||||||
const ThisPageData = {
|
const ThisPageData = {
|
||||||
## 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},
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -55,19 +55,20 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_form_template()">
|
<%def name="render_form_template()">
|
||||||
<script type="text/x-template" id="${form.component}-template">
|
<script type="text/x-template" id="${form.vue_tagname}-template">
|
||||||
${self.render_form_innards()}
|
${self.render_form_innards()}
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_vue_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_vue_vars()}
|
${parent.modify_vue_vars()}
|
||||||
|
<% request.register_component(form.vue_tagname, form.vue_component) %>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako)
|
## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako)
|
||||||
|
|
||||||
let ${form.vue_component} = {
|
let ${form.vue_component} = {
|
||||||
template: '#${form.component}-template',
|
template: '#${form.vue_tagname}-template',
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
## TODO: deprecate / remove the latter option here
|
## TODO: deprecate / remove the latter option here
|
||||||
|
|
|
@ -69,12 +69,12 @@
|
||||||
<h3 class="block is-size-3">Vendors</h3>
|
<h3 class="block is-size-3">Vendors</h3>
|
||||||
<div class="block" style="padding-left: 2rem;">
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
<b-field message="If set, user must choose a "supported" vendor; otherwise they may choose "any" vendor.">
|
<b-field message="If not set, user must choose a "supported" vendor.">
|
||||||
<b-checkbox name="rattail.batch.purchase.supported_vendors_only"
|
<b-checkbox name="rattail.batch.purchase.allow_receiving_any_vendor"
|
||||||
v-model="simpleSettings['rattail.batch.purchase.supported_vendors_only']"
|
v-model="simpleSettings['rattail.batch.purchase.allow_receiving_any_vendor']"
|
||||||
native-value="true"
|
native-value="true"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true">
|
||||||
Only allow batch for "supported" vendors
|
Allow receiving for <span class="has-text-weight-bold">any</span> vendor
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
|
|
@ -45,11 +45,10 @@
|
||||||
<b-button @click="runReportShowDialog = false">
|
<b-button @click="runReportShowDialog = false">
|
||||||
Cancel
|
Cancel
|
||||||
</b-button>
|
</b-button>
|
||||||
${h.form(master.get_action_url('execute', instance))}
|
${h.form(master.get_action_url('execute', instance), **{'@submit': 'runReportSubmitting = true'})}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
@click="runReportSubmitting = true"
|
|
||||||
:disabled="runReportSubmitting"
|
:disabled="runReportSubmitting"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="arrow-circle-right">
|
icon-left="arrow-circle-right">
|
||||||
|
|
|
@ -909,7 +909,7 @@
|
||||||
${h.form(url('stop_root'), ref='stopBeingRootForm')}
|
${h.form(url('stop_root'), ref='stopBeingRootForm')}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
||||||
<a @click="stopBeingRoot()"
|
<a @click="$refs.stopBeingRootForm.submit()"
|
||||||
class="navbar-item has-background-danger has-text-white">
|
class="navbar-item has-background-danger has-text-white">
|
||||||
Stop being root
|
Stop being root
|
||||||
</a>
|
</a>
|
||||||
|
@ -918,7 +918,7 @@
|
||||||
${h.form(url('become_root'), ref='startBeingRootForm')}
|
${h.form(url('become_root'), ref='startBeingRootForm')}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
||||||
<a @click="startBeingRoot()"
|
<a @click="$refs.startBeingRootForm.submit()"
|
||||||
class="navbar-item has-background-danger has-text-white">
|
class="navbar-item has-background-danger has-text-white">
|
||||||
Become root
|
Become root
|
||||||
</a>
|
</a>
|
||||||
|
@ -1103,18 +1103,6 @@
|
||||||
const key = 'menu_' + hash + '_shown'
|
const key = 'menu_' + hash + '_shown'
|
||||||
this[key] = !this[key]
|
this[key] = !this[key]
|
||||||
},
|
},
|
||||||
|
|
||||||
% if request.is_admin:
|
|
||||||
|
|
||||||
startBeingRoot() {
|
|
||||||
this.$refs.startBeingRootForm.submit()
|
|
||||||
},
|
|
||||||
|
|
||||||
stopBeingRoot() {
|
|
||||||
this.$refs.stopBeingRootForm.submit()
|
|
||||||
},
|
|
||||||
|
|
||||||
% endif
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -666,6 +666,7 @@
|
||||||
<%def name="make_b_tooltip_component()">
|
<%def name="make_b_tooltip_component()">
|
||||||
<script type="text/x-template" id="b-tooltip-template">
|
<script type="text/x-template" id="b-tooltip-template">
|
||||||
<o-tooltip :label="label"
|
<o-tooltip :label="label"
|
||||||
|
:position="orugaPosition"
|
||||||
:multiline="multilined">
|
:multiline="multilined">
|
||||||
<slot />
|
<slot />
|
||||||
</o-tooltip>
|
</o-tooltip>
|
||||||
|
@ -676,6 +677,14 @@
|
||||||
props: {
|
props: {
|
||||||
label: String,
|
label: String,
|
||||||
multilined: Boolean,
|
multilined: Boolean,
|
||||||
|
position: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
orugaPosition() {
|
||||||
|
if (this.position) {
|
||||||
|
return this.position.replace(/^is-/, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
<%def name="base_styles()">
|
<%def name="base_styles()">
|
||||||
${parent.base_styles()}
|
${parent.base_styles()}
|
||||||
|
${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css') + '?ver={}'.format(tailbone.__version__))}
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.filters .filter-fieldname .field,
|
.filters .filter-fieldname .field,
|
||||||
|
@ -50,9 +51,11 @@
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item" href="${url('home')}"
|
<a class="navbar-item" href="${url('home')}"
|
||||||
v-show="!menuSearchActive">
|
v-show="!menuSearchActive">
|
||||||
${base_meta.header_logo()}
|
<div style="display: flex; align-items: center;">
|
||||||
<div id="global-header-title">
|
${base_meta.header_logo()}
|
||||||
${base_meta.global_title()}
|
<div id="navbar-brand-title">
|
||||||
|
${base_meta.global_title()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div v-show="menuSearchActive"
|
<div v-show="menuSearchActive"
|
||||||
|
@ -161,11 +164,88 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
% if request.has_perm('common.feedback'):
|
${parent.render_feedback_button()}
|
||||||
<feedback-form
|
</%def>
|
||||||
action="${url('feedback')}"
|
|
||||||
:message="feedbackMessage">
|
<%def name="render_crud_header_buttons()">
|
||||||
</feedback-form>
|
% if master:
|
||||||
|
% if master.viewing:
|
||||||
|
% if instance_editable and master.has_perm('edit'):
|
||||||
|
<wutta-button once
|
||||||
|
tag="a" href="${master.get_action_url('edit', instance)}"
|
||||||
|
icon-left="edit"
|
||||||
|
label="Edit This" />
|
||||||
|
% endif
|
||||||
|
% if getattr(master, 'cloneable', False) and not master.cloning and master.has_perm('clone'):
|
||||||
|
<wutta-button once
|
||||||
|
tag="a" href="${master.get_action_url('clone', instance)}"
|
||||||
|
icon-left="object-ungroup"
|
||||||
|
label="Clone This" />
|
||||||
|
% endif
|
||||||
|
% if instance_deletable and master.has_perm('delete'):
|
||||||
|
<wutta-button once type="is-danger"
|
||||||
|
tag="a" href="${master.get_action_url('delete', instance)}"
|
||||||
|
icon-left="trash"
|
||||||
|
label="Delete This" />
|
||||||
|
% endif
|
||||||
|
% elif master.editing:
|
||||||
|
% if master.has_perm('view'):
|
||||||
|
<wutta-button once
|
||||||
|
tag="a" href="${master.get_action_url('view', instance)}"
|
||||||
|
icon-left="eye"
|
||||||
|
label="View This" />
|
||||||
|
% endif
|
||||||
|
% if instance_deletable and master.has_perm('delete'):
|
||||||
|
<wutta-button once type="is-danger"
|
||||||
|
tag="a" href="${master.get_action_url('delete', instance)}"
|
||||||
|
icon-left="trash"
|
||||||
|
label="Delete This" />
|
||||||
|
% endif
|
||||||
|
% elif master.deleting:
|
||||||
|
% if master.has_perm('view'):
|
||||||
|
<wutta-button once
|
||||||
|
tag="a" href="${master.get_action_url('view', instance)}"
|
||||||
|
icon-left="eye"
|
||||||
|
label="View This" />
|
||||||
|
% endif
|
||||||
|
% if instance_editable and master.has_perm('edit'):
|
||||||
|
<wutta-button once
|
||||||
|
tag="a" href="${master.get_action_url('edit', instance)}"
|
||||||
|
icon-left="edit"
|
||||||
|
label="Edit This" />
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="render_prevnext_header_buttons()">
|
||||||
|
% if show_prev_next is not Undefined and show_prev_next:
|
||||||
|
% if prev_url:
|
||||||
|
<wutta-button once
|
||||||
|
tag="a" href="${prev_url}"
|
||||||
|
icon-left="arrow-left"
|
||||||
|
label="Older" />
|
||||||
|
% else:
|
||||||
|
<b-button tag="a" href="#"
|
||||||
|
disabled
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="arrow-left">
|
||||||
|
Older
|
||||||
|
</b-button>
|
||||||
|
% endif
|
||||||
|
% if next_url:
|
||||||
|
<wutta-button once
|
||||||
|
tag="a" href="${next_url}"
|
||||||
|
icon-left="arrow-right"
|
||||||
|
label="Newer" />
|
||||||
|
% else:
|
||||||
|
<b-button tag="a" href="#"
|
||||||
|
disabled
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="arrow-right">
|
||||||
|
Newer
|
||||||
|
</b-button>
|
||||||
|
% endif
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
@ -177,174 +257,133 @@
|
||||||
/>
|
/>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_vue_templates()">
|
<%def name="render_vue_template_feedback()">
|
||||||
${parent.render_vue_templates()}
|
<script type="text/x-template" id="feedback-template">
|
||||||
|
<div>
|
||||||
|
|
||||||
${page_help.render_template()}
|
<div class="level-item">
|
||||||
${page_help.declare_vars()}
|
<b-button type="is-primary"
|
||||||
|
@click="showFeedback()"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="comment">
|
||||||
|
Feedback
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
% if request.has_perm('common.feedback'):
|
<b-modal has-modal-card
|
||||||
<script type="text/x-template" id="feedback-template">
|
:active.sync="showDialog">
|
||||||
<div>
|
<div class="modal-card">
|
||||||
|
|
||||||
<div class="level-item">
|
<header class="modal-card-head">
|
||||||
<b-button type="is-primary"
|
<p class="modal-card-title">User Feedback</p>
|
||||||
@click="showFeedback()"
|
</header>
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="comment">
|
|
||||||
Feedback
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<b-modal has-modal-card
|
<section class="modal-card-body">
|
||||||
:active.sync="showDialog">
|
<p class="block">
|
||||||
<div class="modal-card">
|
Questions, suggestions, comments, complaints, etc.
|
||||||
|
<span class="red">regarding this website</span> are
|
||||||
|
welcome and may be submitted below.
|
||||||
|
</p>
|
||||||
|
|
||||||
<header class="modal-card-head">
|
<b-field label="User Name">
|
||||||
<p class="modal-card-title">User Feedback</p>
|
<b-input v-model="userName"
|
||||||
</header>
|
% if request.user:
|
||||||
|
disabled
|
||||||
|
% endif
|
||||||
|
>
|
||||||
|
</b-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
<section class="modal-card-body">
|
<b-field label="Referring URL">
|
||||||
<p class="block">
|
<b-input
|
||||||
Questions, suggestions, comments, complaints, etc.
|
v-model="referrer"
|
||||||
<span class="red">regarding this website</span> are
|
disabled="true">
|
||||||
welcome and may be submitted below.
|
</b-input>
|
||||||
</p>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="User Name">
|
<b-field label="Message">
|
||||||
<b-input v-model="userName"
|
<b-input type="textarea"
|
||||||
% if request.user:
|
v-model="message"
|
||||||
disabled
|
ref="textarea">
|
||||||
% endif
|
</b-input>
|
||||||
>
|
</b-field>
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Referring URL">
|
% if config.get_bool('tailbone.feedback_allows_reply'):
|
||||||
<b-input
|
<div class="level">
|
||||||
v-model="referrer"
|
<div class="level-left">
|
||||||
disabled="true">
|
<div class="level-item">
|
||||||
</b-input>
|
<b-checkbox v-model="pleaseReply"
|
||||||
</b-field>
|
@input="pleaseReplyChanged">
|
||||||
|
Please email me back{{ pleaseReply ? " at: " : "" }}
|
||||||
<b-field label="Message">
|
</b-checkbox>
|
||||||
<b-input type="textarea"
|
|
||||||
v-model="message"
|
|
||||||
ref="textarea">
|
|
||||||
</b-input>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
% if config.get_bool('tailbone.feedback_allows_reply'):
|
|
||||||
<div class="level">
|
|
||||||
<div class="level-left">
|
|
||||||
<div class="level-item">
|
|
||||||
<b-checkbox v-model="pleaseReply"
|
|
||||||
@input="pleaseReplyChanged">
|
|
||||||
Please email me back{{ pleaseReply ? " at: " : "" }}
|
|
||||||
</b-checkbox>
|
|
||||||
</div>
|
|
||||||
<div class="level-item" v-show="pleaseReply">
|
|
||||||
<b-input v-model="userEmail"
|
|
||||||
ref="userEmail">
|
|
||||||
</b-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
% endif
|
<div class="level-item" v-show="pleaseReply">
|
||||||
|
<b-input v-model="userEmail"
|
||||||
|
ref="userEmail">
|
||||||
|
</b-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<b-button @click="showDialog = false">
|
|
||||||
Cancel
|
|
||||||
</b-button>
|
|
||||||
<b-button type="is-primary"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="paper-plane"
|
|
||||||
@click="sendFeedback()"
|
|
||||||
:disabled="sendingFeedback || !message.trim()">
|
|
||||||
{{ sendingFeedback ? "Working, please wait..." : "Send Message" }}
|
|
||||||
</b-button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</b-modal>
|
|
||||||
|
|
||||||
|
<footer class="modal-card-foot">
|
||||||
|
<b-button @click="showDialog = false">
|
||||||
|
Cancel
|
||||||
|
</b-button>
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="paper-plane"
|
||||||
|
@click="sendFeedback()"
|
||||||
|
:disabled="sendingFeedback || !message || !message.trim()">
|
||||||
|
{{ sendingFeedback ? "Working, please wait..." : "Send Message" }}
|
||||||
|
</b-button>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</b-modal>
|
||||||
<script>
|
|
||||||
|
|
||||||
const FeedbackForm = {
|
</div>
|
||||||
template: '#feedback-template',
|
</script>
|
||||||
mixins: [SimpleRequestMixin],
|
</%def>
|
||||||
props: [
|
|
||||||
'action',
|
|
||||||
'message',
|
|
||||||
],
|
|
||||||
methods: {
|
|
||||||
|
|
||||||
showFeedback() {
|
<%def name="render_vue_script_feedback()">
|
||||||
this.referrer = location.href
|
${parent.render_vue_script_feedback()}
|
||||||
this.showDialog = true
|
<script>
|
||||||
this.$nextTick(function() {
|
|
||||||
this.$refs.textarea.focus()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
% if config.get_bool('tailbone.feedback_allows_reply'):
|
WuttaFeedbackForm.template = '#feedback-template'
|
||||||
pleaseReplyChanged(value) {
|
WuttaFeedbackForm.props.message = String
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$refs.userEmail.focus()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
% endif
|
|
||||||
|
|
||||||
sendFeedback() {
|
% if config.get_bool('tailbone.feedback_allows_reply'):
|
||||||
this.sendingFeedback = true
|
|
||||||
|
|
||||||
const params = {
|
WuttaFeedbackFormData.pleaseReply = false
|
||||||
referrer: this.referrer,
|
WuttaFeedbackFormData.userEmail = null
|
||||||
user: this.userUUID,
|
|
||||||
user_name: this.userName,
|
|
||||||
% if config.get_bool('tailbone.feedback_allows_reply'):
|
|
||||||
please_reply_to: this.pleaseReply ? this.userEmail : null,
|
|
||||||
% endif
|
|
||||||
message: this.message.trim(),
|
|
||||||
}
|
|
||||||
|
|
||||||
this.simplePOST(this.action, params, response => {
|
WuttaFeedbackForm.methods.pleaseReplyChanged = function(value) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.userEmail.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.$buefy.toast.open({
|
WuttaFeedbackForm.methods.getExtraParams = function() {
|
||||||
message: "Message sent! Thank you for your feedback.",
|
return {
|
||||||
type: 'is-info',
|
please_reply_to: this.pleaseReply ? this.userEmail : null,
|
||||||
duration: 4000, // 4 seconds
|
|
||||||
})
|
|
||||||
|
|
||||||
this.showDialog = false
|
|
||||||
// clear out message, in case they need to send another
|
|
||||||
this.message = ""
|
|
||||||
this.sendingFeedback = false
|
|
||||||
|
|
||||||
}, response => { // failure
|
|
||||||
this.sendingFeedback = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeedbackFormData = {
|
% endif
|
||||||
referrer: null,
|
|
||||||
userUUID: null,
|
|
||||||
userName: null,
|
|
||||||
userEmail: null,
|
|
||||||
% if config.get_bool('tailbone.feedback_allows_reply'):
|
|
||||||
pleaseReply: false,
|
|
||||||
% endif
|
|
||||||
showDialog: false,
|
|
||||||
sendingFeedback: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
// TODO: deprecate / remove these
|
||||||
% endif
|
const FeedbackForm = WuttaFeedbackForm
|
||||||
|
const FeedbackFormData = WuttaFeedbackFormData
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="render_vue_templates()">
|
||||||
|
${parent.render_vue_templates()}
|
||||||
|
${page_help.render_template()}
|
||||||
|
${page_help.declare_vars()}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_vue_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
|
@ -443,21 +482,6 @@
|
||||||
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
##############################
|
|
||||||
## feedback
|
|
||||||
##############################
|
|
||||||
|
|
||||||
% if request.has_perm('common.feedback'):
|
|
||||||
|
|
||||||
WholePageData.feedbackMessage = ""
|
|
||||||
|
|
||||||
% if request.user:
|
|
||||||
FeedbackFormData.userUUID = ${json.dumps(request.user.uuid)|n}
|
|
||||||
FeedbackFormData.userName = ${json.dumps(str(request.user))|n}
|
|
||||||
% endif
|
|
||||||
|
|
||||||
% endif
|
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
## edit fields help
|
## edit fields help
|
||||||
##############################
|
##############################
|
||||||
|
@ -477,10 +501,4 @@
|
||||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + f'?ver={tailbone.__version__}')}
|
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + f'?ver={tailbone.__version__}')}
|
||||||
${make_grid_filter_components()}
|
${make_grid_filter_components()}
|
||||||
${page_help.make_component()}
|
${page_help.make_component()}
|
||||||
% if request.has_perm('common.feedback'):
|
|
||||||
<script>
|
|
||||||
FeedbackForm.data = function() { return FeedbackFormData }
|
|
||||||
Vue.component('feedback-form', FeedbackForm)
|
|
||||||
</script>
|
|
||||||
% endif
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
@ -1,2 +1,78 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="wuttaweb:templates/configure.mako" />
|
<%inherit file="wuttaweb:templates/configure.mako" />
|
||||||
|
<%namespace name="tailbone_base" file="tailbone:templates/configure.mako" />
|
||||||
|
|
||||||
|
<%def name="input_file_templates_section()">
|
||||||
|
${tailbone_base.input_file_templates_section()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="output_file_templates_section()">
|
||||||
|
${tailbone_base.output_file_templates_section()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="modify_vue_vars()">
|
||||||
|
${parent.modify_vue_vars()}
|
||||||
|
<script>
|
||||||
|
|
||||||
|
##############################
|
||||||
|
## input file templates
|
||||||
|
##############################
|
||||||
|
|
||||||
|
% if input_file_template_settings is not Undefined:
|
||||||
|
|
||||||
|
ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n}
|
||||||
|
ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n}
|
||||||
|
ThisPageData.inputFileTemplateUploads = {
|
||||||
|
% for key in input_file_templates:
|
||||||
|
'${key}': null,
|
||||||
|
% endfor
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.validateInputFileTemplateSettings = function() {
|
||||||
|
% for tmpl in input_file_templates.values():
|
||||||
|
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
|
||||||
|
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
|
||||||
|
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
|
||||||
|
return "You must provide a file to upload for the ${tmpl['label']} template."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
% endfor
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPageData.validators.push(ThisPage.methods.validateInputFileTemplateSettings)
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
|
##############################
|
||||||
|
## output file templates
|
||||||
|
##############################
|
||||||
|
|
||||||
|
% if output_file_template_settings is not Undefined:
|
||||||
|
|
||||||
|
ThisPageData.outputFileTemplateSettings = ${json.dumps(output_file_template_settings)|n}
|
||||||
|
ThisPageData.outputFileTemplateFileOptions = ${json.dumps(output_file_options)|n}
|
||||||
|
ThisPageData.outputFileTemplateUploads = {
|
||||||
|
% for key in output_file_templates:
|
||||||
|
'${key}': null,
|
||||||
|
% endfor
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.validateOutputFileTemplateSettings = function() {
|
||||||
|
% for tmpl in output_file_templates.values():
|
||||||
|
if (this.outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
|
||||||
|
if (!this.outputFileTemplateSettings['${tmpl['setting_file']}']) {
|
||||||
|
if (!this.outputFileTemplateUploads['${tmpl['key']}']) {
|
||||||
|
return "You must provide a file to upload for the ${tmpl['label']} template."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
% endfor
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPageData.validators.push(ThisPage.methods.validateOutputFileTemplateSettings)
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
|
@ -1,2 +1,10 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="wuttaweb:templates/form.mako" />
|
<%inherit file="wuttaweb:templates/form.mako" />
|
||||||
|
|
||||||
|
<%def name="render_vue_template_form()">
|
||||||
|
% if form is not Undefined:
|
||||||
|
${form.render_vue_template(buttons=capture(self.render_form_buttons))}
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="render_form_buttons()"></%def>
|
||||||
|
|
|
@ -254,6 +254,11 @@
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
## DEPRECATED; remains for back-compat
|
||||||
|
<%def name="render_this_page()">
|
||||||
|
${self.page_content()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="render_vue_template_grid()">
|
<%def name="render_vue_template_grid()">
|
||||||
${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())}
|
${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
${parent.modify_vue_vars()}
|
${parent.modify_vue_vars()}
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
ThisPageData.csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}
|
ThisPageData.csrftoken = ${json.dumps(h.get_csrf_token(request))|n}
|
||||||
|
|
||||||
% if can_edit_help:
|
% if can_edit_help:
|
||||||
ThisPage.props.configureFieldsHelp = Boolean
|
ThisPage.props.configureFieldsHelp = Boolean
|
||||||
|
|
|
@ -41,7 +41,9 @@ from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
from wuttaweb.util import (get_form_data as wutta_get_form_data,
|
from wuttaweb.util import (get_form_data as wutta_get_form_data,
|
||||||
get_libver as wutta_get_libver,
|
get_libver as wutta_get_libver,
|
||||||
get_liburl as wutta_get_liburl)
|
get_liburl as wutta_get_liburl,
|
||||||
|
get_csrf_token as wutta_get_csrf_token,
|
||||||
|
render_csrf_token)
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -59,22 +61,19 @@ class SortColumn(object):
|
||||||
|
|
||||||
|
|
||||||
def get_csrf_token(request):
|
def get_csrf_token(request):
|
||||||
"""
|
""" """
|
||||||
Convenience function to retrieve the effective CSRF token for the given
|
warnings.warn("tailbone.util.get_csrf_token() is deprecated; "
|
||||||
request.
|
"please use wuttaweb.util.get_csrf_token() instead",
|
||||||
"""
|
DeprecationWarning, stacklevel=2)
|
||||||
token = request.session.get_csrf_token()
|
return wutta_get_csrf_token(request)
|
||||||
if token is None:
|
|
||||||
token = request.session.new_csrf_token()
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
def csrf_token(request, name='_csrf'):
|
def csrf_token(request, name='_csrf'):
|
||||||
"""
|
""" """
|
||||||
Convenience function. Returns CSRF hidden tag inside hidden DIV.
|
warnings.warn("tailbone.util.csrf_token() is deprecated; "
|
||||||
"""
|
"please use wuttaweb.util.render_csrf_token() instead",
|
||||||
token = get_csrf_token(request)
|
DeprecationWarning, stacklevel=2)
|
||||||
return HTML.tag("div", tags.hidden(name, value=token), style="display:none;")
|
return render_csrf_token(request, name=name)
|
||||||
|
|
||||||
|
|
||||||
def get_form_data(request):
|
def get_form_data(request):
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
Auth Views
|
Auth Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rattail.db.auth import set_user_password
|
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
from pyramid.httpexceptions import HTTPForbidden
|
from pyramid.httpexceptions import HTTPForbidden
|
||||||
|
@ -46,28 +44,6 @@ class UserLogin(colander.MappingSchema):
|
||||||
widget=dfwidget.PasswordWidget())
|
widget=dfwidget.PasswordWidget())
|
||||||
|
|
||||||
|
|
||||||
@colander.deferred
|
|
||||||
def current_password_correct(node, kw):
|
|
||||||
request = kw['request']
|
|
||||||
app = request.rattail_config.get_app()
|
|
||||||
auth = app.get_auth_handler()
|
|
||||||
user = kw['user']
|
|
||||||
def validate(node, value):
|
|
||||||
if not auth.authenticate_user(Session(), user.username, value):
|
|
||||||
raise colander.Invalid(node, "The password is incorrect")
|
|
||||||
return validate
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePassword(colander.MappingSchema):
|
|
||||||
|
|
||||||
current_password = colander.SchemaNode(colander.String(),
|
|
||||||
widget=dfwidget.PasswordWidget(),
|
|
||||||
validator=current_password_correct)
|
|
||||||
|
|
||||||
new_password = colander.SchemaNode(colander.String(),
|
|
||||||
widget=dfwidget.CheckedPasswordWidget())
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationView(View):
|
class AuthenticationView(View):
|
||||||
|
|
||||||
def forbidden(self):
|
def forbidden(self):
|
||||||
|
@ -104,6 +80,7 @@ class AuthenticationView(View):
|
||||||
form.save_label = "Login"
|
form.save_label = "Login"
|
||||||
form.show_reset = True
|
form.show_reset = True
|
||||||
form.show_cancel = False
|
form.show_cancel = False
|
||||||
|
form.button_icon_submit = 'user'
|
||||||
if form.validate():
|
if form.validate():
|
||||||
user = self.authenticate_user(form.validated['username'],
|
user = self.authenticate_user(form.validated['username'],
|
||||||
form.validated['password'])
|
form.validated['password'])
|
||||||
|
@ -117,10 +94,6 @@ class AuthenticationView(View):
|
||||||
else:
|
else:
|
||||||
self.request.session.flash("Invalid username or password", 'error')
|
self.request.session.flash("Invalid username or password", 'error')
|
||||||
|
|
||||||
image_url = self.rattail_config.get(
|
|
||||||
'tailbone', 'main_image_url',
|
|
||||||
default=self.request.static_url('tailbone:static/img/home_logo.png'))
|
|
||||||
|
|
||||||
# nb. hacky..but necessary, to add the refs, for autofocus
|
# nb. hacky..but necessary, to add the refs, for autofocus
|
||||||
# (also add key handler, so ENTER acts like TAB)
|
# (also add key handler, so ENTER acts like TAB)
|
||||||
dform = form.make_deform_form()
|
dform = form.make_deform_form()
|
||||||
|
@ -133,7 +106,6 @@ class AuthenticationView(View):
|
||||||
return {
|
return {
|
||||||
'form': form,
|
'form': form,
|
||||||
'referrer': referrer,
|
'referrer': referrer,
|
||||||
'image_url': image_url,
|
|
||||||
'index_title': app.get_node_title(),
|
'index_title': app.get_node_title(),
|
||||||
'help_url': global_help_url(self.rattail_config),
|
'help_url': global_help_url(self.rattail_config),
|
||||||
}
|
}
|
||||||
|
@ -182,10 +154,27 @@ class AuthenticationView(View):
|
||||||
self.request.user))
|
self.request.user))
|
||||||
return self.redirect(self.request.get_referrer())
|
return self.redirect(self.request.get_referrer())
|
||||||
|
|
||||||
schema = ChangePassword().bind(user=self.request.user, request=self.request)
|
def check_user_password(node, value):
|
||||||
|
auth = self.app.get_auth_handler()
|
||||||
|
user = self.request.user
|
||||||
|
if not auth.check_user_password(user, value):
|
||||||
|
node.raise_invalid("The password is incorrect")
|
||||||
|
|
||||||
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
schema.add(colander.SchemaNode(colander.String(),
|
||||||
|
name='current_password',
|
||||||
|
widget=dfwidget.PasswordWidget(),
|
||||||
|
validator=check_user_password))
|
||||||
|
|
||||||
|
schema.add(colander.SchemaNode(colander.String(),
|
||||||
|
name='new_password',
|
||||||
|
widget=dfwidget.CheckedPasswordWidget()))
|
||||||
|
|
||||||
form = forms.Form(schema=schema, request=self.request)
|
form = forms.Form(schema=schema, request=self.request)
|
||||||
if form.validate():
|
if form.validate():
|
||||||
set_user_password(self.request.user, form.validated['new_password'])
|
auth = self.app.get_auth_handler()
|
||||||
|
auth.set_user_password(self.request.user, form.validated['new_password'])
|
||||||
self.request.session.flash("Your password has been changed.")
|
self.request.session.flash("Your password has been changed.")
|
||||||
return self.redirect(self.request.get_referrer())
|
return self.redirect(self.request.get_referrer())
|
||||||
|
|
||||||
|
|
|
@ -46,10 +46,11 @@ import colander
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
|
from wuttaweb.util import render_csrf_token
|
||||||
|
|
||||||
from tailbone import forms, grids
|
from tailbone import forms, grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView
|
||||||
from tailbone.util import csrf_token
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -383,7 +384,7 @@ class BatchMasterView(MasterView):
|
||||||
f.set_label('executed_by', "Executed by")
|
f.set_label('executed_by', "Executed by")
|
||||||
|
|
||||||
# notes
|
# notes
|
||||||
f.set_type('notes', 'text')
|
f.set_type('notes', 'text_wrapped')
|
||||||
|
|
||||||
# if self.creating and self.request.user:
|
# if self.creating and self.request.user:
|
||||||
# batch = fs.model
|
# batch = fs.model
|
||||||
|
@ -441,7 +442,7 @@ class BatchMasterView(MasterView):
|
||||||
|
|
||||||
form = [
|
form = [
|
||||||
begin_form,
|
begin_form,
|
||||||
csrf_token(self.request),
|
render_csrf_token(self.request),
|
||||||
tags.hidden('complete', value=value),
|
tags.hidden('complete', value=value),
|
||||||
submit,
|
submit,
|
||||||
tags.end_form(),
|
tags.end_form(),
|
||||||
|
@ -861,7 +862,7 @@ class BatchMasterView(MasterView):
|
||||||
if not schema:
|
if not schema:
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
|
||||||
kwargs['component'] = 'execute-form'
|
kwargs['vue_tagname'] = 'execute-form'
|
||||||
form = forms.Form(schema=schema, request=self.request, defaults=defaults, **kwargs)
|
form = forms.Form(schema=schema, request=self.request, defaults=defaults, **kwargs)
|
||||||
self.configure_execute_form(form)
|
self.configure_execute_form(form)
|
||||||
return form
|
return form
|
||||||
|
|
|
@ -25,6 +25,7 @@ Various common views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import warnings
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from rattail.batch import consume_batch_id
|
from rattail.batch import consume_batch_id
|
||||||
|
@ -50,13 +51,31 @@ class CommonView(View):
|
||||||
Home page view.
|
Home page view.
|
||||||
"""
|
"""
|
||||||
app = self.get_rattail_app()
|
app = self.get_rattail_app()
|
||||||
if not self.request.user:
|
|
||||||
if self.rattail_config.getbool('tailbone', 'login_is_home', default=True):
|
|
||||||
raise self.redirect(self.request.route_url('login'))
|
|
||||||
|
|
||||||
image_url = self.rattail_config.get(
|
# maybe auto-redirect anons to login
|
||||||
'tailbone', 'main_image_url',
|
if not self.request.user:
|
||||||
default=self.request.static_url('tailbone:static/img/home_logo.png'))
|
redirect = self.config.get_bool('wuttaweb.home_redirect_to_login')
|
||||||
|
if redirect is None:
|
||||||
|
redirect = self.config.get_bool('tailbone.login_is_home')
|
||||||
|
if redirect is not None:
|
||||||
|
warnings.warn("tailbone.login_is_home setting is deprecated; "
|
||||||
|
"please set wuttaweb.home_redirect_to_login instead",
|
||||||
|
DeprecationWarning)
|
||||||
|
else:
|
||||||
|
# TODO: this is opposite of upstream default, should change
|
||||||
|
redirect = True
|
||||||
|
if redirect:
|
||||||
|
return self.redirect(self.request.route_url('login'))
|
||||||
|
|
||||||
|
image_url = self.config.get('wuttaweb.logo_url')
|
||||||
|
if not image_url:
|
||||||
|
image_url = self.config.get('tailbone.main_image_url')
|
||||||
|
if image_url:
|
||||||
|
warnings.warn("tailbone.main_image_url setting is deprecated; "
|
||||||
|
"please set wuttaweb.logo_url instead",
|
||||||
|
DeprecationWarning)
|
||||||
|
else:
|
||||||
|
image_url = self.request.static_url('tailbone:static/img/home_logo.png')
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'image_url': image_url,
|
'image_url': image_url,
|
||||||
|
|
|
@ -202,10 +202,36 @@ class DataSyncThreadView(MasterView):
|
||||||
return self.redirect(self.request.get_referrer(
|
return self.redirect(self.request.get_referrer(
|
||||||
default=self.request.route_url('datasyncchanges')))
|
default=self.request.route_url('datasyncchanges')))
|
||||||
|
|
||||||
def configure_get_context(self):
|
def configure_get_simple_settings(self):
|
||||||
|
""" """
|
||||||
|
return [
|
||||||
|
|
||||||
|
# basic
|
||||||
|
{'section': 'rattail.datasync',
|
||||||
|
'option': 'use_profile_settings',
|
||||||
|
'type': bool},
|
||||||
|
|
||||||
|
# misc.
|
||||||
|
{'section': 'rattail.datasync',
|
||||||
|
'option': 'supervisor_process_name'},
|
||||||
|
{'section': 'rattail.datasync',
|
||||||
|
'option': 'batch_size_limit',
|
||||||
|
'type': int},
|
||||||
|
|
||||||
|
# legacy
|
||||||
|
{'section': 'tailbone',
|
||||||
|
'option': 'datasync.restart'},
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
def configure_get_context(self, **kwargs):
|
||||||
|
""" """
|
||||||
|
context = super().configure_get_context(**kwargs)
|
||||||
|
|
||||||
profiles = self.datasync_handler.get_configured_profiles(
|
profiles = self.datasync_handler.get_configured_profiles(
|
||||||
include_disabled=True,
|
include_disabled=True,
|
||||||
ignore_problems=True)
|
ignore_problems=True)
|
||||||
|
context['profiles'] = profiles
|
||||||
|
|
||||||
profiles_data = []
|
profiles_data = []
|
||||||
for profile in sorted(profiles.values(), key=lambda p: p.key):
|
for profile in sorted(profiles.values(), key=lambda p: p.key):
|
||||||
|
@ -243,25 +269,15 @@ class DataSyncThreadView(MasterView):
|
||||||
data['consumers_data'] = consumers
|
data['consumers_data'] = consumers
|
||||||
profiles_data.append(data)
|
profiles_data.append(data)
|
||||||
|
|
||||||
return {
|
context['profiles_data'] = profiles_data
|
||||||
'profiles': profiles,
|
return context
|
||||||
'profiles_data': profiles_data,
|
|
||||||
'use_profile_settings': self.datasync_handler.should_use_profile_settings(),
|
|
||||||
'supervisor_process_name': self.rattail_config.get(
|
|
||||||
'rattail.datasync', 'supervisor_process_name'),
|
|
||||||
'restart_command': self.rattail_config.get(
|
|
||||||
'tailbone', 'datasync.restart'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def configure_gather_settings(self, data):
|
def configure_gather_settings(self, data, **kwargs):
|
||||||
settings = []
|
""" """
|
||||||
watch = []
|
settings = super().configure_gather_settings(data, **kwargs)
|
||||||
|
|
||||||
use_profile_settings = data.get('use_profile_settings') == 'true'
|
if data.get('rattail.datasync.use_profile_settings') == 'true':
|
||||||
settings.append({'name': 'rattail.datasync.use_profile_settings',
|
watch = []
|
||||||
'value': 'true' if use_profile_settings else 'false'})
|
|
||||||
|
|
||||||
if use_profile_settings:
|
|
||||||
|
|
||||||
for profile in json.loads(data['profiles']):
|
for profile in json.loads(data['profiles']):
|
||||||
pkey = profile['key']
|
pkey = profile['key']
|
||||||
|
@ -323,17 +339,12 @@ class DataSyncThreadView(MasterView):
|
||||||
settings.append({'name': 'rattail.datasync.watch',
|
settings.append({'name': 'rattail.datasync.watch',
|
||||||
'value': ', '.join(watch)})
|
'value': ', '.join(watch)})
|
||||||
|
|
||||||
if data['supervisor_process_name']:
|
|
||||||
settings.append({'name': 'rattail.datasync.supervisor_process_name',
|
|
||||||
'value': data['supervisor_process_name']})
|
|
||||||
|
|
||||||
if data['restart_command']:
|
|
||||||
settings.append({'name': 'tailbone.datasync.restart',
|
|
||||||
'value': data['restart_command']})
|
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
def configure_remove_settings(self):
|
def configure_remove_settings(self, **kwargs):
|
||||||
|
""" """
|
||||||
|
super().configure_remove_settings(**kwargs)
|
||||||
|
|
||||||
purge_datasync_settings(self.rattail_config, self.Session())
|
purge_datasync_settings(self.rattail_config, self.Session())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -116,11 +116,12 @@ class EmailSettingView(MasterView):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.sorters['key'] = g.make_simple_sorter('key', foldcase=True)
|
super().configure_grid(g)
|
||||||
g.sorters['prefix'] = g.make_simple_sorter('prefix', foldcase=True)
|
|
||||||
g.sorters['subject'] = g.make_simple_sorter('subject', foldcase=True)
|
g.sort_on_backend = False
|
||||||
g.sorters['enabled'] = g.make_simple_sorter('enabled')
|
g.sort_multiple = False
|
||||||
g.set_sort_defaults('key')
|
g.set_sort_defaults('key')
|
||||||
|
|
||||||
g.set_type('enabled', 'boolean')
|
g.set_type('enabled', 'boolean')
|
||||||
g.set_link('key')
|
g.set_link('key')
|
||||||
g.set_link('subject')
|
g.set_link('subject')
|
||||||
|
@ -130,11 +131,9 @@ class EmailSettingView(MasterView):
|
||||||
|
|
||||||
# to
|
# to
|
||||||
g.set_renderer('to', self.render_to_short)
|
g.set_renderer('to', self.render_to_short)
|
||||||
g.sorters['to'] = g.make_simple_sorter('to', foldcase=True)
|
|
||||||
|
|
||||||
# hidden
|
# hidden
|
||||||
if self.has_perm('configure'):
|
if self.has_perm('configure'):
|
||||||
g.sorters['hidden'] = g.make_simple_sorter('hidden')
|
|
||||||
g.set_type('hidden', 'boolean')
|
g.set_type('hidden', 'boolean')
|
||||||
else:
|
else:
|
||||||
g.remove('hidden')
|
g.remove('hidden')
|
||||||
|
|
|
@ -117,6 +117,7 @@ class MasterView(View):
|
||||||
supports_prev_next = False
|
supports_prev_next = False
|
||||||
supports_import_batch_from_file = False
|
supports_import_batch_from_file = False
|
||||||
has_input_file_templates = False
|
has_input_file_templates = False
|
||||||
|
has_output_file_templates = False
|
||||||
configurable = False
|
configurable = False
|
||||||
|
|
||||||
# set to True to add "View *global* Objects" permission, and
|
# set to True to add "View *global* Objects" permission, and
|
||||||
|
@ -334,7 +335,7 @@ class MasterView(View):
|
||||||
|
|
||||||
# If user just refreshed the page with a reset instruction, issue a
|
# If user just refreshed the page with a reset instruction, issue a
|
||||||
# redirect in order to clear out the query string.
|
# redirect in order to clear out the query string.
|
||||||
if self.request.GET.get('reset-to-default-filters') == 'true':
|
if self.request.GET.get('reset-view'):
|
||||||
kw = {'_query': None}
|
kw = {'_query': None}
|
||||||
hash_ = self.request.GET.get('hash')
|
hash_ = self.request.GET.get('hash')
|
||||||
if hash_:
|
if hash_:
|
||||||
|
@ -411,7 +412,7 @@ class MasterView(View):
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
kwargs.setdefault('paginated', False)
|
kwargs.setdefault('paginated', False)
|
||||||
grid = self.make_grid(session=session, **kwargs)
|
grid = self.make_grid(session=session, **kwargs)
|
||||||
return grid.make_visible_data()
|
return grid.get_visible_data()
|
||||||
|
|
||||||
def get_grid_columns(self):
|
def get_grid_columns(self):
|
||||||
"""
|
"""
|
||||||
|
@ -550,7 +551,8 @@ class MasterView(View):
|
||||||
def get_quickie_result_url(self, obj):
|
def get_quickie_result_url(self, obj):
|
||||||
return self.get_action_url('view', obj)
|
return self.get_action_url('view', obj)
|
||||||
|
|
||||||
def make_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
|
def make_row_grid(self, factory=None, key=None, data=None, columns=None,
|
||||||
|
session=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Make and return a new (configured) rows grid instance.
|
Make and return a new (configured) rows grid instance.
|
||||||
"""
|
"""
|
||||||
|
@ -611,7 +613,9 @@ class MasterView(View):
|
||||||
|
|
||||||
# delete action
|
# delete action
|
||||||
if self.rows_deletable and self.has_perm('delete_row'):
|
if self.rows_deletable and self.has_perm('delete_row'):
|
||||||
actions.append(self.make_action('delete', icon='trash', url=self.row_delete_action_url))
|
actions.append(self.make_action('delete', icon='trash',
|
||||||
|
url=self.row_delete_action_url,
|
||||||
|
link_class='has-text-danger'))
|
||||||
defaults['delete_speedbump'] = self.rows_deletable_speedbump
|
defaults['delete_speedbump'] = self.rows_deletable_speedbump
|
||||||
|
|
||||||
defaults['actions'] = actions
|
defaults['actions'] = actions
|
||||||
|
@ -899,7 +903,7 @@ class MasterView(View):
|
||||||
|
|
||||||
def valid_employee_uuid(self, node, value):
|
def valid_employee_uuid(self, node, value):
|
||||||
if value:
|
if value:
|
||||||
model = self.model
|
model = self.app.model
|
||||||
employee = self.Session.get(model.Employee, value)
|
employee = self.Session.get(model.Employee, value)
|
||||||
if not employee:
|
if not employee:
|
||||||
node.raise_invalid("Employee not found")
|
node.raise_invalid("Employee not found")
|
||||||
|
@ -935,7 +939,7 @@ class MasterView(View):
|
||||||
|
|
||||||
def valid_vendor_uuid(self, node, value):
|
def valid_vendor_uuid(self, node, value):
|
||||||
if value:
|
if value:
|
||||||
model = self.model
|
model = self.app.model
|
||||||
vendor = self.Session.get(model.Vendor, value)
|
vendor = self.Session.get(model.Vendor, value)
|
||||||
if not vendor:
|
if not vendor:
|
||||||
node.raise_invalid("Vendor not found")
|
node.raise_invalid("Vendor not found")
|
||||||
|
@ -1183,7 +1187,7 @@ class MasterView(View):
|
||||||
|
|
||||||
# If user just refreshed the page with a reset instruction, issue a
|
# If user just refreshed the page with a reset instruction, issue a
|
||||||
# redirect in order to clear out the query string.
|
# redirect in order to clear out the query string.
|
||||||
if self.request.GET.get('reset-to-default-filters') == 'true':
|
if self.request.GET.get('reset-view'):
|
||||||
kw = {'_query': None}
|
kw = {'_query': None}
|
||||||
hash_ = self.request.GET.get('hash')
|
hash_ = self.request.GET.get('hash')
|
||||||
if hash_:
|
if hash_:
|
||||||
|
@ -1378,7 +1382,7 @@ class MasterView(View):
|
||||||
return classes
|
return classes
|
||||||
|
|
||||||
def make_revisions_grid(self, obj, empty_data=False):
|
def make_revisions_grid(self, obj, empty_data=False):
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
row_url = lambda txn, i: self.request.route_url(f'{route_prefix}.version',
|
row_url = lambda txn, i: self.request.route_url(f'{route_prefix}.version',
|
||||||
uuid=obj.uuid,
|
uuid=obj.uuid,
|
||||||
|
@ -1706,7 +1710,7 @@ class MasterView(View):
|
||||||
kwargs.setdefault('paginated', False)
|
kwargs.setdefault('paginated', False)
|
||||||
kwargs.setdefault('sortable', sort)
|
kwargs.setdefault('sortable', sort)
|
||||||
grid = self.make_row_grid(session=session, **kwargs)
|
grid = self.make_row_grid(session=session, **kwargs)
|
||||||
return grid.make_visible_data()
|
return grid.get_visible_data()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_row_url_prefix(cls):
|
def get_row_url_prefix(cls):
|
||||||
|
@ -1820,6 +1824,26 @@ class MasterView(View):
|
||||||
path = os.path.join(basedir, filespec)
|
path = os.path.join(basedir, filespec)
|
||||||
return self.file_response(path)
|
return self.file_response(path)
|
||||||
|
|
||||||
|
def download_output_file_template(self):
|
||||||
|
"""
|
||||||
|
View for downloading an output file template.
|
||||||
|
"""
|
||||||
|
key = self.request.GET['key']
|
||||||
|
filespec = self.request.GET['file']
|
||||||
|
|
||||||
|
matches = [tmpl for tmpl in self.get_output_file_templates()
|
||||||
|
if tmpl['key'] == key]
|
||||||
|
if not matches:
|
||||||
|
raise self.notfound()
|
||||||
|
|
||||||
|
template = matches[0]
|
||||||
|
templatesdir = os.path.join(self.rattail_config.datadir(),
|
||||||
|
'templates', 'output_files',
|
||||||
|
self.get_route_prefix())
|
||||||
|
basedir = os.path.join(templatesdir, template['key'])
|
||||||
|
path = os.path.join(basedir, filespec)
|
||||||
|
return self.file_response(path)
|
||||||
|
|
||||||
def edit(self):
|
def edit(self):
|
||||||
"""
|
"""
|
||||||
View for editing an existing model record.
|
View for editing an existing model record.
|
||||||
|
@ -2129,7 +2153,7 @@ class MasterView(View):
|
||||||
Thread target for executing an object.
|
Thread target for executing an object.
|
||||||
"""
|
"""
|
||||||
app = self.get_rattail_app()
|
app = self.get_rattail_app()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
session = app.make_session()
|
session = app.make_session()
|
||||||
obj = self.get_instance_for_key(key, session)
|
obj = self.get_instance_for_key(key, session)
|
||||||
user = session.get(model.User, user_uuid)
|
user = session.get(model.User, user_uuid)
|
||||||
|
@ -2570,7 +2594,7 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
info = session.query(model.TailbonePageHelp)\
|
info = session.query(model.TailbonePageHelp)\
|
||||||
|
@ -2593,7 +2617,7 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
info = session.query(model.TailbonePageHelp)\
|
info = session.query(model.TailbonePageHelp)\
|
||||||
|
@ -2615,7 +2639,7 @@ class MasterView(View):
|
||||||
|
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
@ -2649,7 +2673,7 @@ class MasterView(View):
|
||||||
|
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
@ -2848,6 +2872,12 @@ class MasterView(View):
|
||||||
kwargs['input_file_templates'] = OrderedDict([(tmpl['key'], tmpl)
|
kwargs['input_file_templates'] = OrderedDict([(tmpl['key'], tmpl)
|
||||||
for tmpl in templates])
|
for tmpl in templates])
|
||||||
|
|
||||||
|
# add info for downloadable output file templates, if any
|
||||||
|
if self.has_output_file_templates:
|
||||||
|
templates = self.normalize_output_file_templates()
|
||||||
|
kwargs['output_file_templates'] = OrderedDict([(tmpl['key'], tmpl)
|
||||||
|
for tmpl in templates])
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_input_file_templates(self):
|
def get_input_file_templates(self):
|
||||||
|
@ -2922,6 +2952,81 @@ class MasterView(View):
|
||||||
|
|
||||||
return templates
|
return templates
|
||||||
|
|
||||||
|
def get_output_file_templates(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def normalize_output_file_templates(self, templates=None,
|
||||||
|
include_file_options=False):
|
||||||
|
if templates is None:
|
||||||
|
templates = self.get_output_file_templates()
|
||||||
|
|
||||||
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
|
if include_file_options:
|
||||||
|
templatesdir = os.path.join(self.rattail_config.datadir(),
|
||||||
|
'templates', 'output_files',
|
||||||
|
route_prefix)
|
||||||
|
|
||||||
|
for template in templates:
|
||||||
|
|
||||||
|
if 'config_section' not in template:
|
||||||
|
if hasattr(self, 'output_file_template_config_section'):
|
||||||
|
template['config_section'] = self.output_file_template_config_section
|
||||||
|
else:
|
||||||
|
template['config_section'] = route_prefix
|
||||||
|
section = template['config_section']
|
||||||
|
|
||||||
|
if 'config_prefix' not in template:
|
||||||
|
template['config_prefix'] = '{}.{}'.format(
|
||||||
|
self.output_file_template_config_prefix,
|
||||||
|
template['key'])
|
||||||
|
prefix = template['config_prefix']
|
||||||
|
|
||||||
|
for key in ('mode', 'file', 'url'):
|
||||||
|
|
||||||
|
if 'option_{}'.format(key) not in template:
|
||||||
|
template['option_{}'.format(key)] = '{}.{}'.format(prefix, key)
|
||||||
|
|
||||||
|
if 'setting_{}'.format(key) not in template:
|
||||||
|
template['setting_{}'.format(key)] = '{}.{}'.format(
|
||||||
|
section,
|
||||||
|
template['option_{}'.format(key)])
|
||||||
|
|
||||||
|
if key not in template:
|
||||||
|
value = self.rattail_config.get(
|
||||||
|
section,
|
||||||
|
template['option_{}'.format(key)])
|
||||||
|
if value is not None:
|
||||||
|
template[key] = value
|
||||||
|
|
||||||
|
template.setdefault('mode', 'default')
|
||||||
|
template.setdefault('file', None)
|
||||||
|
template.setdefault('url', template['default_url'])
|
||||||
|
|
||||||
|
if include_file_options:
|
||||||
|
options = []
|
||||||
|
basedir = os.path.join(templatesdir, template['key'])
|
||||||
|
if os.path.exists(basedir):
|
||||||
|
for name in sorted(os.listdir(basedir)):
|
||||||
|
if len(name) == 4 and name.isdigit():
|
||||||
|
files = os.listdir(os.path.join(basedir, name))
|
||||||
|
if len(files) == 1:
|
||||||
|
options.append(os.path.join(name, files[0]))
|
||||||
|
template['file_options'] = options
|
||||||
|
template['file_options_dir'] = basedir
|
||||||
|
|
||||||
|
if template['mode'] == 'external':
|
||||||
|
template['effective_url'] = template['url']
|
||||||
|
elif template['mode'] == 'hosted':
|
||||||
|
template['effective_url'] = self.request.route_url(
|
||||||
|
'{}.download_output_file_template'.format(route_prefix),
|
||||||
|
_query={'key': template['key'],
|
||||||
|
'file': template['file']})
|
||||||
|
else:
|
||||||
|
template['effective_url'] = template['default_url']
|
||||||
|
|
||||||
|
return templates
|
||||||
|
|
||||||
def template_kwargs_index(self, **kwargs):
|
def template_kwargs_index(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Method stub, so subclass can always invoke super() for it.
|
Method stub, so subclass can always invoke super() for it.
|
||||||
|
@ -2969,6 +3074,12 @@ class MasterView(View):
|
||||||
items.append(tags.link_to(f"Download {template['label']} Template",
|
items.append(tags.link_to(f"Download {template['label']} Template",
|
||||||
template['effective_url']))
|
template['effective_url']))
|
||||||
|
|
||||||
|
if self.has_output_file_templates and self.has_perm('configure'):
|
||||||
|
templates = self.normalize_output_file_templates()
|
||||||
|
for template in templates:
|
||||||
|
items.append(tags.link_to(f"Download {template['label']} Template",
|
||||||
|
template['effective_url']))
|
||||||
|
|
||||||
# if self.viewing:
|
# if self.viewing:
|
||||||
|
|
||||||
# # # TODO: either make this configurable, or just lose it.
|
# # # TODO: either make this configurable, or just lose it.
|
||||||
|
@ -3214,7 +3325,7 @@ class MasterView(View):
|
||||||
url=self.default_clone_url)
|
url=self.default_clone_url)
|
||||||
|
|
||||||
def make_grid_action_delete(self):
|
def make_grid_action_delete(self):
|
||||||
kwargs = {}
|
kwargs = {'link_class': 'has-text-danger'}
|
||||||
if self.delete_confirm == 'simple':
|
if self.delete_confirm == 'simple':
|
||||||
kwargs['click_handler'] = 'deleteObject'
|
kwargs['click_handler'] = 'deleteObject'
|
||||||
return self.make_action('delete', icon='trash', url=self.default_delete_url, **kwargs)
|
return self.make_action('delete', icon='trash', url=self.default_delete_url, **kwargs)
|
||||||
|
@ -5204,6 +5315,39 @@ class MasterView(View):
|
||||||
data[template['setting_file']] = os.path.join(numdir,
|
data[template['setting_file']] = os.path.join(numdir,
|
||||||
info['filename'])
|
info['filename'])
|
||||||
|
|
||||||
|
if self.has_output_file_templates:
|
||||||
|
templatesdir = os.path.join(self.rattail_config.datadir(),
|
||||||
|
'templates', 'output_files',
|
||||||
|
self.get_route_prefix())
|
||||||
|
|
||||||
|
def get_next_filedir(basedir):
|
||||||
|
nextid = 1
|
||||||
|
while True:
|
||||||
|
path = os.path.join(basedir, '{:04d}'.format(nextid))
|
||||||
|
if not os.path.exists(path):
|
||||||
|
# this should fail if there happens to be a race
|
||||||
|
# condition and someone else got to this id first
|
||||||
|
os.mkdir(path)
|
||||||
|
return path
|
||||||
|
nextid += 1
|
||||||
|
|
||||||
|
for template in self.normalize_output_file_templates():
|
||||||
|
key = '{}.upload'.format(template['setting_file'])
|
||||||
|
if key in uploads:
|
||||||
|
assert self.request.POST[template['setting_mode']] == 'hosted'
|
||||||
|
assert not self.request.POST[template['setting_file']]
|
||||||
|
info = uploads[key]
|
||||||
|
basedir = os.path.join(templatesdir, template['key'])
|
||||||
|
if not os.path.exists(basedir):
|
||||||
|
os.makedirs(basedir)
|
||||||
|
filedir = get_next_filedir(basedir)
|
||||||
|
filepath = os.path.join(filedir, info['filename'])
|
||||||
|
shutil.copyfile(info['filepath'], filepath)
|
||||||
|
shutil.rmtree(info['filedir'])
|
||||||
|
numdir = os.path.basename(filedir)
|
||||||
|
data[template['setting_file']] = os.path.join(numdir,
|
||||||
|
info['filename'])
|
||||||
|
|
||||||
def configure_get_simple_settings(self):
|
def configure_get_simple_settings(self):
|
||||||
"""
|
"""
|
||||||
If you have some "simple" settings, each of which basically
|
If you have some "simple" settings, each of which basically
|
||||||
|
@ -5248,7 +5392,8 @@ class MasterView(View):
|
||||||
simple['option'])
|
simple['option'])
|
||||||
|
|
||||||
def configure_get_context(self, simple_settings=None,
|
def configure_get_context(self, simple_settings=None,
|
||||||
input_file_templates=True):
|
input_file_templates=True,
|
||||||
|
output_file_templates=True):
|
||||||
"""
|
"""
|
||||||
Returns the full context dict, for rendering the configure
|
Returns the full context dict, for rendering the configure
|
||||||
page template.
|
page template.
|
||||||
|
@ -5297,7 +5442,7 @@ class MasterView(View):
|
||||||
for template in self.normalize_input_file_templates(
|
for template in self.normalize_input_file_templates(
|
||||||
include_file_options=True):
|
include_file_options=True):
|
||||||
settings[template['setting_mode']] = template['mode']
|
settings[template['setting_mode']] = template['mode']
|
||||||
settings[template['setting_file']] = template['file']
|
settings[template['setting_file']] = template['file'] or ''
|
||||||
settings[template['setting_url']] = template['url']
|
settings[template['setting_url']] = template['url']
|
||||||
file_options[template['key']] = template['file_options']
|
file_options[template['key']] = template['file_options']
|
||||||
file_option_dirs[template['key']] = template['file_options_dir']
|
file_option_dirs[template['key']] = template['file_options_dir']
|
||||||
|
@ -5305,10 +5450,27 @@ class MasterView(View):
|
||||||
context['input_file_options'] = file_options
|
context['input_file_options'] = file_options
|
||||||
context['input_file_option_dirs'] = file_option_dirs
|
context['input_file_option_dirs'] = file_option_dirs
|
||||||
|
|
||||||
|
# add settings for output file templates, if any
|
||||||
|
if output_file_templates and self.has_output_file_templates:
|
||||||
|
settings = {}
|
||||||
|
file_options = {}
|
||||||
|
file_option_dirs = {}
|
||||||
|
for template in self.normalize_output_file_templates(
|
||||||
|
include_file_options=True):
|
||||||
|
settings[template['setting_mode']] = template['mode']
|
||||||
|
settings[template['setting_file']] = template['file'] or ''
|
||||||
|
settings[template['setting_url']] = template['url']
|
||||||
|
file_options[template['key']] = template['file_options']
|
||||||
|
file_option_dirs[template['key']] = template['file_options_dir']
|
||||||
|
context['output_file_template_settings'] = settings
|
||||||
|
context['output_file_options'] = file_options
|
||||||
|
context['output_file_option_dirs'] = file_option_dirs
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def configure_gather_settings(self, data, simple_settings=None,
|
def configure_gather_settings(self, data, simple_settings=None,
|
||||||
input_file_templates=True):
|
input_file_templates=True,
|
||||||
|
output_file_templates=True):
|
||||||
settings = []
|
settings = []
|
||||||
|
|
||||||
# maybe collect "simple" settings
|
# maybe collect "simple" settings
|
||||||
|
@ -5354,12 +5516,32 @@ class MasterView(View):
|
||||||
settings.append({'name': template['setting_url'],
|
settings.append({'name': template['setting_url'],
|
||||||
'value': data.get(template['setting_url'])})
|
'value': data.get(template['setting_url'])})
|
||||||
|
|
||||||
|
# maybe also collect output file template settings
|
||||||
|
if output_file_templates and self.has_output_file_templates:
|
||||||
|
for template in self.normalize_output_file_templates():
|
||||||
|
|
||||||
|
# mode
|
||||||
|
settings.append({'name': template['setting_mode'],
|
||||||
|
'value': data.get(template['setting_mode'])})
|
||||||
|
|
||||||
|
# file
|
||||||
|
value = data.get(template['setting_file'])
|
||||||
|
if value:
|
||||||
|
# nb. avoid saving if empty, so can remain "null"
|
||||||
|
settings.append({'name': template['setting_file'],
|
||||||
|
'value': value})
|
||||||
|
|
||||||
|
# url
|
||||||
|
settings.append({'name': template['setting_url'],
|
||||||
|
'value': data.get(template['setting_url'])})
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
def configure_remove_settings(self, simple_settings=None,
|
def configure_remove_settings(self, simple_settings=None,
|
||||||
input_file_templates=True):
|
input_file_templates=True,
|
||||||
|
output_file_templates=True):
|
||||||
app = self.get_rattail_app()
|
app = self.get_rattail_app()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
names = []
|
names = []
|
||||||
|
|
||||||
if simple_settings is None:
|
if simple_settings is None:
|
||||||
|
@ -5376,6 +5558,14 @@ class MasterView(View):
|
||||||
template['setting_url'],
|
template['setting_url'],
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if output_file_templates and self.has_output_file_templates:
|
||||||
|
for template in self.normalize_output_file_templates():
|
||||||
|
names.extend([
|
||||||
|
template['setting_mode'],
|
||||||
|
template['setting_file'],
|
||||||
|
template['setting_url'],
|
||||||
|
])
|
||||||
|
|
||||||
if names:
|
if names:
|
||||||
# nb. using thread-local session here; we do not use
|
# nb. using thread-local session here; we do not use
|
||||||
# self.Session b/c it may not point to Rattail
|
# self.Session b/c it may not point to Rattail
|
||||||
|
@ -5638,6 +5828,15 @@ class MasterView(View):
|
||||||
route_name='{}.download_input_file_template'.format(route_prefix),
|
route_name='{}.download_input_file_template'.format(route_prefix),
|
||||||
permission='{}.create'.format(permission_prefix))
|
permission='{}.create'.format(permission_prefix))
|
||||||
|
|
||||||
|
# download output file template
|
||||||
|
if cls.has_output_file_templates and cls.configurable:
|
||||||
|
config.add_route(f'{route_prefix}.download_output_file_template',
|
||||||
|
f'{url_prefix}/download-output-file-template')
|
||||||
|
config.add_view(cls, attr='download_output_file_template',
|
||||||
|
route_name=f'{route_prefix}.download_output_file_template',
|
||||||
|
# TODO: this is different from input file, should change?
|
||||||
|
permission=f'{permission_prefix}.configure')
|
||||||
|
|
||||||
# view
|
# view
|
||||||
if cls.viewable:
|
if cls.viewable:
|
||||||
cls._defaults_view(config)
|
cls._defaults_view(config)
|
||||||
|
@ -5901,7 +6100,7 @@ class MasterView(View):
|
||||||
renderer='json')
|
renderer='json')
|
||||||
|
|
||||||
|
|
||||||
class ViewSupplement(object):
|
class ViewSupplement:
|
||||||
"""
|
"""
|
||||||
Base class for view "supplements" - which are sort of like plugins
|
Base class for view "supplements" - which are sort of like plugins
|
||||||
which can "supplement" certain aspects of the view.
|
which can "supplement" certain aspects of the view.
|
||||||
|
@ -5928,6 +6127,7 @@ class ViewSupplement(object):
|
||||||
def __init__(self, master):
|
def __init__(self, master):
|
||||||
self.master = master
|
self.master = master
|
||||||
self.request = master.request
|
self.request = master.request
|
||||||
|
self.app = master.app
|
||||||
self.model = master.model
|
self.model = master.model
|
||||||
self.rattail_config = master.rattail_config
|
self.rattail_config = master.rattail_config
|
||||||
self.Session = master.Session
|
self.Session = master.Session
|
||||||
|
@ -5961,7 +6161,7 @@ class ViewSupplement(object):
|
||||||
This is accomplished by subjecting the current base query to a
|
This is accomplished by subjecting the current base query to a
|
||||||
join, e.g. something like::
|
join, e.g. something like::
|
||||||
|
|
||||||
model = self.model
|
model = self.app.model
|
||||||
query = query.outerjoin(model.MyExtension)
|
query = query.outerjoin(model.MyExtension)
|
||||||
return query
|
return query
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -564,15 +564,19 @@ class PersonView(MasterView):
|
||||||
Method which must return the base query for the profile's POS
|
Method which must return the base query for the profile's POS
|
||||||
Transactions grid data.
|
Transactions grid data.
|
||||||
"""
|
"""
|
||||||
app = self.get_rattail_app()
|
customer = self.app.get_customer(person)
|
||||||
customer = app.get_customer(person)
|
|
||||||
|
|
||||||
key_field = app.get_customer_key_field()
|
if customer:
|
||||||
customer_key = getattr(customer, key_field)
|
key_field = self.app.get_customer_key_field()
|
||||||
if customer_key is not None:
|
customer_key = getattr(customer, key_field)
|
||||||
customer_key = str(customer_key)
|
if customer_key is not None:
|
||||||
|
customer_key = str(customer_key)
|
||||||
|
else:
|
||||||
|
# nb. this should *not* match anything, so query returns
|
||||||
|
# no results..
|
||||||
|
customer_key = person.uuid
|
||||||
|
|
||||||
trainwreck = app.get_trainwreck_handler()
|
trainwreck = self.app.get_trainwreck_handler()
|
||||||
model = trainwreck.get_model()
|
model = trainwreck.get_model()
|
||||||
query = TrainwreckSession.query(model.Transaction)\
|
query = TrainwreckSession.query(model.Transaction)\
|
||||||
.filter(model.Transaction.customer_id == customer_key)
|
.filter(model.Transaction.customer_id == customer_key)
|
||||||
|
@ -1382,8 +1386,8 @@ class PersonView(MasterView):
|
||||||
}
|
}
|
||||||
|
|
||||||
if not context['users']:
|
if not context['users']:
|
||||||
context['suggested_username'] = auth.generate_unique_username(self.Session(),
|
context['suggested_username'] = auth.make_unique_username(self.Session(),
|
||||||
person=person)
|
person=person)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ import sqlalchemy_continuum as continuum
|
||||||
|
|
||||||
from rattail import enum, pod, sil
|
from rattail import enum, pod, sil
|
||||||
from rattail.db import api, auth, Session as RattailSession
|
from rattail.db import api, auth, Session as RattailSession
|
||||||
from rattail.db.model import Product, PendingProduct, CustomerOrderItem
|
from rattail.db.model import Product, PendingProduct, ProductCost, CustomerOrderItem
|
||||||
from rattail.gpc import GPC
|
from rattail.gpc import GPC
|
||||||
from rattail.threads import Thread
|
from rattail.threads import Thread
|
||||||
from rattail.exceptions import LabelPrintingError
|
from rattail.exceptions import LabelPrintingError
|
||||||
|
@ -1857,7 +1857,8 @@ class ProductView(MasterView):
|
||||||
lookup_fields.append('alt_code')
|
lookup_fields.append('alt_code')
|
||||||
if lookup_fields:
|
if lookup_fields:
|
||||||
product = self.products_handler.locate_product_for_entry(
|
product = self.products_handler.locate_product_for_entry(
|
||||||
session, term, lookup_fields=lookup_fields)
|
session, term, lookup_fields=lookup_fields,
|
||||||
|
first_if_multiple=True)
|
||||||
if product:
|
if product:
|
||||||
final_results.append(self.search_normalize_result(product))
|
final_results.append(self.search_normalize_result(product))
|
||||||
|
|
||||||
|
@ -2668,6 +2669,78 @@ class PendingProductView(MasterView):
|
||||||
permission=f'{permission_prefix}.ignore_product')
|
permission=f'{permission_prefix}.ignore_product')
|
||||||
|
|
||||||
|
|
||||||
|
class ProductCostView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for Product Costs
|
||||||
|
"""
|
||||||
|
model_class = ProductCost
|
||||||
|
route_prefix = 'product_costs'
|
||||||
|
url_prefix = '/products/costs'
|
||||||
|
has_versions = True
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'_product_key_',
|
||||||
|
'vendor',
|
||||||
|
'preference',
|
||||||
|
'code',
|
||||||
|
'case_size',
|
||||||
|
'case_cost',
|
||||||
|
'pack_size',
|
||||||
|
'pack_cost',
|
||||||
|
'unit_cost',
|
||||||
|
]
|
||||||
|
|
||||||
|
def query(self, session):
|
||||||
|
""" """
|
||||||
|
query = super().query(session)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# always join on Product
|
||||||
|
return query.join(model.Product)
|
||||||
|
|
||||||
|
def configure_grid(self, g):
|
||||||
|
""" """
|
||||||
|
super().configure_grid(g)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# product key
|
||||||
|
field = self.get_product_key_field()
|
||||||
|
g.set_renderer(field, self.render_product_key)
|
||||||
|
g.set_sorter(field, getattr(model.Product, field))
|
||||||
|
g.set_sort_defaults(field)
|
||||||
|
g.set_filter(field, getattr(model.Product, field))
|
||||||
|
|
||||||
|
# vendor
|
||||||
|
g.set_joiner('vendor', lambda q: q.join(model.Vendor))
|
||||||
|
g.set_sorter('vendor', model.Vendor.name)
|
||||||
|
g.set_filter('vendor', model.Vendor.name, label="Vendor Name")
|
||||||
|
|
||||||
|
def render_product_key(self, cost, field):
|
||||||
|
""" """
|
||||||
|
handler = self.app.get_products_handler()
|
||||||
|
return handler.render_product_key(cost.product)
|
||||||
|
|
||||||
|
def configure_form(self, f):
|
||||||
|
""" """
|
||||||
|
super().configure_form(f)
|
||||||
|
|
||||||
|
# product
|
||||||
|
f.set_renderer('product', self.render_product)
|
||||||
|
if 'product_uuid' in f and 'product' in f:
|
||||||
|
f.remove('product')
|
||||||
|
f.replace('product_uuid', 'product')
|
||||||
|
|
||||||
|
# vendor
|
||||||
|
f.set_renderer('vendor', self.render_vendor)
|
||||||
|
if 'vendor_uuid' in f and 'vendor' in f:
|
||||||
|
f.remove('vendor')
|
||||||
|
f.replace('vendor_uuid', 'vendor')
|
||||||
|
|
||||||
|
# futures
|
||||||
|
# TODO: should eventually show a subgrid here?
|
||||||
|
f.remove('futures')
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
|
@ -2677,6 +2750,9 @@ def defaults(config, **kwargs):
|
||||||
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
|
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
|
||||||
PendingProductView.defaults(config)
|
PendingProductView.defaults(config)
|
||||||
|
|
||||||
|
ProductCostView = kwargs.get('ProductCostView', base['ProductCostView'])
|
||||||
|
ProductCostView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
Base class for purchasing batch views
|
Base class for purchasing batch views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
|
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
|
@ -67,6 +69,8 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
'store',
|
'store',
|
||||||
'buyer',
|
'buyer',
|
||||||
'vendor',
|
'vendor',
|
||||||
|
'description',
|
||||||
|
'workflow',
|
||||||
'department',
|
'department',
|
||||||
'purchase',
|
'purchase',
|
||||||
'vendor_email',
|
'vendor_email',
|
||||||
|
@ -158,6 +162,174 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
def batch_mode(self):
|
def batch_mode(self):
|
||||||
raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
|
raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
|
||||||
|
|
||||||
|
def get_supported_workflows(self):
|
||||||
|
"""
|
||||||
|
Return the supported "create batch" workflows.
|
||||||
|
"""
|
||||||
|
enum = self.app.enum
|
||||||
|
if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING:
|
||||||
|
return self.batch_handler.supported_ordering_workflows()
|
||||||
|
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING:
|
||||||
|
return self.batch_handler.supported_receiving_workflows()
|
||||||
|
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_COSTING:
|
||||||
|
return self.batch_handler.supported_costing_workflows()
|
||||||
|
raise ValueError("unknown batch mode")
|
||||||
|
|
||||||
|
def allow_any_vendor(self):
|
||||||
|
"""
|
||||||
|
Return boolean indicating whether creating a batch for "any"
|
||||||
|
vendor is allowed, vs. only supported vendors.
|
||||||
|
"""
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
|
if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING:
|
||||||
|
return self.batch_handler.allow_ordering_any_vendor()
|
||||||
|
|
||||||
|
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING:
|
||||||
|
value = self.config.get_bool('rattail.batch.purchase.allow_receiving_any_vendor')
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
value = self.config.get_bool('rattail.batch.purchase.supported_vendors_only')
|
||||||
|
if value is not None:
|
||||||
|
warnings.warn("setting rattail.batch.purchase.supported_vendors_only is deprecated; "
|
||||||
|
"please use rattail.batch.purchase.allow_receiving_any_vendor instead",
|
||||||
|
DeprecationWarning)
|
||||||
|
# nb. must negate this setting
|
||||||
|
return not value
|
||||||
|
return False
|
||||||
|
|
||||||
|
raise ValueError("unknown batch mode")
|
||||||
|
|
||||||
|
def get_supported_vendors(self):
|
||||||
|
"""
|
||||||
|
Return the supported vendors for creating a batch.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def create(self, form=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Custom view for creating a new batch. We split the process
|
||||||
|
into two steps, 1) choose workflow and 2) create batch. This
|
||||||
|
is because the specific form details for creating a batch will
|
||||||
|
depend on which "type" of batch creation is to be done, and
|
||||||
|
it's much easier to keep conditional logic for that in the
|
||||||
|
server instead of client-side etc.
|
||||||
|
"""
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
|
workflows = self.get_supported_workflows()
|
||||||
|
valid_workflows = [workflow['workflow_key']
|
||||||
|
for workflow in workflows]
|
||||||
|
|
||||||
|
# if user has already identified their desired workflow, then
|
||||||
|
# we can just farm out to the default logic. we will of
|
||||||
|
# course configure our form differently, based on workflow,
|
||||||
|
# but this create() method at least will not need
|
||||||
|
# customization for that.
|
||||||
|
if self.request.matched_route.name.endswith('create_workflow'):
|
||||||
|
|
||||||
|
redirect = self.redirect(self.request.route_url(f'{route_prefix}.create'))
|
||||||
|
|
||||||
|
# however we do have one more thing to check - the workflow
|
||||||
|
# requested must of course be valid!
|
||||||
|
workflow_key = self.request.matchdict['workflow_key']
|
||||||
|
if workflow_key not in valid_workflows:
|
||||||
|
self.request.session.flash(f"Not a supported workflow: {workflow_key}", 'error')
|
||||||
|
raise redirect
|
||||||
|
|
||||||
|
# also, we require vendor to be correctly identified. if
|
||||||
|
# someone e.g. navigates to a URL by accident etc. we want
|
||||||
|
# to gracefully handle and redirect
|
||||||
|
uuid = self.request.matchdict['vendor_uuid']
|
||||||
|
vendor = self.Session.get(model.Vendor, uuid)
|
||||||
|
if not vendor:
|
||||||
|
self.request.session.flash("Invalid vendor selection. "
|
||||||
|
"Please choose an existing vendor.",
|
||||||
|
'warning')
|
||||||
|
raise redirect
|
||||||
|
|
||||||
|
# okay now do the normal thing, per workflow
|
||||||
|
return super().create(**kwargs)
|
||||||
|
|
||||||
|
# on the other hand, if caller provided a form, that means we are in
|
||||||
|
# the middle of some other custom workflow, e.g. "add child to truck
|
||||||
|
# dump parent" or some such. in which case we also defer to the normal
|
||||||
|
# logic, so as to not interfere with that.
|
||||||
|
if form:
|
||||||
|
return super().create(form=form, **kwargs)
|
||||||
|
|
||||||
|
# okay, at this point we need the user to select a vendor and workflow
|
||||||
|
self.creating = True
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
# form to accept user choice of vendor/workflow
|
||||||
|
schema = colander.Schema()
|
||||||
|
schema.add(colander.SchemaNode(colander.String(), name='vendor'))
|
||||||
|
schema.add(colander.SchemaNode(colander.String(), name='workflow',
|
||||||
|
validator=colander.OneOf(valid_workflows)))
|
||||||
|
factory = self.get_form_factory()
|
||||||
|
form = factory(schema=schema, request=self.request)
|
||||||
|
|
||||||
|
# configure vendor field
|
||||||
|
vendor_handler = self.app.get_vendor_handler()
|
||||||
|
if self.allow_any_vendor():
|
||||||
|
# user may choose *any* available vendor
|
||||||
|
use_dropdown = vendor_handler.choice_uses_dropdown()
|
||||||
|
if use_dropdown:
|
||||||
|
vendors = self.Session.query(model.Vendor)\
|
||||||
|
.order_by(model.Vendor.id)\
|
||||||
|
.all()
|
||||||
|
vendor_values = [(vendor.uuid, f"({vendor.id}) {vendor.name}")
|
||||||
|
for vendor in vendors]
|
||||||
|
form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values))
|
||||||
|
if len(vendors) == 1:
|
||||||
|
form.set_default('vendor', vendors[0].uuid)
|
||||||
|
else:
|
||||||
|
vendor_display = ""
|
||||||
|
if self.request.method == 'POST':
|
||||||
|
if self.request.POST.get('vendor'):
|
||||||
|
vendor = self.Session.get(model.Vendor, self.request.POST['vendor'])
|
||||||
|
if vendor:
|
||||||
|
vendor_display = str(vendor)
|
||||||
|
vendors_url = self.request.route_url('vendors.autocomplete')
|
||||||
|
form.set_widget('vendor', forms.widgets.JQueryAutocompleteWidget(
|
||||||
|
field_display=vendor_display, service_url=vendors_url))
|
||||||
|
else: # only "supported" vendors allowed
|
||||||
|
vendors = self.get_supported_vendors()
|
||||||
|
vendor_values = [(vendor.uuid, vendor_handler.render_vendor(vendor))
|
||||||
|
for vendor in vendors]
|
||||||
|
form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values))
|
||||||
|
form.set_validator('vendor', self.valid_vendor_uuid)
|
||||||
|
|
||||||
|
# configure workflow field
|
||||||
|
values = [(workflow['workflow_key'], workflow['display'])
|
||||||
|
for workflow in workflows]
|
||||||
|
form.set_widget('workflow',
|
||||||
|
dfwidget.SelectWidget(values=values))
|
||||||
|
if len(workflows) == 1:
|
||||||
|
form.set_default('workflow', workflows[0]['workflow_key'])
|
||||||
|
|
||||||
|
form.submit_label = "Continue"
|
||||||
|
form.cancel_url = self.get_index_url()
|
||||||
|
|
||||||
|
# if form validates, that means user has chosen a creation
|
||||||
|
# type, so we just redirect to the appropriate "new batch of
|
||||||
|
# type X" page
|
||||||
|
if form.validate():
|
||||||
|
workflow_key = form.validated['workflow']
|
||||||
|
vendor_uuid = form.validated['vendor']
|
||||||
|
url = self.request.route_url(f'{route_prefix}.create_workflow',
|
||||||
|
workflow_key=workflow_key,
|
||||||
|
vendor_uuid=vendor_uuid)
|
||||||
|
raise self.redirect(url)
|
||||||
|
|
||||||
|
context['form'] = form
|
||||||
|
if hasattr(form, 'make_deform_form'):
|
||||||
|
context['dform'] = form.make_deform_form()
|
||||||
|
return self.render_to_response('create', context)
|
||||||
|
|
||||||
def query(self, session):
|
def query(self, session):
|
||||||
model = self.model
|
model = self.model
|
||||||
return session.query(model.PurchaseBatch)\
|
return session.query(model.PurchaseBatch)\
|
||||||
|
@ -226,20 +398,40 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
model = self.model
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
|
today = self.app.today()
|
||||||
batch = f.model_instance
|
batch = f.model_instance
|
||||||
app = self.get_rattail_app()
|
workflow = self.request.matchdict.get('workflow_key')
|
||||||
today = app.localtime().date()
|
vendor_handler = self.app.get_vendor_handler()
|
||||||
|
|
||||||
# mode
|
# mode
|
||||||
f.set_enum('mode', self.enum.PURCHASE_BATCH_MODE)
|
f.set_enum('mode', enum.PURCHASE_BATCH_MODE)
|
||||||
|
|
||||||
|
# workflow
|
||||||
|
if self.creating:
|
||||||
|
if workflow:
|
||||||
|
f.set_widget('workflow', dfwidget.HiddenWidget())
|
||||||
|
f.set_default('workflow', workflow)
|
||||||
|
f.set_hidden('workflow')
|
||||||
|
# nb. show readonly '_workflow'
|
||||||
|
f.insert_after('workflow', '_workflow')
|
||||||
|
f.set_readonly('_workflow')
|
||||||
|
f.set_renderer('_workflow', self.render_workflow)
|
||||||
|
else:
|
||||||
|
f.set_readonly('workflow')
|
||||||
|
f.set_renderer('workflow', self.render_workflow)
|
||||||
|
else:
|
||||||
|
f.remove('workflow')
|
||||||
|
|
||||||
# store
|
# store
|
||||||
single_store = self.rattail_config.single_store()
|
single_store = self.config.single_store()
|
||||||
if self.creating:
|
if self.creating:
|
||||||
f.replace('store', 'store_uuid')
|
f.replace('store', 'store_uuid')
|
||||||
if single_store:
|
if single_store:
|
||||||
store = self.rattail_config.get_store(self.Session())
|
store = self.config.get_store(self.Session())
|
||||||
f.set_widget('store_uuid', dfwidget.HiddenWidget())
|
f.set_widget('store_uuid', dfwidget.HiddenWidget())
|
||||||
f.set_default('store_uuid', store.uuid)
|
f.set_default('store_uuid', store.uuid)
|
||||||
f.set_hidden('store_uuid')
|
f.set_hidden('store_uuid')
|
||||||
|
@ -263,7 +455,6 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
if self.creating:
|
if self.creating:
|
||||||
f.replace('vendor', 'vendor_uuid')
|
f.replace('vendor', 'vendor_uuid')
|
||||||
f.set_label('vendor_uuid', "Vendor")
|
f.set_label('vendor_uuid', "Vendor")
|
||||||
vendor_handler = app.get_vendor_handler()
|
|
||||||
use_dropdown = vendor_handler.choice_uses_dropdown()
|
use_dropdown = vendor_handler.choice_uses_dropdown()
|
||||||
if use_dropdown:
|
if use_dropdown:
|
||||||
vendors = self.Session.query(model.Vendor)\
|
vendors = self.Session.query(model.Vendor)\
|
||||||
|
@ -313,7 +504,7 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
if buyer:
|
if buyer:
|
||||||
buyer_display = str(buyer)
|
buyer_display = str(buyer)
|
||||||
elif self.creating:
|
elif self.creating:
|
||||||
buyer = app.get_employee(self.request.user)
|
buyer = self.app.get_employee(self.request.user)
|
||||||
if buyer:
|
if buyer:
|
||||||
buyer_display = str(buyer)
|
buyer_display = str(buyer)
|
||||||
f.set_default('buyer_uuid', buyer.uuid)
|
f.set_default('buyer_uuid', buyer.uuid)
|
||||||
|
@ -324,6 +515,30 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
field_display=buyer_display, service_url=buyers_url))
|
field_display=buyer_display, service_url=buyers_url))
|
||||||
f.set_label('buyer_uuid', "Buyer")
|
f.set_label('buyer_uuid', "Buyer")
|
||||||
|
|
||||||
|
# order_file
|
||||||
|
if self.creating:
|
||||||
|
f.set_type('order_file', 'file', required=False)
|
||||||
|
else:
|
||||||
|
f.set_readonly('order_file')
|
||||||
|
f.set_renderer('order_file', self.render_downloadable_file)
|
||||||
|
|
||||||
|
# order_parser_key
|
||||||
|
if self.creating:
|
||||||
|
kwargs = {}
|
||||||
|
if 'vendor_uuid' in self.request.matchdict:
|
||||||
|
vendor = self.Session.get(model.Vendor,
|
||||||
|
self.request.matchdict['vendor_uuid'])
|
||||||
|
if vendor:
|
||||||
|
kwargs['vendor'] = vendor
|
||||||
|
parsers = vendor_handler.get_supported_order_parsers(**kwargs)
|
||||||
|
parser_values = [(p.key, p.title) for p in parsers]
|
||||||
|
if len(parsers) == 1:
|
||||||
|
f.set_default('order_parser_key', parsers[0].key)
|
||||||
|
f.set_widget('order_parser_key', dfwidget.SelectWidget(values=parser_values))
|
||||||
|
f.set_label('order_parser_key', "Order Parser")
|
||||||
|
else:
|
||||||
|
f.remove_field('order_parser_key')
|
||||||
|
|
||||||
# invoice_file
|
# invoice_file
|
||||||
if self.creating:
|
if self.creating:
|
||||||
f.set_type('invoice_file', 'file', required=False)
|
f.set_type('invoice_file', 'file', required=False)
|
||||||
|
@ -341,7 +556,7 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
if vendor:
|
if vendor:
|
||||||
kwargs['vendor'] = vendor
|
kwargs['vendor'] = vendor
|
||||||
|
|
||||||
parsers = self.handler.get_supported_invoice_parsers(**kwargs)
|
parsers = self.batch_handler.get_supported_invoice_parsers(**kwargs)
|
||||||
parser_values = [(p.key, p.display) for p in parsers]
|
parser_values = [(p.key, p.display) for p in parsers]
|
||||||
if len(parsers) == 1:
|
if len(parsers) == 1:
|
||||||
f.set_default('invoice_parser_key', parsers[0].key)
|
f.set_default('invoice_parser_key', parsers[0].key)
|
||||||
|
@ -400,6 +615,35 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
'vendor_contact',
|
'vendor_contact',
|
||||||
'status_code')
|
'status_code')
|
||||||
|
|
||||||
|
# tweak some things if we are in "step 2" of creating new batch
|
||||||
|
if self.creating and workflow:
|
||||||
|
|
||||||
|
# display vendor but do not allow changing
|
||||||
|
vendor = self.Session.get(model.Vendor, self.request.matchdict['vendor_uuid'])
|
||||||
|
if not vendor:
|
||||||
|
raise ValueError(f"vendor not found: {self.request.matchdict['vendor_uuid']}")
|
||||||
|
f.set_readonly('vendor_uuid')
|
||||||
|
f.set_default('vendor_uuid', str(vendor))
|
||||||
|
|
||||||
|
# cancel should take us back to choosing a workflow
|
||||||
|
f.cancel_url = self.request.route_url(f'{route_prefix}.create')
|
||||||
|
|
||||||
|
def render_workflow(self, batch, field):
|
||||||
|
key = self.request.matchdict['workflow_key']
|
||||||
|
info = self.get_workflow_info(key)
|
||||||
|
if info:
|
||||||
|
return info['display']
|
||||||
|
|
||||||
|
def get_workflow_info(self, key):
|
||||||
|
enum = self.app.enum
|
||||||
|
if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING:
|
||||||
|
return self.batch_handler.ordering_workflow_info(key)
|
||||||
|
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING:
|
||||||
|
return self.batch_handler.receiving_workflow_info(key)
|
||||||
|
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_COSTING:
|
||||||
|
return self.batch_handler.costing_workflow_info(key)
|
||||||
|
raise ValueError("unknown batch mode")
|
||||||
|
|
||||||
def render_store(self, batch, field):
|
def render_store(self, batch, field):
|
||||||
store = batch.store
|
store = batch.store
|
||||||
if not store:
|
if not store:
|
||||||
|
@ -515,10 +759,12 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
|
|
||||||
def get_batch_kwargs(self, batch, **kwargs):
|
def get_batch_kwargs(self, batch, **kwargs):
|
||||||
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
||||||
model = self.model
|
model = self.app.model
|
||||||
|
|
||||||
kwargs['mode'] = self.batch_mode
|
kwargs['mode'] = self.batch_mode
|
||||||
|
kwargs['workflow'] = self.request.POST['workflow']
|
||||||
kwargs['truck_dump'] = batch.truck_dump
|
kwargs['truck_dump'] = batch.truck_dump
|
||||||
|
kwargs['order_parser_key'] = batch.order_parser_key
|
||||||
kwargs['invoice_parser_key'] = batch.invoice_parser_key
|
kwargs['invoice_parser_key'] = batch.invoice_parser_key
|
||||||
|
|
||||||
if batch.store:
|
if batch.store:
|
||||||
|
@ -536,6 +782,11 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
elif batch.vendor_uuid:
|
elif batch.vendor_uuid:
|
||||||
kwargs['vendor_uuid'] = batch.vendor_uuid
|
kwargs['vendor_uuid'] = batch.vendor_uuid
|
||||||
|
|
||||||
|
# must pull vendor from URL if it was not in form data
|
||||||
|
if 'vendor_uuid' not in kwargs and 'vendor' not in kwargs:
|
||||||
|
if 'vendor_uuid' in self.request.matchdict:
|
||||||
|
kwargs['vendor_uuid'] = self.request.matchdict['vendor_uuid']
|
||||||
|
|
||||||
if batch.department:
|
if batch.department:
|
||||||
kwargs['department'] = batch.department
|
kwargs['department'] = batch.department
|
||||||
elif batch.department_uuid:
|
elif batch.department_uuid:
|
||||||
|
@ -919,6 +1170,25 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
# # otherwise just view batch again
|
# # otherwise just view batch again
|
||||||
# return self.get_action_url('view', batch)
|
# return self.get_action_url('view', batch)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config):
|
||||||
|
cls._purchase_batch_defaults(config)
|
||||||
|
cls._batch_defaults(config)
|
||||||
|
cls._defaults(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _purchase_batch_defaults(cls, config):
|
||||||
|
route_prefix = cls.get_route_prefix()
|
||||||
|
url_prefix = cls.get_url_prefix()
|
||||||
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
|
||||||
|
# new batch using workflow X
|
||||||
|
config.add_route(f'{route_prefix}.create_workflow',
|
||||||
|
f'{url_prefix}/new/{{workflow_key}}/{{vendor_uuid}}')
|
||||||
|
config.add_view(cls, attr='create',
|
||||||
|
route_name=f'{route_prefix}.create_workflow',
|
||||||
|
permission=f'{permission_prefix}.create')
|
||||||
|
|
||||||
|
|
||||||
class NewProduct(colander.Schema):
|
class NewProduct(colander.Schema):
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
#
|
#
|
||||||
|
@ -28,14 +28,10 @@ import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import openpyxl
|
import openpyxl
|
||||||
from sqlalchemy import orm
|
|
||||||
|
|
||||||
from rattail.db import model, api
|
|
||||||
from rattail.core import Object
|
from rattail.core import Object
|
||||||
from rattail.time import localtime
|
|
||||||
|
|
||||||
from webhelpers2.html import tags
|
|
||||||
|
|
||||||
|
from tailbone.db import Session
|
||||||
from tailbone.views.purchasing import PurchasingBatchView
|
from tailbone.views.purchasing import PurchasingBatchView
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,6 +47,8 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
rows_editable = True
|
rows_editable = True
|
||||||
has_worksheet = True
|
has_worksheet = True
|
||||||
default_help_url = 'https://rattailproject.org/docs/rattail-manual/features/purchasing/ordering/index.html'
|
default_help_url = 'https://rattailproject.org/docs/rattail-manual/features/purchasing/ordering/index.html'
|
||||||
|
downloadable = True
|
||||||
|
configurable = True
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'po_total_calculated': "PO Total",
|
'po_total_calculated': "PO Total",
|
||||||
|
@ -59,9 +57,14 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
'id',
|
'id',
|
||||||
'store',
|
'store',
|
||||||
'buyer',
|
|
||||||
'vendor',
|
'vendor',
|
||||||
|
'description',
|
||||||
|
'workflow',
|
||||||
|
'order_file',
|
||||||
|
'order_parser_key',
|
||||||
|
'buyer',
|
||||||
'department',
|
'department',
|
||||||
|
'params',
|
||||||
'purchase',
|
'purchase',
|
||||||
'vendor_email',
|
'vendor_email',
|
||||||
'vendor_fax',
|
'vendor_fax',
|
||||||
|
@ -132,15 +135,26 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
return self.enum.PURCHASE_BATCH_MODE_ORDERING
|
return self.enum.PURCHASE_BATCH_MODE_ORDERING
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
super(OrderingBatchView, self).configure_form(f)
|
super().configure_form(f)
|
||||||
batch = f.model_instance
|
batch = f.model_instance
|
||||||
|
workflow = self.request.matchdict.get('workflow_key')
|
||||||
|
|
||||||
# purchase
|
# purchase
|
||||||
if self.creating or not batch.executed or not batch.purchase:
|
if self.creating or not batch.executed or not batch.purchase:
|
||||||
f.remove_field('purchase')
|
f.remove_field('purchase')
|
||||||
|
|
||||||
|
# now that all fields are setup, some final tweaks based on workflow
|
||||||
|
if self.creating and workflow:
|
||||||
|
|
||||||
|
if workflow == 'from_scratch':
|
||||||
|
f.remove('order_file',
|
||||||
|
'order_parser_key')
|
||||||
|
|
||||||
|
elif workflow == 'from_file':
|
||||||
|
f.set_required('order_file')
|
||||||
|
|
||||||
def get_batch_kwargs(self, batch, **kwargs):
|
def get_batch_kwargs(self, batch, **kwargs):
|
||||||
kwargs = super(OrderingBatchView, self).get_batch_kwargs(batch, **kwargs)
|
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
||||||
kwargs['ship_method'] = batch.ship_method
|
kwargs['ship_method'] = batch.ship_method
|
||||||
kwargs['notes_to_vendor'] = batch.notes_to_vendor
|
kwargs['notes_to_vendor'] = batch.notes_to_vendor
|
||||||
return kwargs
|
return kwargs
|
||||||
|
@ -155,7 +169,7 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
* ``cases_ordered``
|
* ``cases_ordered``
|
||||||
* ``units_ordered``
|
* ``units_ordered``
|
||||||
"""
|
"""
|
||||||
super(OrderingBatchView, self).configure_row_form(f)
|
super().configure_row_form(f)
|
||||||
|
|
||||||
# when editing, only certain fields should allow changes
|
# when editing, only certain fields should allow changes
|
||||||
if self.editing:
|
if self.editing:
|
||||||
|
@ -308,7 +322,7 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
title = self.get_instance_title(batch)
|
title = self.get_instance_title(batch)
|
||||||
order_date = batch.date_ordered
|
order_date = batch.date_ordered
|
||||||
if not order_date:
|
if not order_date:
|
||||||
order_date = localtime(self.rattail_config).date()
|
order_date = self.app.today()
|
||||||
|
|
||||||
return self.render_to_response('worksheet', {
|
return self.render_to_response('worksheet', {
|
||||||
'batch': batch,
|
'batch': batch,
|
||||||
|
@ -369,6 +383,7 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
of being updated. If a matching row is not found, it will not be
|
of being updated. If a matching row is not found, it will not be
|
||||||
created.
|
created.
|
||||||
"""
|
"""
|
||||||
|
model = self.app.model
|
||||||
batch = self.get_instance()
|
batch = self.get_instance()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -478,13 +493,75 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
return self.file_response(path)
|
return self.file_response(path)
|
||||||
|
|
||||||
def get_execute_success_url(self, batch, result, **kwargs):
|
def get_execute_success_url(self, batch, result, **kwargs):
|
||||||
|
model = self.app.model
|
||||||
if isinstance(result, model.Purchase):
|
if isinstance(result, model.Purchase):
|
||||||
return self.request.route_url('purchases.view', uuid=result.uuid)
|
return self.request.route_url('purchases.view', uuid=result.uuid)
|
||||||
return super(OrderingBatchView, self).get_execute_success_url(batch, result, **kwargs)
|
return super().get_execute_success_url(batch, result, **kwargs)
|
||||||
|
|
||||||
|
def configure_get_simple_settings(self):
|
||||||
|
return [
|
||||||
|
|
||||||
|
# workflows
|
||||||
|
{'section': 'rattail.batch',
|
||||||
|
'option': 'purchase.allow_ordering_from_scratch',
|
||||||
|
'type': bool,
|
||||||
|
'default': True},
|
||||||
|
{'section': 'rattail.batch',
|
||||||
|
'option': 'purchase.allow_ordering_from_file',
|
||||||
|
'type': bool,
|
||||||
|
'default': True},
|
||||||
|
|
||||||
|
# vendors
|
||||||
|
{'section': 'rattail.batch',
|
||||||
|
'option': 'purchase.allow_ordering_any_vendor',
|
||||||
|
'type': bool,
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def configure_get_context(self):
|
||||||
|
context = super().configure_get_context()
|
||||||
|
vendor_handler = self.app.get_vendor_handler()
|
||||||
|
|
||||||
|
Parsers = vendor_handler.get_all_order_parsers()
|
||||||
|
Supported = vendor_handler.get_supported_order_parsers()
|
||||||
|
context['order_parsers'] = Parsers
|
||||||
|
context['order_parsers_data'] = dict([(Parser.key, Parser in Supported)
|
||||||
|
for Parser in Parsers])
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def configure_gather_settings(self, data):
|
||||||
|
settings = super().configure_gather_settings(data)
|
||||||
|
vendor_handler = self.app.get_vendor_handler()
|
||||||
|
|
||||||
|
supported = []
|
||||||
|
for Parser in vendor_handler.get_all_order_parsers():
|
||||||
|
name = f'order_parser_{Parser.key}'
|
||||||
|
if data.get(name) == 'true':
|
||||||
|
supported.append(Parser.key)
|
||||||
|
settings.append({'name': 'rattail.vendors.supported_order_parsers',
|
||||||
|
'value': ', '.join(supported)})
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
def configure_remove_settings(self):
|
||||||
|
super().configure_remove_settings()
|
||||||
|
|
||||||
|
names = [
|
||||||
|
'rattail.vendors.supported_order_parsers',
|
||||||
|
]
|
||||||
|
|
||||||
|
# nb. using thread-local session here; we do not use
|
||||||
|
# self.Session b/c it may not point to Rattail
|
||||||
|
session = Session()
|
||||||
|
for name in names:
|
||||||
|
self.app.delete_setting(session, name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
cls._ordering_defaults(config)
|
cls._ordering_defaults(config)
|
||||||
|
cls._purchase_batch_defaults(config)
|
||||||
cls._batch_defaults(config)
|
cls._batch_defaults(config)
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
'store',
|
'store',
|
||||||
'vendor',
|
'vendor',
|
||||||
'description',
|
'description',
|
||||||
'receiving_workflow',
|
'workflow',
|
||||||
'truck_dump',
|
'truck_dump',
|
||||||
'truck_dump_children_first',
|
'truck_dump_children_first',
|
||||||
'truck_dump_children',
|
'truck_dump_children',
|
||||||
|
@ -235,135 +235,18 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
if not self.handler.allow_truck_dump_receiving():
|
if not self.handler.allow_truck_dump_receiving():
|
||||||
g.remove('truck_dump')
|
g.remove('truck_dump')
|
||||||
|
|
||||||
def create(self, form=None, **kwargs):
|
def get_supported_vendors(self):
|
||||||
"""
|
""" """
|
||||||
Custom view for creating a new receiving batch. We split the process
|
vendor_handler = self.app.get_vendor_handler()
|
||||||
into two steps, 1) choose and 2) create. This is because the specific
|
vendors = {}
|
||||||
form details for creating a batch will depend on which "type" of batch
|
for parser in self.batch_handler.get_supported_invoice_parsers():
|
||||||
creation is to be done, and it's much easier to keep conditional logic
|
if parser.vendor_key:
|
||||||
for that in the server instead of client-side etc.
|
vendor = vendor_handler.get_vendor(self.Session(),
|
||||||
|
parser.vendor_key)
|
||||||
See also
|
if vendor:
|
||||||
:meth:`tailbone.views.purchasing.costing:CostingBatchView.create()`
|
vendors[vendor.uuid] = vendor
|
||||||
which uses similar logic.
|
vendors = sorted(vendors.values(), key=lambda v: v.name)
|
||||||
"""
|
return vendors
|
||||||
model = self.model
|
|
||||||
route_prefix = self.get_route_prefix()
|
|
||||||
workflows = self.handler.supported_receiving_workflows()
|
|
||||||
valid_workflows = [workflow['workflow_key']
|
|
||||||
for workflow in workflows]
|
|
||||||
|
|
||||||
# if user has already identified their desired workflow, then we can
|
|
||||||
# just farm out to the default logic. we will of course configure our
|
|
||||||
# form differently, based on workflow, but this create() method at
|
|
||||||
# least will not need customization for that.
|
|
||||||
if self.request.matched_route.name.endswith('create_workflow'):
|
|
||||||
|
|
||||||
redirect = self.redirect(self.request.route_url('{}.create'.format(route_prefix)))
|
|
||||||
|
|
||||||
# however we do have one more thing to check - the workflow
|
|
||||||
# requested must of course be valid!
|
|
||||||
workflow_key = self.request.matchdict['workflow_key']
|
|
||||||
if workflow_key not in valid_workflows:
|
|
||||||
self.request.session.flash(
|
|
||||||
"Not a supported workflow: {}".format(workflow_key),
|
|
||||||
'error')
|
|
||||||
raise redirect
|
|
||||||
|
|
||||||
# also, we require vendor to be correctly identified. if
|
|
||||||
# someone e.g. navigates to a URL by accident etc. we want
|
|
||||||
# to gracefully handle and redirect
|
|
||||||
uuid = self.request.matchdict['vendor_uuid']
|
|
||||||
vendor = self.Session.get(model.Vendor, uuid)
|
|
||||||
if not vendor:
|
|
||||||
self.request.session.flash("Invalid vendor selection. "
|
|
||||||
"Please choose an existing vendor.",
|
|
||||||
'warning')
|
|
||||||
raise redirect
|
|
||||||
|
|
||||||
# okay now do the normal thing, per workflow
|
|
||||||
return super().create(**kwargs)
|
|
||||||
|
|
||||||
# on the other hand, if caller provided a form, that means we are in
|
|
||||||
# the middle of some other custom workflow, e.g. "add child to truck
|
|
||||||
# dump parent" or some such. in which case we also defer to the normal
|
|
||||||
# logic, so as to not interfere with that.
|
|
||||||
if form:
|
|
||||||
return super().create(form=form, **kwargs)
|
|
||||||
|
|
||||||
# okay, at this point we need the user to select a vendor and workflow
|
|
||||||
self.creating = True
|
|
||||||
context = {}
|
|
||||||
|
|
||||||
# form to accept user choice of vendor/workflow
|
|
||||||
schema = NewReceivingBatch().bind(valid_workflows=valid_workflows)
|
|
||||||
form = forms.Form(schema=schema, request=self.request)
|
|
||||||
|
|
||||||
# configure vendor field
|
|
||||||
app = self.get_rattail_app()
|
|
||||||
vendor_handler = app.get_vendor_handler()
|
|
||||||
if self.rattail_config.getbool('rattail.batch', 'purchase.supported_vendors_only'):
|
|
||||||
# only show vendors for which we have dedicated invoice parsers
|
|
||||||
vendors = {}
|
|
||||||
for parser in self.batch_handler.get_supported_invoice_parsers():
|
|
||||||
if parser.vendor_key:
|
|
||||||
vendor = vendor_handler.get_vendor(self.Session(),
|
|
||||||
parser.vendor_key)
|
|
||||||
if vendor:
|
|
||||||
vendors[vendor.uuid] = vendor
|
|
||||||
vendors = sorted(vendors.values(), key=lambda v: v.name)
|
|
||||||
vendor_values = [(vendor.uuid, vendor_handler.render_vendor(vendor))
|
|
||||||
for vendor in vendors]
|
|
||||||
form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values))
|
|
||||||
else:
|
|
||||||
# user may choose *any* available vendor
|
|
||||||
use_dropdown = vendor_handler.choice_uses_dropdown()
|
|
||||||
if use_dropdown:
|
|
||||||
vendors = self.Session.query(model.Vendor)\
|
|
||||||
.order_by(model.Vendor.id)\
|
|
||||||
.all()
|
|
||||||
vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, vendor.name))
|
|
||||||
for vendor in vendors]
|
|
||||||
form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values))
|
|
||||||
if len(vendors) == 1:
|
|
||||||
form.set_default('vendor', vendors[0].uuid)
|
|
||||||
else:
|
|
||||||
vendor_display = ""
|
|
||||||
if self.request.method == 'POST':
|
|
||||||
if self.request.POST.get('vendor'):
|
|
||||||
vendor = self.Session.get(model.Vendor, self.request.POST['vendor'])
|
|
||||||
if vendor:
|
|
||||||
vendor_display = str(vendor)
|
|
||||||
vendors_url = self.request.route_url('vendors.autocomplete')
|
|
||||||
form.set_widget('vendor', forms.widgets.JQueryAutocompleteWidget(
|
|
||||||
field_display=vendor_display, service_url=vendors_url))
|
|
||||||
form.set_validator('vendor', self.valid_vendor_uuid)
|
|
||||||
|
|
||||||
# configure workflow field
|
|
||||||
values = [(workflow['workflow_key'], workflow['display'])
|
|
||||||
for workflow in workflows]
|
|
||||||
form.set_widget('workflow',
|
|
||||||
dfwidget.SelectWidget(values=values))
|
|
||||||
if len(workflows) == 1:
|
|
||||||
form.set_default('workflow', workflows[0]['workflow_key'])
|
|
||||||
|
|
||||||
form.submit_label = "Continue"
|
|
||||||
form.cancel_url = self.get_index_url()
|
|
||||||
|
|
||||||
# if form validates, that means user has chosen a creation type, so we
|
|
||||||
# just redirect to the appropriate "new batch of type X" page
|
|
||||||
if form.validate():
|
|
||||||
workflow_key = form.validated['workflow']
|
|
||||||
vendor_uuid = form.validated['vendor']
|
|
||||||
url = self.request.route_url('{}.create_workflow'.format(route_prefix),
|
|
||||||
workflow_key=workflow_key,
|
|
||||||
vendor_uuid=vendor_uuid)
|
|
||||||
raise self.redirect(url)
|
|
||||||
|
|
||||||
context['form'] = form
|
|
||||||
if hasattr(form, 'make_deform_form'):
|
|
||||||
context['dform'] = form.make_deform_form()
|
|
||||||
return self.render_to_response('create', context)
|
|
||||||
|
|
||||||
def row_deletable(self, row):
|
def row_deletable(self, row):
|
||||||
|
|
||||||
|
@ -404,13 +287,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
# cancel should take us back to choosing a workflow
|
# cancel should take us back to choosing a workflow
|
||||||
f.cancel_url = self.request.route_url('{}.create'.format(route_prefix))
|
f.cancel_url = self.request.route_url('{}.create'.format(route_prefix))
|
||||||
|
|
||||||
# receiving_workflow
|
# TODO: remove this
|
||||||
if self.creating and workflow:
|
|
||||||
f.set_readonly('receiving_workflow')
|
|
||||||
f.set_renderer('receiving_workflow', self.render_receiving_workflow)
|
|
||||||
else:
|
|
||||||
f.remove('receiving_workflow')
|
|
||||||
|
|
||||||
# batch_type
|
# batch_type
|
||||||
if self.creating:
|
if self.creating:
|
||||||
f.set_widget('batch_type', dfwidget.HiddenWidget())
|
f.set_widget('batch_type', dfwidget.HiddenWidget())
|
||||||
|
@ -525,7 +402,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
|
|
||||||
# multiple invoice files (if applicable)
|
# multiple invoice files (if applicable)
|
||||||
if (not self.creating
|
if (not self.creating
|
||||||
and batch.get_param('receiving_workflow') == 'from_multi_invoice'):
|
and batch.get_param('workflow') == 'from_multi_invoice'):
|
||||||
|
|
||||||
if 'invoice_files' not in f:
|
if 'invoice_files' not in f:
|
||||||
f.insert_before('invoice_file', 'invoice_files')
|
f.insert_before('invoice_file', 'invoice_files')
|
||||||
|
@ -624,12 +501,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
items.append(HTML.tag('li', c=[link]))
|
items.append(HTML.tag('li', c=[link]))
|
||||||
return HTML.tag('ul', c=items)
|
return HTML.tag('ul', c=items)
|
||||||
|
|
||||||
def render_receiving_workflow(self, batch, field):
|
|
||||||
key = self.request.matchdict['workflow_key']
|
|
||||||
info = self.handler.receiving_workflow_info(key)
|
|
||||||
if info:
|
|
||||||
return info['display']
|
|
||||||
|
|
||||||
def get_visible_params(self, batch):
|
def get_visible_params(self, batch):
|
||||||
params = super().get_visible_params(batch)
|
params = super().get_visible_params(batch)
|
||||||
|
|
||||||
|
@ -654,42 +525,40 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
|
|
||||||
def get_batch_kwargs(self, batch, **kwargs):
|
def get_batch_kwargs(self, batch, **kwargs):
|
||||||
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
||||||
batch_type = self.request.POST['batch_type']
|
|
||||||
|
|
||||||
# must pull vendor from URL if it was not in form data
|
# must pull vendor from URL if it was not in form data
|
||||||
if 'vendor_uuid' not in kwargs and 'vendor' not in kwargs:
|
if 'vendor_uuid' not in kwargs and 'vendor' not in kwargs:
|
||||||
if 'vendor_uuid' in self.request.matchdict:
|
if 'vendor_uuid' in self.request.matchdict:
|
||||||
kwargs['vendor_uuid'] = self.request.matchdict['vendor_uuid']
|
kwargs['vendor_uuid'] = self.request.matchdict['vendor_uuid']
|
||||||
|
|
||||||
# TODO: ugh should just have workflow and no batch_type
|
workflow = kwargs['workflow']
|
||||||
kwargs['receiving_workflow'] = batch_type
|
if workflow == 'from_scratch':
|
||||||
if batch_type == 'from_scratch':
|
|
||||||
kwargs.pop('truck_dump_batch', None)
|
kwargs.pop('truck_dump_batch', None)
|
||||||
kwargs.pop('truck_dump_batch_uuid', None)
|
kwargs.pop('truck_dump_batch_uuid', None)
|
||||||
elif batch_type == 'from_invoice':
|
elif workflow == 'from_invoice':
|
||||||
pass
|
pass
|
||||||
elif batch_type == 'from_multi_invoice':
|
elif workflow == 'from_multi_invoice':
|
||||||
pass
|
pass
|
||||||
elif batch_type == 'from_po':
|
elif workflow == 'from_po':
|
||||||
# TODO: how to best handle this field? this doesn't seem flexible
|
# TODO: how to best handle this field? this doesn't seem flexible
|
||||||
kwargs['purchase_key'] = batch.purchase_uuid
|
kwargs['purchase_key'] = batch.purchase_uuid
|
||||||
elif batch_type == 'from_po_with_invoice':
|
elif workflow == 'from_po_with_invoice':
|
||||||
# TODO: how to best handle this field? this doesn't seem flexible
|
# TODO: how to best handle this field? this doesn't seem flexible
|
||||||
kwargs['purchase_key'] = batch.purchase_uuid
|
kwargs['purchase_key'] = batch.purchase_uuid
|
||||||
elif batch_type == 'truck_dump_children_first':
|
elif workflow == 'truck_dump_children_first':
|
||||||
kwargs['truck_dump'] = True
|
kwargs['truck_dump'] = True
|
||||||
kwargs['truck_dump_children_first'] = True
|
kwargs['truck_dump_children_first'] = True
|
||||||
kwargs['order_quantities_known'] = True
|
kwargs['order_quantities_known'] = True
|
||||||
# TODO: this makes sense in some cases, but all?
|
# TODO: this makes sense in some cases, but all?
|
||||||
# (should just omit that field when not relevant)
|
# (should just omit that field when not relevant)
|
||||||
kwargs['date_ordered'] = None
|
kwargs['date_ordered'] = None
|
||||||
elif batch_type == 'truck_dump_children_last':
|
elif workflow == 'truck_dump_children_last':
|
||||||
kwargs['truck_dump'] = True
|
kwargs['truck_dump'] = True
|
||||||
kwargs['truck_dump_ready'] = True
|
kwargs['truck_dump_ready'] = True
|
||||||
# TODO: this makes sense in some cases, but all?
|
# TODO: this makes sense in some cases, but all?
|
||||||
# (should just omit that field when not relevant)
|
# (should just omit that field when not relevant)
|
||||||
kwargs['date_ordered'] = None
|
kwargs['date_ordered'] = None
|
||||||
elif batch_type.startswith('truck_dump_child'):
|
elif workflow.startswith('truck_dump_child'):
|
||||||
truck_dump = self.get_instance()
|
truck_dump = self.get_instance()
|
||||||
kwargs['store'] = truck_dump.store
|
kwargs['store'] = truck_dump.store
|
||||||
kwargs['vendor'] = truck_dump.vendor
|
kwargs['vendor'] = truck_dump.vendor
|
||||||
|
@ -1986,6 +1855,12 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
'type': bool},
|
'type': bool},
|
||||||
|
|
||||||
# vendors
|
# vendors
|
||||||
|
{'section': 'rattail.batch',
|
||||||
|
'option': 'purchase.allow_receiving_any_vendor',
|
||||||
|
'type': bool},
|
||||||
|
# TODO: deprecated; can remove this once all live config
|
||||||
|
# is updated. but for now it remains so this setting is
|
||||||
|
# auto-deleted
|
||||||
{'section': 'rattail.batch',
|
{'section': 'rattail.batch',
|
||||||
'option': 'purchase.supported_vendors_only',
|
'option': 'purchase.supported_vendors_only',
|
||||||
'type': bool},
|
'type': bool},
|
||||||
|
@ -2036,6 +1911,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
cls._receiving_defaults(config)
|
cls._receiving_defaults(config)
|
||||||
|
cls._purchase_batch_defaults(config)
|
||||||
cls._batch_defaults(config)
|
cls._batch_defaults(config)
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
|
@ -2043,17 +1919,11 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
def _receiving_defaults(cls, config):
|
def _receiving_defaults(cls, config):
|
||||||
rattail_config = config.registry.settings.get('rattail_config')
|
rattail_config = config.registry.settings.get('rattail_config')
|
||||||
route_prefix = cls.get_route_prefix()
|
route_prefix = cls.get_route_prefix()
|
||||||
url_prefix = cls.get_url_prefix()
|
|
||||||
instance_url_prefix = cls.get_instance_url_prefix()
|
instance_url_prefix = cls.get_instance_url_prefix()
|
||||||
model_key = cls.get_model_key()
|
model_key = cls.get_model_key()
|
||||||
model_title = cls.get_model_title()
|
model_title = cls.get_model_title()
|
||||||
permission_prefix = cls.get_permission_prefix()
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
|
||||||
# new receiving batch using workflow X
|
|
||||||
config.add_route('{}.create_workflow'.format(route_prefix), '{}/new/{{workflow_key}}/{{vendor_uuid}}'.format(url_prefix))
|
|
||||||
config.add_view(cls, attr='create', route_name='{}.create_workflow'.format(route_prefix),
|
|
||||||
permission='{}.create'.format(permission_prefix))
|
|
||||||
|
|
||||||
# row-level receiving
|
# row-level receiving
|
||||||
config.add_route('{}.receive_row'.format(route_prefix), '{}/rows/{{row_uuid}}/receive'.format(instance_url_prefix))
|
config.add_route('{}.receive_row'.format(route_prefix), '{}/rows/{{row_uuid}}/receive'.format(instance_url_prefix))
|
||||||
config.add_view(cls, attr='receive_row', route_name='{}.receive_row'.format(route_prefix),
|
config.add_view(cls, attr='receive_row', route_name='{}.receive_row'.format(route_prefix),
|
||||||
|
@ -2106,33 +1976,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
permission='{}.auto_receive'.format(permission_prefix))
|
permission='{}.auto_receive'.format(permission_prefix))
|
||||||
|
|
||||||
|
|
||||||
@colander.deferred
|
|
||||||
def valid_workflow(node, kw):
|
|
||||||
"""
|
|
||||||
Deferred validator for ``workflow`` field, for new batches.
|
|
||||||
"""
|
|
||||||
valid_workflows = kw['valid_workflows']
|
|
||||||
|
|
||||||
def validate(node, value):
|
|
||||||
# we just need to provide possible values, and let stock validator
|
|
||||||
# handle the rest
|
|
||||||
oneof = colander.OneOf(valid_workflows)
|
|
||||||
return oneof(node, value)
|
|
||||||
|
|
||||||
return validate
|
|
||||||
|
|
||||||
|
|
||||||
class NewReceivingBatch(colander.Schema):
|
|
||||||
"""
|
|
||||||
Schema for choosing which "type" of new receiving batch should be created.
|
|
||||||
"""
|
|
||||||
vendor = colander.SchemaNode(colander.String(),
|
|
||||||
label="Vendor")
|
|
||||||
|
|
||||||
workflow = colander.SchemaNode(colander.String(),
|
|
||||||
validator=valid_workflow)
|
|
||||||
|
|
||||||
|
|
||||||
class ReceiveRowForm(colander.MappingSchema):
|
class ReceiveRowForm(colander.MappingSchema):
|
||||||
|
|
||||||
mode = colander.SchemaNode(colander.String(),
|
mode = colander.SchemaNode(colander.String(),
|
||||||
|
|
|
@ -25,11 +25,7 @@ Settings Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
|
|
||||||
|
@ -37,194 +33,159 @@ from rattail.db.model import Setting
|
||||||
from rattail.settings import Setting as AppSetting
|
from rattail.settings import Setting as AppSetting
|
||||||
from rattail.util import import_module_path
|
from rattail.util import import_module_path
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms, grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView, View
|
from tailbone.views import MasterView, View
|
||||||
from wuttaweb.util import get_libver, get_liburl
|
from wuttaweb.util import get_libver, get_liburl
|
||||||
|
from wuttaweb.views.settings import AppInfoView as WuttaAppInfoView
|
||||||
|
|
||||||
|
|
||||||
class AppInfoView(MasterView):
|
class AppInfoView(WuttaAppInfoView):
|
||||||
"""
|
""" """
|
||||||
Master view for the overall app, to show/edit config etc.
|
Session = Session
|
||||||
"""
|
weblib_config_prefix = 'tailbone'
|
||||||
route_prefix = 'appinfo'
|
|
||||||
model_key = 'UNUSED'
|
|
||||||
model_title = "UNUSED"
|
|
||||||
model_title_plural = "App Details"
|
|
||||||
creatable = False
|
|
||||||
viewable = False
|
|
||||||
editable = False
|
|
||||||
deletable = False
|
|
||||||
filterable = False
|
|
||||||
pageable = False
|
|
||||||
configurable = True
|
|
||||||
|
|
||||||
grid_columns = [
|
# TODO: for now we override to get tailbone searchable grid
|
||||||
'name',
|
def make_grid(self, **kwargs):
|
||||||
'version',
|
""" """
|
||||||
'editable_project_location',
|
return grids.Grid(self.request, **kwargs)
|
||||||
]
|
|
||||||
|
|
||||||
def get_index_title(self):
|
|
||||||
app = self.get_rattail_app()
|
|
||||||
return "{} for {}".format(self.get_model_title_plural(),
|
|
||||||
app.get_title())
|
|
||||||
|
|
||||||
def get_data(self, session=None):
|
|
||||||
pip = os.path.join(sys.prefix, 'bin', 'pip')
|
|
||||||
output = subprocess.check_output([pip, 'list', '--format=json'])
|
|
||||||
data = json.loads(output.decode('utf_8').strip())
|
|
||||||
|
|
||||||
for pkg in data:
|
|
||||||
pkg.setdefault('editable_project_location', '')
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
""" """
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# sort on frontend
|
|
||||||
g.sort_on_backend = False
|
|
||||||
g.sort_multiple = False
|
|
||||||
g.set_sort_defaults('name')
|
|
||||||
|
|
||||||
# name
|
# name
|
||||||
g.set_searchable('name')
|
g.set_searchable('name')
|
||||||
|
|
||||||
# editable_project_location
|
# editable_project_location
|
||||||
g.set_searchable('editable_project_location')
|
g.set_searchable('editable_project_location')
|
||||||
|
|
||||||
def template_kwargs_index(self, **kwargs):
|
|
||||||
kwargs = super().template_kwargs_index(**kwargs)
|
|
||||||
kwargs['configure_button_title'] = "Configure App"
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def get_weblibs(self):
|
|
||||||
""" """
|
|
||||||
return OrderedDict([
|
|
||||||
('vue', "Vue"),
|
|
||||||
('vue_resource', "vue-resource"),
|
|
||||||
('buefy', "Buefy"),
|
|
||||||
('buefy.css', "Buefy CSS"),
|
|
||||||
('fontawesome', "FontAwesome"),
|
|
||||||
('bb_vue', "(BB) vue"),
|
|
||||||
('bb_oruga', "(BB) @oruga-ui/oruga-next"),
|
|
||||||
('bb_oruga_bulma', "(BB) @oruga-ui/theme-bulma (JS)"),
|
|
||||||
('bb_oruga_bulma_css', "(BB) @oruga-ui/theme-bulma (CSS)"),
|
|
||||||
('bb_fontawesome_svg_core', "(BB) @fortawesome/fontawesome-svg-core"),
|
|
||||||
('bb_free_solid_svg_icons', "(BB) @fortawesome/free-solid-svg-icons"),
|
|
||||||
('bb_vue_fontawesome', "(BB) @fortawesome/vue-fontawesome"),
|
|
||||||
])
|
|
||||||
|
|
||||||
def configure_get_context(self, **kwargs):
|
def configure_get_context(self, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
context = super().configure_get_context(**kwargs)
|
context = super().configure_get_context(**kwargs)
|
||||||
simple_settings = context['simple_settings']
|
simple_settings = context['simple_settings']
|
||||||
weblibs = self.get_weblibs()
|
weblibs = context['weblibs']
|
||||||
|
|
||||||
for key in weblibs:
|
for weblib in weblibs:
|
||||||
title = weblibs[key]
|
key = weblib['key']
|
||||||
weblibs[key] = {
|
|
||||||
'key': key,
|
|
||||||
'title': title,
|
|
||||||
|
|
||||||
# nb. these values are exactly as configured, and are
|
|
||||||
# used for editing the settings
|
|
||||||
'configured_version': get_libver(self.request, key,
|
|
||||||
prefix='tailbone',
|
|
||||||
configured_only=True),
|
|
||||||
'configured_url': get_liburl(self.request, key,
|
|
||||||
prefix='tailbone',
|
|
||||||
configured_only=True),
|
|
||||||
|
|
||||||
# these are for informational purposes only
|
|
||||||
'default_version': get_libver(self.request, key,
|
|
||||||
prefix='tailbone',
|
|
||||||
default_only=True),
|
|
||||||
'live_url': get_liburl(self.request, key,
|
|
||||||
prefix='tailbone'),
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO: this is only needed to migrate legacy settings to
|
# TODO: this is only needed to migrate legacy settings to
|
||||||
# use the newer wutaweb setting names
|
# use the newer wuttaweb setting names
|
||||||
url = simple_settings[f'wuttaweb.liburl.{key}']
|
url = simple_settings[f'wuttaweb.liburl.{key}']
|
||||||
if not url and weblibs[key]['configured_url']:
|
if not url and weblib['configured_url']:
|
||||||
simple_settings[f'wuttaweb.liburl.{key}'] = weblibs[key]['configured_url']
|
simple_settings[f'wuttaweb.liburl.{key}'] = weblib['configured_url']
|
||||||
|
|
||||||
context['weblibs'] = list(weblibs.values())
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
# nb. these email settings require special handling below
|
||||||
|
configure_profile_key_mismatches = [
|
||||||
|
'default.subject',
|
||||||
|
'default.to',
|
||||||
|
'default.cc',
|
||||||
|
'default.bcc',
|
||||||
|
'feedback.subject',
|
||||||
|
'feedback.to',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_get_simple_settings(self):
|
def configure_get_simple_settings(self):
|
||||||
""" """
|
""" """
|
||||||
simple_settings = [
|
simple_settings = super().configure_get_simple_settings()
|
||||||
|
|
||||||
# basics
|
# TODO:
|
||||||
{'section': 'rattail',
|
# there are several email config keys which differ between
|
||||||
'option': 'app_title'},
|
# wuttjamaican and rattail. basically all of the "profile" keys
|
||||||
{'section': 'rattail',
|
# have a different prefix.
|
||||||
'option': 'node_type'},
|
|
||||||
{'section': 'rattail',
|
|
||||||
'option': 'node_title'},
|
|
||||||
{'section': 'rattail',
|
|
||||||
'option': 'production',
|
|
||||||
'type': bool},
|
|
||||||
{'section': 'rattail',
|
|
||||||
'option': 'running_from_source',
|
|
||||||
'type': bool},
|
|
||||||
{'section': 'rattail',
|
|
||||||
'option': 'running_from_source.rootpkg'},
|
|
||||||
|
|
||||||
# display
|
# after wuttaweb has declared its settings, we examine each and
|
||||||
{'section': 'tailbone',
|
# overwrite the value if one is defined with rattail config key.
|
||||||
'option': 'background_color'},
|
# (nb. this happens even if wuttjamaican key has a value!)
|
||||||
|
|
||||||
# grids
|
# note that we *do* declare the profile mismatch keys for
|
||||||
{'section': 'tailbone',
|
# rattail, as part of simple settings. this ensures the
|
||||||
'option': 'grid.default_pagesize',
|
# parent logic will always remove them when saving. however
|
||||||
# TODO: seems like should enforce this, but validation is
|
# we must also include them in gather_settings() to ensure
|
||||||
# not setup yet
|
# they are saved to match wuttjamaican values.
|
||||||
# 'type': int
|
|
||||||
},
|
|
||||||
|
|
||||||
# nb. these are no longer used (deprecated), but we keep
|
# there are also a couple of flags where rattail's default is the
|
||||||
# them defined here so the tool auto-deletes them
|
# opposite of wuttjamaican. so we overwrite those too as needed.
|
||||||
{'section': 'tailbone',
|
|
||||||
'option': 'buefy_version'},
|
|
||||||
{'section': 'tailbone',
|
|
||||||
'option': 'vue_version'},
|
|
||||||
|
|
||||||
]
|
for setting in simple_settings:
|
||||||
|
|
||||||
def getval(key):
|
# nb. the update home page redirect setting is off by
|
||||||
return self.config.get(f'tailbone.{key}')
|
# default for wuttaweb, but on for tailbone
|
||||||
|
if setting['name'] == 'wuttaweb.home_redirect_to_login':
|
||||||
|
value = self.config.get_bool('wuttaweb.home_redirect_to_login')
|
||||||
|
if value is None:
|
||||||
|
value = self.config.get_bool('tailbone.login_is_home', default=True)
|
||||||
|
setting['value'] = value
|
||||||
|
|
||||||
weblibs = self.get_weblibs()
|
# nb. sending email is off by default for wuttjamaican,
|
||||||
for key, title in weblibs.items():
|
# but on for rattail
|
||||||
|
elif setting['name'] == 'rattail.mail.send_emails':
|
||||||
|
value = self.config.get_bool('rattail.mail.send_emails', default=True)
|
||||||
|
setting['value'] = value
|
||||||
|
|
||||||
simple_settings.append({
|
# nb. this one is even more special, key is entirely different
|
||||||
'section': 'wuttaweb',
|
elif setting['name'] == 'rattail.email.default.sender':
|
||||||
'option': f"libver.{key}",
|
value = self.config.get('rattail.email.default.sender')
|
||||||
'default': getval(f"libver.{key}"),
|
if value is None:
|
||||||
})
|
value = self.config.get('rattail.mail.default.from')
|
||||||
simple_settings.append({
|
setting['value'] = value
|
||||||
'section': 'wuttaweb',
|
|
||||||
'option': f"liburl.{key}",
|
|
||||||
'default': getval(f"liburl.{key}"),
|
|
||||||
})
|
|
||||||
|
|
||||||
# nb. these are no longer used (deprecated), but we keep
|
else:
|
||||||
# them defined here so the tool auto-deletes them
|
|
||||||
simple_settings.append({
|
# nb. fetch alternate value for profile key mismatch
|
||||||
'section': 'tailbone',
|
for key in self.configure_profile_key_mismatches:
|
||||||
'option': f"libver.{key}",
|
if setting['name'] == f'rattail.email.{key}':
|
||||||
})
|
value = self.config.get(f'rattail.email.{key}')
|
||||||
simple_settings.append({
|
if value is None:
|
||||||
'section': 'tailbone',
|
value = self.config.get(f'rattail.mail.{key}')
|
||||||
'option': f"liburl.{key}",
|
setting['value'] = value
|
||||||
})
|
break
|
||||||
|
|
||||||
|
# nb. these are no longer used (deprecated), but we keep
|
||||||
|
# them defined here so the tool auto-deletes them
|
||||||
|
|
||||||
|
simple_settings.extend([
|
||||||
|
{'name': 'tailbone.login_is_home'},
|
||||||
|
{'name': 'tailbone.buefy_version'},
|
||||||
|
{'name': 'tailbone.vue_version'},
|
||||||
|
])
|
||||||
|
|
||||||
|
simple_settings.append({'name': 'rattail.mail.default.from'})
|
||||||
|
for key in self.configure_profile_key_mismatches:
|
||||||
|
simple_settings.append({'name': f'rattail.mail.{key}'})
|
||||||
|
|
||||||
|
for key in self.get_weblibs():
|
||||||
|
simple_settings.extend([
|
||||||
|
{'name': f'tailbone.libver.{key}'},
|
||||||
|
{'name': f'tailbone.liburl.{key}'},
|
||||||
|
])
|
||||||
|
|
||||||
return simple_settings
|
return simple_settings
|
||||||
|
|
||||||
|
def configure_gather_settings(self, data, simple_settings=None):
|
||||||
|
""" """
|
||||||
|
settings = super().configure_gather_settings(data, simple_settings=simple_settings)
|
||||||
|
|
||||||
|
# nb. must add legacy rattail profile settings to match new ones
|
||||||
|
for setting in list(settings):
|
||||||
|
|
||||||
|
if setting['name'] == 'rattail.email.default.sender':
|
||||||
|
value = setting['value']
|
||||||
|
settings.append({'name': 'rattail.mail.default.from',
|
||||||
|
'value': value})
|
||||||
|
|
||||||
|
else:
|
||||||
|
for key in self.configure_profile_key_mismatches:
|
||||||
|
if setting['name'] == f'rattail.email.{key}':
|
||||||
|
value = setting['value']
|
||||||
|
settings.append({'name': f'rattail.mail.{key}',
|
||||||
|
'value': value})
|
||||||
|
break
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
class SettingView(MasterView):
|
class SettingView(MasterView):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -348,56 +348,27 @@ class UpgradeView(MasterView):
|
||||||
commit_hash_pattern = re.compile(r'^.{40}$')
|
commit_hash_pattern = re.compile(r'^.{40}$')
|
||||||
|
|
||||||
def get_changelog_projects(self):
|
def get_changelog_projects(self):
|
||||||
projects = {
|
project_map = {
|
||||||
'rattail': {
|
'onager': 'onager',
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail/changelog/{new_version}/?size=10',
|
'pyCOREPOS': 'pycorepos',
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail/files/v{new_version}/CHANGES.rst',
|
'rattail': 'rattail',
|
||||||
},
|
'rattail_corepos': 'rattail-corepos',
|
||||||
'Tailbone': {
|
'rattail-onager': 'rattail-onager',
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone/changelog/{new_version}/?size=10',
|
'rattail_tempmon': 'rattail-tempmon',
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone/files/v{new_version}/CHANGES.rst',
|
'rattail_woocommerce': 'rattail-woocommerce',
|
||||||
},
|
'Tailbone': 'tailbone',
|
||||||
'pyCOREPOS': {
|
'tailbone_corepos': 'tailbone-corepos',
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/pycorepos/changelog/{new_version}/?size=10',
|
'tailbone-onager': 'tailbone-onager',
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/pycorepos/files/v{new_version}/CHANGES.rst',
|
'tailbone_theo': 'theo',
|
||||||
},
|
'tailbone_woocommerce': 'tailbone-woocommerce',
|
||||||
'rattail_corepos': {
|
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-corepos/changelog/{new_version}/?size=10',
|
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-corepos/files/v{new_version}/CHANGES.rst',
|
|
||||||
},
|
|
||||||
'tailbone_corepos': {
|
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-corepos/changelog/{new_version}/?size=10',
|
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-corepos/files/v{new_version}/CHANGES.rst',
|
|
||||||
},
|
|
||||||
'onager': {
|
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/onager/changelog/{new_version}/?size=10',
|
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/onager/files/v{new_version}/CHANGES.rst',
|
|
||||||
},
|
|
||||||
'rattail-onager': {
|
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/rattail-onager/changelog/{new_version}/?size=10',
|
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/rattail-onager/files/v{new_version}/CHANGELOG.md',
|
|
||||||
},
|
|
||||||
'rattail_tempmon': {
|
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-tempmon/changelog/{new_version}/?size=10',
|
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-tempmon/files/v{new_version}/CHANGES.rst',
|
|
||||||
},
|
|
||||||
'tailbone-onager': {
|
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/tailbone-onager/changelog/{new_version}/?size=10',
|
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/tailbone-onager/files/v{new_version}/CHANGELOG.md',
|
|
||||||
},
|
|
||||||
'rattail_woocommerce': {
|
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-woocommerce/changelog/{new_version}/?size=10',
|
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-woocommerce/files/v{new_version}/CHANGES.rst',
|
|
||||||
},
|
|
||||||
'tailbone_woocommerce': {
|
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-woocommerce/changelog/{new_version}/?size=10',
|
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-woocommerce/files/v{new_version}/CHANGES.rst',
|
|
||||||
},
|
|
||||||
'tailbone_theo': {
|
|
||||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/theo/changelog/{new_version}/?size=10',
|
|
||||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/theo/files/v{new_version}/CHANGES.rst',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projects = {}
|
||||||
|
for name, repo in project_map.items():
|
||||||
|
projects[name] = {
|
||||||
|
'commit_url': f'https://forgejo.wuttaproject.org/rattail/{repo}/compare/{{old_version}}...{{new_version}}',
|
||||||
|
'release_url': f'https://forgejo.wuttaproject.org/rattail/{repo}/src/tag/v{{new_version}}/CHANGELOG.md',
|
||||||
|
}
|
||||||
return projects
|
return projects
|
||||||
|
|
||||||
def get_changelog_url(self, project, old_version, new_version):
|
def get_changelog_url(self, project, old_version, new_version):
|
||||||
|
|
|
@ -801,4 +801,8 @@ def defaults(config, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
defaults(config)
|
wutta_config = config.registry.settings['wutta_config']
|
||||||
|
if wutta_config.get_bool('tailbone.use_wutta_views', default=False, usedb=False):
|
||||||
|
config.include('tailbone.views.wutta.users')
|
||||||
|
else:
|
||||||
|
defaults(config)
|
||||||
|
|
|
@ -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.
|
||||||
#
|
#
|
||||||
|
@ -83,12 +83,12 @@ class WorkOrderView(MasterView):
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
super(WorkOrderView, self).__init__(request)
|
super().__init__(request)
|
||||||
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 configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super(WorkOrderView, self).configure_grid(g)
|
super().configure_grid(g)
|
||||||
model = self.model
|
model = self.model
|
||||||
|
|
||||||
# customer
|
# customer
|
||||||
|
@ -113,7 +113,7 @@ class WorkOrderView(MasterView):
|
||||||
return 'warning'
|
return 'warning'
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
super(WorkOrderView, self).configure_form(f)
|
super().configure_form(f)
|
||||||
model = self.model
|
model = self.model
|
||||||
SelectWidget = forms.widgets.JQuerySelectWidget
|
SelectWidget = forms.widgets.JQuerySelectWidget
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ class WorkOrderView(MasterView):
|
||||||
return event.workorder
|
return event.workorder
|
||||||
|
|
||||||
def configure_row_grid(self, g):
|
def configure_row_grid(self, g):
|
||||||
super(WorkOrderView, self).configure_row_grid(g)
|
super().configure_row_grid(g)
|
||||||
g.set_enum('type_code', self.enum.WORKORDER_EVENT)
|
g.set_enum('type_code', self.enum.WORKORDER_EVENT)
|
||||||
g.set_sort_defaults('occurred')
|
g.set_sort_defaults('occurred')
|
||||||
|
|
||||||
|
@ -353,7 +353,7 @@ class WorkOrderView(MasterView):
|
||||||
class StatusFilter(grids.filters.AlchemyIntegerFilter):
|
class StatusFilter(grids.filters.AlchemyIntegerFilter):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(StatusFilter, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
from drild import enum
|
from drild import enum
|
||||||
|
|
||||||
|
@ -369,14 +369,14 @@ class StatusFilter(grids.filters.AlchemyIntegerFilter):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def verb_labels(self):
|
def verb_labels(self):
|
||||||
labels = dict(super(StatusFilter, self).verb_labels)
|
labels = dict(super().verb_labels)
|
||||||
labels['is_active'] = "Is Active"
|
labels['is_active'] = "Is Active"
|
||||||
labels['not_active'] = "Is Not Active"
|
labels['not_active'] = "Is Not Active"
|
||||||
return labels
|
return labels
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def valueless_verbs(self):
|
def valueless_verbs(self):
|
||||||
verbs = list(super(StatusFilter, self).valueless_verbs)
|
verbs = list(super().valueless_verbs)
|
||||||
verbs.extend([
|
verbs.extend([
|
||||||
'is_active',
|
'is_active',
|
||||||
'not_active',
|
'not_active',
|
||||||
|
@ -385,7 +385,11 @@ class StatusFilter(grids.filters.AlchemyIntegerFilter):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_verbs(self):
|
def default_verbs(self):
|
||||||
verbs = list(super(StatusFilter, self).default_verbs)
|
verbs = super().default_verbs
|
||||||
|
if callable(verbs):
|
||||||
|
verbs = verbs()
|
||||||
|
|
||||||
|
verbs = list(verbs or [])
|
||||||
verbs.insert(0, 'is_active')
|
verbs.insert(0, 'is_active')
|
||||||
verbs.insert(1, 'not_active')
|
verbs.insert(1, 'not_active')
|
||||||
return verbs
|
return verbs
|
||||||
|
|
|
@ -32,6 +32,7 @@ from wuttaweb.views import people as wutta
|
||||||
from tailbone.views import people as tailbone
|
from tailbone.views import people as tailbone
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from rattail.db.model import Person
|
from rattail.db.model import Person
|
||||||
|
from tailbone.grids import Grid
|
||||||
|
|
||||||
|
|
||||||
class PersonView(wutta.PersonView):
|
class PersonView(wutta.PersonView):
|
||||||
|
@ -44,7 +45,6 @@ class PersonView(wutta.PersonView):
|
||||||
"""
|
"""
|
||||||
model_class = Person
|
model_class = Person
|
||||||
Session = Session
|
Session = Session
|
||||||
sort_defaults = 'display_name'
|
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'display_name': "Full Name",
|
'display_name': "Full Name",
|
||||||
|
@ -59,6 +59,11 @@ class PersonView(wutta.PersonView):
|
||||||
'merge_requested',
|
'merge_requested',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
filter_defaults = {
|
||||||
|
'display_name': {'active': True, 'verb': 'contains'},
|
||||||
|
}
|
||||||
|
sort_defaults = 'display_name'
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
'first_name',
|
'first_name',
|
||||||
'middle_name',
|
'middle_name',
|
||||||
|
@ -74,6 +79,11 @@ class PersonView(wutta.PersonView):
|
||||||
# CRUD methods
|
# CRUD methods
|
||||||
##############################
|
##############################
|
||||||
|
|
||||||
|
# TODO: must use older grid for now, to render filters correctly
|
||||||
|
def make_grid(self, **kwargs):
|
||||||
|
""" """
|
||||||
|
return Grid(self.request, **kwargs)
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
""" """
|
""" """
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
57
tailbone/views/wutta/users.py
Normal file
57
tailbone/views/wutta/users.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation, either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
User Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from wuttaweb.views import users as wutta
|
||||||
|
from tailbone.views import users as tailbone
|
||||||
|
from tailbone.db import Session
|
||||||
|
from rattail.db.model import User
|
||||||
|
from tailbone.grids import Grid
|
||||||
|
|
||||||
|
|
||||||
|
class UserView(wutta.UserView):
|
||||||
|
"""
|
||||||
|
This is the first attempt at blending newer Wutta views with
|
||||||
|
legacy Tailbone config.
|
||||||
|
|
||||||
|
So, this is a Wutta-based view but it should be included by a
|
||||||
|
Tailbone app configurator.
|
||||||
|
"""
|
||||||
|
model_class = User
|
||||||
|
Session = Session
|
||||||
|
|
||||||
|
# TODO: must use older grid for now, to render filters correctly
|
||||||
|
def make_grid(self, **kwargs):
|
||||||
|
""" """
|
||||||
|
return Grid(self.request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config, **kwargs):
|
||||||
|
kwargs.setdefault('UserView', UserView)
|
||||||
|
tailbone.defaults(config, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
|
@ -57,6 +57,12 @@ class TestGrid(WebTestCase):
|
||||||
grid = self.make_grid(default_page=42)
|
grid = self.make_grid(default_page=42)
|
||||||
self.assertEqual(grid.page, 42)
|
self.assertEqual(grid.page, 42)
|
||||||
|
|
||||||
|
# searchable
|
||||||
|
grid = self.make_grid()
|
||||||
|
self.assertEqual(grid.searchable_columns, set())
|
||||||
|
grid = self.make_grid(searchable={'foo': True})
|
||||||
|
self.assertEqual(grid.searchable_columns, {'foo'})
|
||||||
|
|
||||||
def test_vue_tagname(self):
|
def test_vue_tagname(self):
|
||||||
|
|
||||||
# default
|
# default
|
||||||
|
@ -129,7 +135,7 @@ class TestGrid(WebTestCase):
|
||||||
|
|
||||||
def test_set_label(self):
|
def test_set_label(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
grid = self.make_grid(model_class=model.Setting)
|
grid = self.make_grid(model_class=model.Setting, filterable=True)
|
||||||
self.assertEqual(grid.labels, {})
|
self.assertEqual(grid.labels, {})
|
||||||
|
|
||||||
# basic
|
# basic
|
||||||
|
|
|
@ -5,12 +5,9 @@ from unittest import TestCase
|
||||||
|
|
||||||
from pyramid.config import Configurator
|
from pyramid.config import Configurator
|
||||||
|
|
||||||
from wuttjamaican.testing import FileConfigTestCase
|
|
||||||
|
|
||||||
from rattail.exceptions import ConfigurationError
|
from rattail.exceptions import ConfigurationError
|
||||||
from rattail.config import RattailConfig
|
from rattail.testing import DataTestCase
|
||||||
from tailbone import app as mod
|
from tailbone import app as mod
|
||||||
from tests.util import DataTestCase
|
|
||||||
|
|
||||||
|
|
||||||
class TestRattailConfig(TestCase):
|
class TestRattailConfig(TestCase):
|
||||||
|
@ -30,7 +27,7 @@ class TestRattailConfig(TestCase):
|
||||||
|
|
||||||
class TestMakePyramidConfig(DataTestCase):
|
class TestMakePyramidConfig(DataTestCase):
|
||||||
|
|
||||||
def make_config(self):
|
def make_config(self, **kwargs):
|
||||||
myconf = self.write_file('web.conf', """
|
myconf = self.write_file('web.conf', """
|
||||||
[rattail.db]
|
[rattail.db]
|
||||||
default.url = sqlite://
|
default.url = sqlite://
|
||||||
|
|
|
@ -38,7 +38,7 @@ class TestPersonView(WebTestCase):
|
||||||
|
|
||||||
def test_configure_form(self):
|
def test_configure_form(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
barney = model.User(username='barney')
|
barney = model.Person(display_name="Barney Rubble")
|
||||||
self.session.add(barney)
|
self.session.add(barney)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue