Compare commits
No commits in common. "master" and "v0.20.1" have entirely different histories.
60 changed files with 1668 additions and 2100 deletions
CHANGELOG.mdREADME.rstutil.py
docs
pyproject.tomltailbone
api
app.pydiffs.pyforms
grids
helpers.pymenus.pytemplates
appinfo
base.makobase_meta.makoconfigure.makodatasync
deform
formposter.makoforms
grids
home.makologin.makomaster
ordering
page.makoproducts
receiving
reports/problems
themes
butterball
waterpark
views
tests
158
CHANGELOG.md
158
CHANGELOG.md
|
@ -5,164 +5,6 @@ All notable changes to Tailbone will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v0.22.7 (2025-02-19)
|
||||
|
||||
### Fix
|
||||
|
||||
- stop using old config for logo image url on login page
|
||||
- fix warning msg for deprecated Grid param
|
||||
|
||||
## v0.22.6 (2025-02-01)
|
||||
|
||||
### Fix
|
||||
|
||||
- register vue3 form component for products -> make batch
|
||||
|
||||
## v0.22.5 (2024-12-16)
|
||||
|
||||
### Fix
|
||||
|
||||
- whoops this is latest rattail
|
||||
- require newer rattail lib
|
||||
- require newer wuttaweb
|
||||
- let caller request safe HTML literal for rendered grid table
|
||||
|
||||
## v0.22.4 (2024-11-22)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid error in product search for duplicated key
|
||||
- use vmodel for confirm password widget input
|
||||
|
||||
## v0.22.3 (2024-11-19)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid error for trainwreck query when not a customer
|
||||
|
||||
## v0.22.2 (2024-11-18)
|
||||
|
||||
### Fix
|
||||
|
||||
- use local/custom enum for continuum operations
|
||||
- add basic master view for Product Costs
|
||||
- show continuum operation type when viewing version history
|
||||
- always define `app` attr for ViewSupplement
|
||||
- avoid deprecated import
|
||||
|
||||
## v0.22.1 (2024-11-02)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix submit button for running problem report
|
||||
- avoid deprecated grid method
|
||||
|
||||
## v0.22.0 (2024-10-22)
|
||||
|
||||
### Feat
|
||||
|
||||
- add support for new ordering batch from parsed file
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid deprecated method to suggest username
|
||||
|
||||
## v0.21.11 (2024-10-03)
|
||||
|
||||
### Fix
|
||||
|
||||
- custom method for adding grid action
|
||||
- become/stop root should redirect to previous url
|
||||
|
||||
## v0.21.10 (2024-09-15)
|
||||
|
||||
### Fix
|
||||
|
||||
- update project repo links, kallithea -> forgejo
|
||||
- use better icon for submit button on login page
|
||||
- wrap notes text for batch view
|
||||
- expose datasync consumer batch size via configure page
|
||||
|
||||
## v0.21.9 (2024-08-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- render custom attrs in form component tag
|
||||
|
||||
## v0.21.8 (2024-08-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- ignore session kwarg for `MasterView.make_row_grid()`
|
||||
|
||||
## v0.21.7 (2024-08-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid error when form value cannot be obtained
|
||||
|
||||
## v0.21.6 (2024-08-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid error when grid value cannot be obtained
|
||||
|
||||
## v0.21.5 (2024-08-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- set empty string for "-new-" file configure option
|
||||
|
||||
## v0.21.4 (2024-08-26)
|
||||
|
||||
### Fix
|
||||
|
||||
- handle differing email profile keys for appinfo/configure
|
||||
|
||||
## v0.21.3 (2024-08-26)
|
||||
|
||||
### Fix
|
||||
|
||||
- show non-standard config values for app info configure email
|
||||
|
||||
## v0.21.2 (2024-08-26)
|
||||
|
||||
### Fix
|
||||
|
||||
- refactor waterpark base template to use wutta feedback component
|
||||
- fix input/output file upload feature for configure pages, per oruga
|
||||
- tweak how grid data translates to Vue template context
|
||||
- merge filters into main grid template
|
||||
- add basic wutta view for users
|
||||
- some fixes for wutta people view
|
||||
- various fixes for waterpark theme
|
||||
- avoid deprecated `component` form kwarg
|
||||
|
||||
## v0.21.1 (2024-08-22)
|
||||
|
||||
### Fix
|
||||
|
||||
- misc. bugfixes per recent changes
|
||||
|
||||
## v0.21.0 (2024-08-22)
|
||||
|
||||
### Feat
|
||||
|
||||
- move "most" filtering logic for grid class to wuttaweb
|
||||
- inherit from wuttaweb templates for home, login pages
|
||||
- inherit from wuttaweb for AppInfoView, appinfo/configure template
|
||||
- add "has output file templates" config option for master view
|
||||
|
||||
### Fix
|
||||
|
||||
- change grid reset-view param name to match wuttaweb
|
||||
- move "searchable columns" grid feature to wuttaweb
|
||||
- use wuttaweb to get/render csrf token
|
||||
- inherit from wuttaweb for appinfo/index template
|
||||
- prefer wuttaweb config for "home redirect to login" feature
|
||||
- fix master/index template rendering for waterpark theme
|
||||
- fix spacing for navbar logo/title in waterpark theme
|
||||
|
||||
## v0.20.1 (2024-08-20)
|
||||
|
||||
### Fix
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
|
||||
# Tailbone
|
||||
Tailbone
|
||||
========
|
||||
|
||||
Tailbone is an extensible web application based on Rattail. It provides a
|
||||
"back-office network environment" (BONE) for use in managing retail data.
|
||||
|
||||
Please see Rattail's [home page](http://rattailproject.org/) for more
|
||||
information.
|
||||
Please see Rattail's `home page`_ for more information.
|
||||
|
||||
.. _home page: http://rattailproject.org/
|
|
@ -27,10 +27,10 @@ templates_path = ['_templates']
|
|||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
intersphinx_mapping = {
|
||||
'rattail': ('https://docs.wuttaproject.org/rattail/', None),
|
||||
'rattail': ('https://rattailproject.org/docs/rattail/', None),
|
||||
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
||||
'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None),
|
||||
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
|
||||
'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None),
|
||||
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
|
||||
}
|
||||
|
||||
# allow todo entries to show up
|
||||
|
|
|
@ -6,9 +6,9 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "Tailbone"
|
||||
version = "0.22.7"
|
||||
version = "0.20.1"
|
||||
description = "Backoffice Web Application for Rattail"
|
||||
readme = "README.md"
|
||||
readme = "README.rst"
|
||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||
license = {text = "GNU GPL v3+"}
|
||||
classifiers = [
|
||||
|
@ -53,13 +53,13 @@ dependencies = [
|
|||
"pyramid_mako",
|
||||
"pyramid_retry",
|
||||
"pyramid_tm",
|
||||
"rattail[db,bouncer]>=0.20.1",
|
||||
"rattail[db,bouncer]>=0.18.1",
|
||||
"sa-filters",
|
||||
"simplejson",
|
||||
"transaction",
|
||||
"waitress",
|
||||
"WebHelpers2",
|
||||
"WuttaWeb>=0.21.0",
|
||||
"WuttaWeb>=0.11.0",
|
||||
"zope.sqlalchemy>=1.5",
|
||||
]
|
||||
|
||||
|
@ -84,9 +84,9 @@ tailbone = "tailbone.config:ConfigExtension"
|
|||
|
||||
[project.urls]
|
||||
Homepage = "https://rattailproject.org"
|
||||
Repository = "https://forgejo.wuttaproject.org/rattail/tailbone"
|
||||
Issues = "https://forgejo.wuttaproject.org/rattail/tailbone/issues"
|
||||
Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone/src/branch/master/CHANGELOG.md"
|
||||
Repository = "https://kallithea.rattailproject.org/rattail-project/tailbone"
|
||||
Issues = "https://redmine.rattailproject.org/projects/tailbone/issues"
|
||||
Changelog = "https://kallithea.rattailproject.org/rattail-project/tailbone/files/master/CHANGELOG.md"
|
||||
|
||||
|
||||
[tool.commitizen]
|
||||
|
|
|
@ -29,7 +29,8 @@ import logging
|
|||
import humanize
|
||||
import sqlalchemy as sa
|
||||
|
||||
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
|
||||
from rattail.db import model
|
||||
from rattail.util import pretty_quantity
|
||||
|
||||
from cornice import Service
|
||||
from deform import widget as dfwidget
|
||||
|
@ -44,7 +45,7 @@ log = logging.getLogger(__name__)
|
|||
|
||||
class ReceivingBatchViews(APIBatchView):
|
||||
|
||||
model_class = PurchaseBatch
|
||||
model_class = model.PurchaseBatch
|
||||
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
||||
route_prefix = 'receivingbatchviews'
|
||||
permission_prefix = 'receiving'
|
||||
|
@ -54,8 +55,7 @@ class ReceivingBatchViews(APIBatchView):
|
|||
supports_execute = True
|
||||
|
||||
def base_query(self):
|
||||
model = self.app.model
|
||||
query = super().base_query()
|
||||
query = super(ReceivingBatchViews, self).base_query()
|
||||
query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING)
|
||||
return query
|
||||
|
||||
|
@ -85,7 +85,7 @@ class ReceivingBatchViews(APIBatchView):
|
|||
|
||||
# assume "receive from PO" if given a PO key
|
||||
if data.get('purchase_key'):
|
||||
data['workflow'] = 'from_po'
|
||||
data['receiving_workflow'] = 'from_po'
|
||||
|
||||
return super().create_object(data)
|
||||
|
||||
|
@ -120,7 +120,6 @@ class ReceivingBatchViews(APIBatchView):
|
|||
return self._get(obj=batch)
|
||||
|
||||
def eligible_purchases(self):
|
||||
model = self.app.model
|
||||
uuid = self.request.params.get('vendor_uuid')
|
||||
vendor = self.Session.get(model.Vendor, uuid) if uuid else None
|
||||
if not vendor:
|
||||
|
@ -177,7 +176,7 @@ class ReceivingBatchViews(APIBatchView):
|
|||
|
||||
class ReceivingBatchRowViews(APIBatchRowView):
|
||||
|
||||
model_class = PurchaseBatchRow
|
||||
model_class = model.PurchaseBatchRow
|
||||
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
||||
route_prefix = 'receiving.rows'
|
||||
permission_prefix = 'receiving'
|
||||
|
@ -186,8 +185,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
|||
supports_quick_entry = True
|
||||
|
||||
def make_filter_spec(self):
|
||||
model = self.app.model
|
||||
filters = super().make_filter_spec()
|
||||
filters = super(ReceivingBatchRowViews, self).make_filter_spec()
|
||||
if filters:
|
||||
|
||||
# must translate certain convenience filters
|
||||
|
@ -298,11 +296,11 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
|||
return filters
|
||||
|
||||
def normalize(self, row):
|
||||
data = super().normalize(row)
|
||||
model = self.app.model
|
||||
data = super(ReceivingBatchRowViews, self).normalize(row)
|
||||
|
||||
batch = row.batch
|
||||
prodder = self.app.get_products_handler()
|
||||
app = self.get_rattail_app()
|
||||
prodder = app.get_products_handler()
|
||||
|
||||
data['product_uuid'] = row.product_uuid
|
||||
data['item_id'] = row.item_id
|
||||
|
@ -377,7 +375,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
|||
if accounted_for:
|
||||
# some product accounted for; button should receive "remainder" only
|
||||
if remainder:
|
||||
remainder = self.app.render_quantity(remainder)
|
||||
remainder = pretty_quantity(remainder)
|
||||
data['quick_receive_quantity'] = remainder
|
||||
data['quick_receive_text'] = "Receive Remainder ({} {})".format(
|
||||
remainder, data['unit_uom'])
|
||||
|
@ -388,7 +386,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
|||
else: # nothing yet accounted for, button should receive "all"
|
||||
if not remainder:
|
||||
log.warning("quick receive remainder is empty for row %s", row.uuid)
|
||||
remainder = self.app.render_quantity(remainder)
|
||||
remainder = pretty_quantity(remainder)
|
||||
data['quick_receive_quantity'] = remainder
|
||||
data['quick_receive_text'] = "Receive ALL ({} {})".format(
|
||||
remainder, data['unit_uom'])
|
||||
|
@ -416,7 +414,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
|||
data['received_alert'] = None
|
||||
if self.batch_handler.get_units_confirmed(row):
|
||||
msg = "You have already received some of this product; last update was {}.".format(
|
||||
humanize.naturaltime(self.app.make_utc() - row.modified))
|
||||
humanize.naturaltime(app.make_utc() - row.modified))
|
||||
data['received_alert'] = msg
|
||||
|
||||
return data
|
||||
|
@ -425,8 +423,6 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
|||
"""
|
||||
View which handles "receiving" against a particular batch row.
|
||||
"""
|
||||
model = self.app.model
|
||||
|
||||
# first do basic input validation
|
||||
schema = ReceiveRow().bind(session=self.Session())
|
||||
form = forms.Form(schema=schema, request=self.request)
|
||||
|
|
|
@ -26,6 +26,7 @@ Tailbone Web API - Master View
|
|||
|
||||
import json
|
||||
|
||||
from rattail.config import parse_bool
|
||||
from rattail.db.util import get_fieldnames
|
||||
|
||||
from cornice import resource, Service
|
||||
|
@ -184,7 +185,7 @@ class APIMasterView(APIView):
|
|||
if sortcol:
|
||||
spec = {
|
||||
'field': sortcol.field_name,
|
||||
'direction': 'asc' if self.config.parse_bool(self.request.params['ascending']) else 'desc',
|
||||
'direction': 'asc' if parse_bool(self.request.params['ascending']) else 'desc',
|
||||
}
|
||||
if sortcol.model_name:
|
||||
spec['model'] = sortcol.model_name
|
||||
|
|
|
@ -62,17 +62,6 @@ def make_rattail_config(settings):
|
|||
# nb. this is for compaibility with wuttaweb
|
||||
settings['wutta_config'] = rattail_config
|
||||
|
||||
# must import all sqlalchemy models before things get rolling,
|
||||
# otherwise can have errors about continuum TransactionMeta class
|
||||
# not yet mapped, when relevant pages are first requested...
|
||||
# cf. https://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/database/sqlalchemy.html#importing-all-sqlalchemy-models
|
||||
# hat tip to https://stackoverflow.com/a/59241485
|
||||
if getattr(rattail_config, 'tempmon_engine', None):
|
||||
from rattail_tempmon.db import model as tempmon_model, Session as TempmonSession
|
||||
tempmon_session = TempmonSession()
|
||||
tempmon_session.query(tempmon_model.Appliance).first()
|
||||
tempmon_session.close()
|
||||
|
||||
# configure database sessions
|
||||
if hasattr(rattail_config, 'appdb_engine'):
|
||||
tailbone.db.Session.configure(bind=rattail_config.appdb_engine)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2024 Lance Edgar
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -270,21 +270,9 @@ class VersionDiff(Diff):
|
|||
for field in self.fields:
|
||||
values[field] = {'before': self.render_old_value(field),
|
||||
'after': self.render_new_value(field)}
|
||||
|
||||
operation = None
|
||||
if self.version.operation_type == continuum.Operation.INSERT:
|
||||
operation = 'INSERT'
|
||||
elif self.version.operation_type == continuum.Operation.UPDATE:
|
||||
operation = 'UPDATE'
|
||||
elif self.version.operation_type == continuum.Operation.DELETE:
|
||||
operation = 'DELETE'
|
||||
else:
|
||||
operation = self.version.operation_type
|
||||
|
||||
return {
|
||||
'key': id(self.version),
|
||||
'model_title': self.title,
|
||||
'operation': operation,
|
||||
'diff_class': self.nature,
|
||||
'fields': self.fields,
|
||||
'values': values,
|
||||
|
|
|
@ -401,8 +401,6 @@ class Form(object):
|
|||
self.edit_help_url = edit_help_url
|
||||
self.route_prefix = route_prefix
|
||||
|
||||
self.button_icon_submit = kwargs.get('button_icon_submit', 'save')
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.fields)
|
||||
|
||||
|
@ -1039,9 +1037,9 @@ class Form(object):
|
|||
|
||||
def render_vue_tag(self, **kwargs):
|
||||
""" """
|
||||
return self.render_vuejs_component(**kwargs)
|
||||
return self.render_vuejs_component()
|
||||
|
||||
def render_vuejs_component(self, **kwargs):
|
||||
def render_vuejs_component(self):
|
||||
"""
|
||||
Render the Vue.js component HTML for the form.
|
||||
|
||||
|
@ -1052,11 +1050,10 @@ class Form(object):
|
|||
<tailbone-form :configure-fields-help="configureFieldsHelp">
|
||||
</tailbone-form>
|
||||
"""
|
||||
kw = dict(self.vuejs_component_kwargs)
|
||||
kw.update(kwargs)
|
||||
kwargs = dict(self.vuejs_component_kwargs)
|
||||
if self.can_edit_help:
|
||||
kw.setdefault(':configure-fields-help', 'configureFieldsHelp')
|
||||
return HTML.tag(self.vue_tagname, **kw)
|
||||
kwargs.setdefault(':configure-fields-help', 'configureFieldsHelp')
|
||||
return HTML.tag(self.vue_tagname, **kwargs)
|
||||
|
||||
def set_json_data(self, key, value):
|
||||
"""
|
||||
|
@ -1383,11 +1380,7 @@ class Form(object):
|
|||
return getattr(record, field_name)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return record[field_name]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# TODO: is this always safe to do?
|
||||
elif self.defaults and field_name in self.defaults:
|
||||
|
|
|
@ -24,10 +24,9 @@
|
|||
Core Grid Classes
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
import warnings
|
||||
from urllib.parse import urlencode
|
||||
import warnings
|
||||
import logging
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
@ -197,7 +196,11 @@ class Grid(WuttaGrid):
|
|||
raw_renderers={},
|
||||
extra_row_class=None,
|
||||
url='#',
|
||||
joiners={},
|
||||
filterable=False,
|
||||
filters={},
|
||||
use_byte_string_filters=False,
|
||||
searchable={},
|
||||
checkboxes=False,
|
||||
checked=None,
|
||||
check_handler=None,
|
||||
|
@ -235,7 +238,7 @@ class Grid(WuttaGrid):
|
|||
|
||||
if 'pageable' in kwargs:
|
||||
warnings.warn("pageable param is deprecated for Grid(); "
|
||||
"please use paginated param instead",
|
||||
"please use vue_tagname param instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
kwargs.setdefault('paginated', kwargs.pop('pageable'))
|
||||
|
||||
|
@ -251,21 +254,10 @@ class Grid(WuttaGrid):
|
|||
DeprecationWarning, stacklevel=2)
|
||||
kwargs.setdefault('page', kwargs.pop('default_page'))
|
||||
|
||||
if 'searchable' in kwargs:
|
||||
warnings.warn("searchable param is deprecated for Grid(); "
|
||||
"please use searchable_columns param instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
kwargs.setdefault('searchable_columns', kwargs.pop('searchable'))
|
||||
|
||||
# TODO: this should not be needed once all templates correctly
|
||||
# reference grid.vue_component etc.
|
||||
kwargs.setdefault('vue_tagname', 'tailbone-grid')
|
||||
|
||||
# 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['data'] = data
|
||||
super().__init__(request, **kwargs)
|
||||
|
@ -283,11 +275,19 @@ class Grid(WuttaGrid):
|
|||
|
||||
self.width = width
|
||||
self.enums = enums or {}
|
||||
self.assume_local_times = assume_local_times
|
||||
self.renderers = self.make_default_renderers(self.renderers)
|
||||
self.raw_renderers = raw_renderers or {}
|
||||
self.invisible = invisible or []
|
||||
self.extra_row_class = extra_row_class
|
||||
self.url = url
|
||||
self.joiners = joiners or {}
|
||||
|
||||
self.filterable = filterable
|
||||
self.use_byte_string_filters = use_byte_string_filters
|
||||
self.filters = self.make_filters(filters)
|
||||
|
||||
self.searchable = searchable or {}
|
||||
|
||||
self.checkboxes = checkboxes
|
||||
self.checked = checked
|
||||
|
@ -443,14 +443,10 @@ class Grid(WuttaGrid):
|
|||
self.remove(oldfield)
|
||||
|
||||
def set_joiner(self, key, joiner):
|
||||
""" """
|
||||
if joiner is None:
|
||||
warnings.warn("specifying None is deprecated for Grid.set_joiner(); "
|
||||
"please use Grid.remove_joiner() instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
self.remove_joiner(key)
|
||||
self.joiners.pop(key, None)
|
||||
else:
|
||||
super().set_joiner(key, joiner)
|
||||
self.joiners[key] = joiner
|
||||
|
||||
def set_sorter(self, key, *args, **kwargs):
|
||||
""" """
|
||||
|
@ -478,20 +474,42 @@ class Grid(WuttaGrid):
|
|||
self.sorters[key] = self.make_sorter(*args, **kwargs)
|
||||
|
||||
def set_filter(self, key, *args, **kwargs):
|
||||
""" """
|
||||
if len(args) == 1:
|
||||
if args[0] is None:
|
||||
warnings.warn("specifying None is deprecated for Grid.set_filter(); "
|
||||
"please use Grid.remove_filter() instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
if len(args) == 1 and args[0] is None:
|
||||
self.remove_filter(key)
|
||||
return
|
||||
|
||||
# TODO: our make_filter() signature differs from upstream,
|
||||
# so must call it explicitly instead of delegating to super
|
||||
kwargs.setdefault('label', self.get_label(key))
|
||||
else:
|
||||
if 'label' not in kwargs and key in self.labels:
|
||||
kwargs['label'] = self.labels[key]
|
||||
self.filters[key] = self.make_filter(key, *args, **kwargs)
|
||||
|
||||
def set_searchable(self, key, searchable=True):
|
||||
if searchable:
|
||||
self.searchable[key] = True
|
||||
else:
|
||||
self.searchable.pop(key, None)
|
||||
|
||||
def is_searchable(self, key):
|
||||
return self.searchable.get(key, False)
|
||||
|
||||
def remove_filter(self, key):
|
||||
self.filters.pop(key, None)
|
||||
|
||||
def set_label(self, key, label, column_only=False):
|
||||
"""
|
||||
Set/override the label for a column.
|
||||
|
||||
This overrides
|
||||
:meth:`~wuttaweb:wuttaweb.grids.base.Grid.set_label()` to add
|
||||
the following params:
|
||||
|
||||
:param column_only: Boolean indicating whether the label
|
||||
should be applied *only* to the column header (if
|
||||
``True``), vs. applying also to the filter (if ``False``).
|
||||
"""
|
||||
super().set_label(key, label)
|
||||
|
||||
if not column_only and key in self.filters:
|
||||
self.filters[key].label = label
|
||||
|
||||
def set_click_handler(self, key, handler):
|
||||
if handler:
|
||||
self.click_handlers[key] = handler
|
||||
|
@ -575,11 +593,7 @@ class Grid(WuttaGrid):
|
|||
return getattr(obj, column_name)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return obj[column_name]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def render_currency(self, obj, column_name):
|
||||
value = self.obtain_value(obj, column_name)
|
||||
|
@ -694,14 +708,6 @@ class Grid(WuttaGrid):
|
|||
def actions_column_format(self, column_number, row_number, item):
|
||||
return HTML.td(self.render_actions(item, row_number), class_='actions')
|
||||
|
||||
# TODO: upstream should handle this..
|
||||
def make_backend_filters(self, filters=None):
|
||||
""" """
|
||||
final = self.get_default_filters()
|
||||
if filters:
|
||||
final.update(filters)
|
||||
return final
|
||||
|
||||
def get_default_filters(self):
|
||||
"""
|
||||
Returns the default set of filters provided by the grid.
|
||||
|
@ -726,6 +732,16 @@ class Grid(WuttaGrid):
|
|||
filters[prop.key] = self.make_filter(prop.key, column)
|
||||
return filters
|
||||
|
||||
def make_filters(self, filters=None):
|
||||
"""
|
||||
Returns an initial set of filters which will be available to the grid.
|
||||
The grid itself may or may not provide some default filters, and the
|
||||
``filters`` kwarg may contain additions and/or overrides.
|
||||
"""
|
||||
if filters:
|
||||
return filters
|
||||
return self.get_default_filters()
|
||||
|
||||
def make_filter(self, key, column, **kwargs):
|
||||
"""
|
||||
Make a filter suitable for use with the given column.
|
||||
|
@ -863,13 +879,9 @@ class Grid(WuttaGrid):
|
|||
settings['page'] = self.page
|
||||
if self.filterable:
|
||||
for filtr in self.iter_filters():
|
||||
defaults = self.filter_defaults.get(filtr.key, {})
|
||||
settings[f'filter.{filtr.key}.active'] = defaults.get('active',
|
||||
filtr.default_active)
|
||||
settings[f'filter.{filtr.key}.verb'] = defaults.get('verb',
|
||||
filtr.default_verb)
|
||||
settings[f'filter.{filtr.key}.value'] = defaults.get('value',
|
||||
filtr.default_value)
|
||||
settings['filter.{}.active'.format(filtr.key)] = filtr.default_active
|
||||
settings['filter.{}.verb'.format(filtr.key)] = filtr.default_verb
|
||||
settings['filter.{}.value'.format(filtr.key)] = filtr.default_value
|
||||
|
||||
# If user has default settings on file, apply those first.
|
||||
if self.user_has_defaults():
|
||||
|
@ -877,13 +889,13 @@ class Grid(WuttaGrid):
|
|||
|
||||
# If request contains instruction to reset to default filters, then we
|
||||
# can skip the rest of the request/session checks.
|
||||
if self.request.GET.get('reset-view'):
|
||||
if self.request.GET.get('reset-to-default-filters') == 'true':
|
||||
pass
|
||||
|
||||
# If request has filter settings, grab those, then grab sort/pager
|
||||
# settings from request or session.
|
||||
elif self.request_has_settings('filter'):
|
||||
self.update_filter_settings(settings, src='request')
|
||||
elif self.filterable and self.request_has_settings('filter'):
|
||||
self.update_filter_settings(settings, 'request')
|
||||
if self.request_has_settings('sort'):
|
||||
self.update_sort_settings(settings, src='request')
|
||||
else:
|
||||
|
@ -895,7 +907,7 @@ class Grid(WuttaGrid):
|
|||
# settings from request or session.
|
||||
elif self.request_has_settings('sort'):
|
||||
self.update_sort_settings(settings, src='request')
|
||||
self.update_filter_settings(settings, src='session')
|
||||
self.update_filter_settings(settings, 'session')
|
||||
self.update_page_settings(settings)
|
||||
|
||||
# NOTE: These next two are functionally equivalent, but are kept
|
||||
|
@ -905,12 +917,12 @@ class Grid(WuttaGrid):
|
|||
# grab those, then grab filter/sort settings from session.
|
||||
elif self.request_has_settings('page'):
|
||||
self.update_page_settings(settings)
|
||||
self.update_filter_settings(settings, src='session')
|
||||
self.update_filter_settings(settings, 'session')
|
||||
self.update_sort_settings(settings, src='session')
|
||||
|
||||
# If request has no settings, grab all from session.
|
||||
elif self.session_has_settings():
|
||||
self.update_filter_settings(settings, src='session')
|
||||
self.update_filter_settings(settings, 'session')
|
||||
self.update_sort_settings(settings, src='session')
|
||||
self.update_page_settings(settings)
|
||||
|
||||
|
@ -1050,11 +1062,18 @@ class Grid(WuttaGrid):
|
|||
merge('page', int)
|
||||
|
||||
def request_has_settings(self, type_):
|
||||
""" """
|
||||
if super().request_has_settings(type_):
|
||||
"""
|
||||
Determine if the current request (GET query string) contains any
|
||||
filter/sort settings for the grid.
|
||||
"""
|
||||
if type_ == 'filter':
|
||||
for filtr in self.iter_filters():
|
||||
if filtr.key in self.request.GET:
|
||||
return True
|
||||
if 'filter' in self.request.GET: # user may be applying empty filters
|
||||
return True
|
||||
|
||||
if type_ == 'sort':
|
||||
elif type_ == 'sort':
|
||||
|
||||
# TODO: remove this eventually, but some links in the wild
|
||||
# may still include these params, so leave it for now
|
||||
|
@ -1062,6 +1081,14 @@ class Grid(WuttaGrid):
|
|||
if key in self.request.GET:
|
||||
return True
|
||||
|
||||
if 'sort1key' in self.request.GET:
|
||||
return True
|
||||
|
||||
elif type_ == 'page':
|
||||
for key in ['pagesize', 'page']:
|
||||
if key in self.request.GET:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def session_has_settings(self):
|
||||
|
@ -1077,6 +1104,72 @@ class Grid(WuttaGrid):
|
|||
return any([key.startswith(f'{prefix}.filter')
|
||||
for key in self.request.session])
|
||||
|
||||
def update_filter_settings(self, settings, source):
|
||||
"""
|
||||
Updates a settings dictionary according to filter settings data found
|
||||
in either the GET query string, or session storage.
|
||||
|
||||
:param settings: Dictionary of initial settings, which is to be updated.
|
||||
|
||||
:param source: String identifying the source to consult for settings
|
||||
data. Must be one of: ``('request', 'session')``.
|
||||
"""
|
||||
if not self.filterable:
|
||||
return
|
||||
|
||||
for filtr in self.iter_filters():
|
||||
prefix = 'filter.{}'.format(filtr.key)
|
||||
|
||||
if source == 'request':
|
||||
# consider filter active if query string contains a value for it
|
||||
settings['{}.active'.format(prefix)] = filtr.key in self.request.GET
|
||||
settings['{}.verb'.format(prefix)] = self.get_setting(
|
||||
settings, f'{filtr.key}.verb', src='request', default='')
|
||||
settings['{}.value'.format(prefix)] = self.get_setting(
|
||||
settings, filtr.key, src='request', default='')
|
||||
|
||||
else: # source = session
|
||||
settings['{}.active'.format(prefix)] = self.get_setting(
|
||||
settings, f'{prefix}.active', src='session',
|
||||
normalize=lambda v: str(v).lower() == 'true', default=False)
|
||||
settings['{}.verb'.format(prefix)] = self.get_setting(
|
||||
settings, f'{prefix}.verb', src='session', default='')
|
||||
settings['{}.value'.format(prefix)] = self.get_setting(
|
||||
settings, f'{prefix}.value', src='session', default='')
|
||||
|
||||
def update_page_settings(self, settings):
|
||||
"""
|
||||
Updates a settings dictionary according to pager settings data found in
|
||||
either the GET query string, or session storage.
|
||||
|
||||
Note that due to how the actual pager functions, the effective settings
|
||||
will often come from *both* the request and session. This is so that
|
||||
e.g. the page size will remain constant (coming from the session) while
|
||||
the user jumps between pages (which only provides the single setting).
|
||||
|
||||
:param settings: Dictionary of initial settings, which is to be updated.
|
||||
"""
|
||||
if not self.paginated:
|
||||
return
|
||||
|
||||
pagesize = self.request.GET.get('pagesize')
|
||||
if pagesize is not None:
|
||||
if pagesize.isdigit():
|
||||
settings['pagesize'] = int(pagesize)
|
||||
else:
|
||||
pagesize = self.request.session.get('grid.{}.pagesize'.format(self.key))
|
||||
if pagesize is not None:
|
||||
settings['pagesize'] = pagesize
|
||||
|
||||
page = self.request.GET.get('page')
|
||||
if page is not None:
|
||||
if page.isdigit():
|
||||
settings['page'] = int(page)
|
||||
else:
|
||||
page = self.request.session.get('grid.{}.page'.format(self.key))
|
||||
if page is not None:
|
||||
settings['page'] = int(page)
|
||||
|
||||
def persist_settings(self, settings, dest='session'):
|
||||
""" """
|
||||
if dest not in ('defaults', 'session'):
|
||||
|
@ -1164,12 +1257,89 @@ class Grid(WuttaGrid):
|
|||
|
||||
return data
|
||||
|
||||
def make_visible_data(self):
|
||||
def sort_data(self, data, sorters=None):
|
||||
""" """
|
||||
warnings.warn("grid.make_visible_data() method is deprecated; "
|
||||
"please use grid.get_visible_data() instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return self.get_visible_data()
|
||||
if sorters is None:
|
||||
sorters = self.active_sorters
|
||||
if not sorters:
|
||||
return data
|
||||
|
||||
# nb. when data is a query, we want to apply sorters in the
|
||||
# requested order, so the final query has order_by() in the
|
||||
# correct "as-is" sequence. however when data is a list we
|
||||
# must do the opposite, applying in the reverse order, so the
|
||||
# final list has the most "important" sort(s) applied last.
|
||||
if not isinstance(data, orm.Query):
|
||||
sorters = reversed(sorters)
|
||||
|
||||
for sorter in sorters:
|
||||
sortkey = sorter['key']
|
||||
sortdir = sorter['dir']
|
||||
|
||||
# cannot sort unless we have a sorter callable
|
||||
sortfunc = self.sorters.get(sortkey)
|
||||
if not sortfunc:
|
||||
return data
|
||||
|
||||
# join appropriate model if needed
|
||||
if sortkey in self.joiners and sortkey not in self.joined:
|
||||
data = self.joiners[sortkey](data)
|
||||
self.joined.add(sortkey)
|
||||
|
||||
# invoke the sorter
|
||||
data = sortfunc(data, sortdir)
|
||||
|
||||
return data
|
||||
|
||||
def paginate_data(self, data):
|
||||
"""
|
||||
Paginate the given data set according to current settings, and return
|
||||
the result.
|
||||
"""
|
||||
# we of course assume our current page is correct, at first
|
||||
pager = self.make_pager(data)
|
||||
|
||||
# if pager has detected that our current page is outside the valid
|
||||
# range, we must re-orient ourself around the "new" (valid) page
|
||||
if pager.page != self.page:
|
||||
self.page = pager.page
|
||||
self.request.session['grid.{}.page'.format(self.key)] = self.page
|
||||
pager = self.make_pager(data)
|
||||
|
||||
return pager
|
||||
|
||||
def make_pager(self, data):
|
||||
|
||||
# TODO: this seems hacky..normally we expect `data` to be a
|
||||
# query of course, but in some cases it may be a list instead.
|
||||
# if so then we can't use ORM pager
|
||||
if isinstance(data, list):
|
||||
import paginate
|
||||
return paginate.Page(data,
|
||||
items_per_page=self.pagesize,
|
||||
page=self.page)
|
||||
|
||||
return SqlalchemyOrmPage(data,
|
||||
items_per_page=self.pagesize,
|
||||
page=self.page,
|
||||
url_maker=URLMaker(self.request))
|
||||
|
||||
def make_visible_data(self):
|
||||
"""
|
||||
Apply various settings to the raw data set, to produce a final data
|
||||
set. This will page / sort / filter as necessary, according to the
|
||||
grid's defaults and the current request etc.
|
||||
"""
|
||||
self.joined = set()
|
||||
data = self.data
|
||||
if self.filterable:
|
||||
data = self.filter_data(data)
|
||||
if self.sortable:
|
||||
data = self.sort_data(data)
|
||||
if self.paginated:
|
||||
self.pager = self.paginate_data(data)
|
||||
data = self.pager
|
||||
return data
|
||||
|
||||
def render_vue_tag(self, master=None, **kwargs):
|
||||
""" """
|
||||
|
@ -1192,7 +1362,7 @@ class Grid(WuttaGrid):
|
|||
includes the context menu items and grid tools.
|
||||
"""
|
||||
if 'grid_columns' not in kwargs:
|
||||
kwargs['grid_columns'] = self.get_vue_columns()
|
||||
kwargs['grid_columns'] = self.get_table_columns()
|
||||
|
||||
if 'grid_data' not in kwargs:
|
||||
kwargs['grid_data'] = self.get_table_data()
|
||||
|
@ -1215,7 +1385,6 @@ class Grid(WuttaGrid):
|
|||
return HTML.literal(html)
|
||||
|
||||
def render_buefy(self, **kwargs):
|
||||
""" """
|
||||
warnings.warn("Grid.render_buefy() is deprecated; "
|
||||
"please use Grid.render_complete() instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
@ -1223,7 +1392,6 @@ class Grid(WuttaGrid):
|
|||
|
||||
def render_table_element(self, template='/grids/b-table.mako',
|
||||
data_prop='gridData', empty_labels=False,
|
||||
literal=False,
|
||||
**kwargs):
|
||||
"""
|
||||
This is intended for ad-hoc "small" grids with static data. Renders
|
||||
|
@ -1235,15 +1403,12 @@ class Grid(WuttaGrid):
|
|||
context['data_prop'] = data_prop
|
||||
context['empty_labels'] = empty_labels
|
||||
if 'grid_columns' not in context:
|
||||
context['grid_columns'] = self.get_vue_columns()
|
||||
context['grid_columns'] = self.get_table_columns()
|
||||
context.setdefault('paginated', False)
|
||||
if context['paginated']:
|
||||
context.setdefault('per_page', 20)
|
||||
context['view_click_handler'] = self.get_view_click_handler()
|
||||
result = render(template, context)
|
||||
if literal:
|
||||
result = HTML.literal(result)
|
||||
return result
|
||||
return render(template, context)
|
||||
|
||||
def get_view_click_handler(self):
|
||||
""" """
|
||||
|
@ -1252,7 +1417,7 @@ class Grid(WuttaGrid):
|
|||
view = None
|
||||
for action in self.actions:
|
||||
if action.key == 'view':
|
||||
return getattr(action, 'click_handler', None)
|
||||
return action.click_handler
|
||||
|
||||
def set_filters_sequence(self, filters, only=False):
|
||||
"""
|
||||
|
@ -1326,6 +1491,28 @@ class Grid(WuttaGrid):
|
|||
|
||||
return data
|
||||
|
||||
def render_filters(self, template='/grids/filters.mako', **kwargs):
|
||||
"""
|
||||
Render the filters to a Unicode string, using the specified template.
|
||||
Additional kwargs are passed along as context to the template.
|
||||
"""
|
||||
# Provide default data to filters form, so renderer can do some of the
|
||||
# work for us.
|
||||
data = {}
|
||||
for filtr in self.iter_active_filters():
|
||||
data['{}.active'.format(filtr.key)] = filtr.active
|
||||
data['{}.verb'.format(filtr.key)] = filtr.verb
|
||||
data[filtr.key] = filtr.value
|
||||
|
||||
form = gridfilters.GridFiltersForm(self.filters,
|
||||
request=self.request,
|
||||
defaults=data)
|
||||
|
||||
kwargs['request'] = self.request
|
||||
kwargs['grid'] = self
|
||||
kwargs['form'] = form
|
||||
return render(template, kwargs)
|
||||
|
||||
def render_actions(self, row, i): # pragma: no cover
|
||||
""" """
|
||||
warnings.warn("grid.render_actions() is deprecated!",
|
||||
|
@ -1387,19 +1574,22 @@ class Grid(WuttaGrid):
|
|||
|
||||
def get_vue_columns(self):
|
||||
""" """
|
||||
columns = super().get_vue_columns()
|
||||
|
||||
for column in columns:
|
||||
column['visible'] = column['field'] not in self.invisible
|
||||
|
||||
return columns
|
||||
return self.get_table_columns()
|
||||
|
||||
def get_table_columns(self):
|
||||
""" """
|
||||
warnings.warn("grid.get_table_columns() method is deprecated; "
|
||||
"please use grid.get_vue_columns() instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return self.get_vue_columns()
|
||||
"""
|
||||
Return a list of dicts representing all grid columns. Meant
|
||||
for use with the client-side JS table.
|
||||
"""
|
||||
columns = []
|
||||
for name in self.columns:
|
||||
columns.append({
|
||||
'field': name,
|
||||
'label': self.get_label(name),
|
||||
'sortable': self.is_sortable(name),
|
||||
'visible': name not in self.invisible,
|
||||
})
|
||||
return columns
|
||||
|
||||
def get_uuid_for_row(self, rowobj):
|
||||
|
||||
|
@ -1411,10 +1601,6 @@ class Grid(WuttaGrid):
|
|||
if hasattr(rowobj, 'uuid'):
|
||||
return rowobj.uuid
|
||||
|
||||
def get_vue_context(self):
|
||||
""" """
|
||||
return self.get_table_data()
|
||||
|
||||
def get_vue_data(self):
|
||||
""" """
|
||||
table_data = self.get_table_data()
|
||||
|
@ -1429,7 +1615,7 @@ class Grid(WuttaGrid):
|
|||
return self._table_data
|
||||
|
||||
# filter / sort / paginate to get "visible" data
|
||||
raw_data = self.get_visible_data()
|
||||
raw_data = self.make_visible_data()
|
||||
data = []
|
||||
status_map = {}
|
||||
checked = []
|
||||
|
@ -1470,22 +1656,10 @@ class Grid(WuttaGrid):
|
|||
|
||||
# leverage configured rendering logic where applicable;
|
||||
# otherwise use "raw" data value as string
|
||||
value = self.obtain_value(rowobj, name)
|
||||
if self.renderers and name in self.renderers:
|
||||
renderer = self.renderers[name]
|
||||
|
||||
# TODO: legacy renderer callables require 2 args,
|
||||
# but wuttaweb callables require 3 args
|
||||
sig = inspect.signature(renderer)
|
||||
required = [param for param in sig.parameters.values()
|
||||
if param.default == param.empty]
|
||||
|
||||
if len(required) == 2:
|
||||
# TODO: legacy renderer
|
||||
value = renderer(rowobj, name)
|
||||
else: # the future
|
||||
value = renderer(rowobj, name, value)
|
||||
|
||||
value = self.renderers[name](rowobj, name)
|
||||
else:
|
||||
value = self.obtain_value(rowobj, name)
|
||||
if value is None:
|
||||
value = ""
|
||||
|
||||
|
@ -1518,8 +1692,6 @@ class Grid(WuttaGrid):
|
|||
|
||||
results = {
|
||||
'data': data,
|
||||
'row_classes': status_map,
|
||||
# TODO: deprecate / remove this
|
||||
'row_status_map': status_map,
|
||||
}
|
||||
|
||||
|
@ -1548,11 +1720,6 @@ class Grid(WuttaGrid):
|
|||
self._table_data = results
|
||||
return self._table_data
|
||||
|
||||
# TODO: remove this when we use upstream GridAction
|
||||
def add_action(self, key, **kwargs):
|
||||
""" """
|
||||
self.actions.append(GridAction(self.request, key, **kwargs))
|
||||
|
||||
def set_action_urls(self, row, rowobj, i):
|
||||
"""
|
||||
Pre-generate all action URLs for the given data row. Meant for use
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2024 Lance Edgar
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -24,9 +24,6 @@
|
|||
Template Context Helpers
|
||||
"""
|
||||
|
||||
# start off with all from wuttaweb
|
||||
from wuttaweb.helpers import *
|
||||
|
||||
import os
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
|
@ -36,7 +33,12 @@ from rattail.time import localtime, make_utc
|
|||
from rattail.util import pretty_quantity, pretty_hours, hours_as_decimal
|
||||
from rattail.db.util import maxlen
|
||||
|
||||
from tailbone.util import (pretty_datetime, raw_datetime,
|
||||
from webhelpers2.html import *
|
||||
from webhelpers2.html.tags import *
|
||||
|
||||
from wuttaweb.util import get_liburl
|
||||
from tailbone.util import (csrf_token, get_csrf_token,
|
||||
pretty_datetime, raw_datetime,
|
||||
render_markdown,
|
||||
route_exists)
|
||||
|
||||
|
|
|
@ -394,11 +394,6 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
|||
'route': 'products',
|
||||
'perm': 'products.list',
|
||||
},
|
||||
{
|
||||
'title': "Product Costs",
|
||||
'route': 'product_costs',
|
||||
'perm': 'product_costs.list',
|
||||
},
|
||||
{
|
||||
'title': "Departments",
|
||||
'route': 'departments',
|
||||
|
@ -456,11 +451,6 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
|||
'route': 'vendors',
|
||||
'perm': 'vendors.list',
|
||||
},
|
||||
{
|
||||
'title': "Product Costs",
|
||||
'route': 'product_costs',
|
||||
'perm': 'product_costs.list',
|
||||
},
|
||||
{'type': 'sep'},
|
||||
{
|
||||
'title': "Ordering",
|
||||
|
@ -713,7 +703,7 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
|||
},
|
||||
{'type': 'sep'},
|
||||
{
|
||||
'title': "App Info",
|
||||
'title': "App Details",
|
||||
'route': 'appinfo',
|
||||
'perm': 'appinfo.list',
|
||||
},
|
||||
|
|
|
@ -1,2 +1,247 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="wuttaweb:templates/appinfo/configure.mako" />
|
||||
<%inherit file="/configure.mako" />
|
||||
|
||||
<%def name="form_content()">
|
||||
|
||||
<h3 class="block is-size-3">Basics</h3>
|
||||
<div class="block" style="padding-left: 2rem;">
|
||||
|
||||
<b-field grouped>
|
||||
|
||||
<b-field label="App Title">
|
||||
<b-input name="rattail.app_title"
|
||||
v-model="simpleSettings['rattail.app_title']"
|
||||
@input="settingsNeedSaved = true">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Node Type">
|
||||
## TODO: should be a dropdown, app handler defines choices
|
||||
<b-input name="rattail.node_type"
|
||||
v-model="simpleSettings['rattail.node_type']"
|
||||
@input="settingsNeedSaved = true">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Node Title">
|
||||
<b-input name="rattail.node_title"
|
||||
v-model="simpleSettings['rattail.node_title']"
|
||||
@input="settingsNeedSaved = true">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
</b-field>
|
||||
|
||||
<b-field>
|
||||
<b-checkbox name="rattail.production"
|
||||
v-model="simpleSettings['rattail.production']"
|
||||
native-value="true"
|
||||
@input="settingsNeedSaved = true">
|
||||
Production Mode
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<b-field>
|
||||
<b-checkbox name="rattail.running_from_source"
|
||||
v-model="simpleSettings['rattail.running_from_source']"
|
||||
native-value="true"
|
||||
@input="settingsNeedSaved = true">
|
||||
Running from Source
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="level-item">
|
||||
<b-field label="Top-Level Package" horizontal
|
||||
v-if="simpleSettings['rattail.running_from_source']">
|
||||
<b-input name="rattail.running_from_source.rootpkg"
|
||||
v-model="simpleSettings['rattail.running_from_source.rootpkg']"
|
||||
@input="settingsNeedSaved = true">
|
||||
</b-input>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h3 class="block is-size-3">Display</h3>
|
||||
<div class="block" style="padding-left: 2rem;">
|
||||
|
||||
<b-field grouped>
|
||||
|
||||
<b-field label="Background Color">
|
||||
<b-input name="tailbone.background_color"
|
||||
v-model="simpleSettings['tailbone.background_color']"
|
||||
@input="settingsNeedSaved = true">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
</b-field>
|
||||
|
||||
</div>
|
||||
|
||||
<h3 class="block is-size-3">Grids</h3>
|
||||
<div class="block" style="padding-left: 2rem;">
|
||||
|
||||
<b-field grouped>
|
||||
|
||||
<b-field label="Default Page Size">
|
||||
<b-input name="tailbone.grid.default_pagesize"
|
||||
v-model="simpleSettings['tailbone.grid.default_pagesize']"
|
||||
@input="settingsNeedSaved = true">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
</b-field>
|
||||
|
||||
</div>
|
||||
|
||||
<h3 class="block is-size-3">Web Libraries</h3>
|
||||
<div class="block" style="padding-left: 2rem;">
|
||||
|
||||
<${b}-table :data="weblibs">
|
||||
|
||||
<${b}-table-column field="title"
|
||||
label="Name"
|
||||
v-slot="props">
|
||||
{{ props.row.title }}
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column field="configured_version"
|
||||
label="Version"
|
||||
v-slot="props">
|
||||
{{ props.row.configured_version || props.row.default_version }}
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column field="configured_url"
|
||||
label="URL Override"
|
||||
v-slot="props">
|
||||
{{ props.row.configured_url }}
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column field="live_url"
|
||||
label="Effective (Live) URL"
|
||||
v-slot="props">
|
||||
<span v-if="props.row.modified"
|
||||
class="has-text-warning">
|
||||
save settings and refresh page to see new URL
|
||||
</span>
|
||||
<span v-if="!props.row.modified">
|
||||
{{ props.row.live_url }}
|
||||
</span>
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column field="actions"
|
||||
label="Actions"
|
||||
v-slot="props">
|
||||
<a href="#"
|
||||
@click.prevent="editWebLibraryInit(props.row)">
|
||||
% if request.use_oruga:
|
||||
<o-icon icon="edit" />
|
||||
% else:
|
||||
<i class="fas fa-edit"></i>
|
||||
% endif
|
||||
Edit
|
||||
</a>
|
||||
</${b}-table-column>
|
||||
|
||||
</${b}-table>
|
||||
|
||||
% for weblib in weblibs:
|
||||
${h.hidden('wuttaweb.libver.{}'.format(weblib['key']), **{':value': "simpleSettings['wuttaweb.libver.{}']".format(weblib['key'])})}
|
||||
${h.hidden('wuttaweb.liburl.{}'.format(weblib['key']), **{':value': "simpleSettings['wuttaweb.liburl.{}']".format(weblib['key'])})}
|
||||
% endfor
|
||||
|
||||
<${b}-modal has-modal-card
|
||||
% if request.use_oruga:
|
||||
v-model:active="editWebLibraryShowDialog"
|
||||
% else:
|
||||
:active.sync="editWebLibraryShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="modal-card">
|
||||
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Web Library: {{ editWebLibraryRecord.title }}</p>
|
||||
</header>
|
||||
|
||||
<section class="modal-card-body">
|
||||
|
||||
<b-field grouped>
|
||||
|
||||
<b-field label="Default Version">
|
||||
<b-input v-model="editWebLibraryRecord.default_version"
|
||||
disabled>
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Override Version">
|
||||
<b-input v-model="editWebLibraryVersion">
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
</b-field>
|
||||
|
||||
<b-field label="Override URL">
|
||||
<b-input v-model="editWebLibraryURL"
|
||||
expanded />
|
||||
</b-field>
|
||||
|
||||
<b-field label="Effective URL (as of last page load)">
|
||||
<b-input v-model="editWebLibraryRecord.live_url"
|
||||
disabled
|
||||
expanded />
|
||||
</b-field>
|
||||
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<b-button type="is-primary"
|
||||
@click="editWebLibrarySave()"
|
||||
icon-pack="fas"
|
||||
icon-left="save">
|
||||
Save
|
||||
</b-button>
|
||||
<b-button @click="editWebLibraryShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</${b}-modal>
|
||||
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
${parent.modify_vue_vars()}
|
||||
<script>
|
||||
|
||||
ThisPageData.weblibs = ${json.dumps(weblibs)|n}
|
||||
|
||||
ThisPageData.editWebLibraryShowDialog = false
|
||||
ThisPageData.editWebLibraryRecord = {}
|
||||
ThisPageData.editWebLibraryVersion = null
|
||||
ThisPageData.editWebLibraryURL = null
|
||||
|
||||
ThisPage.methods.editWebLibraryInit = function(row) {
|
||||
this.editWebLibraryRecord = row
|
||||
this.editWebLibraryVersion = row.configured_version
|
||||
this.editWebLibraryURL = row.configured_url
|
||||
this.editWebLibraryShowDialog = true
|
||||
}
|
||||
|
||||
ThisPage.methods.editWebLibrarySave = function() {
|
||||
this.editWebLibraryRecord.configured_version = this.editWebLibraryVersion
|
||||
this.editWebLibraryRecord.configured_url = this.editWebLibraryURL
|
||||
this.editWebLibraryRecord.modified = true
|
||||
|
||||
this.simpleSettings[`wuttaweb.libver.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryVersion
|
||||
this.simpleSettings[`wuttaweb.liburl.${'$'}{this.editWebLibraryRecord.key}`] = this.editWebLibraryURL
|
||||
|
||||
this.settingsNeedSaved = true
|
||||
this.editWebLibraryShowDialog = false
|
||||
}
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="wuttaweb:templates/appinfo/index.mako" />
|
||||
<%inherit file="/master/index.mako" />
|
||||
|
||||
<%def name="page_content()">
|
||||
|
||||
<div class="buttons">
|
||||
|
||||
<once-button type="is-primary"
|
||||
|
@ -27,5 +28,95 @@
|
|||
|
||||
</div>
|
||||
|
||||
${parent.page_content()}
|
||||
<${b}-collapse class="panel" open>
|
||||
|
||||
<template #trigger="props">
|
||||
<div class="panel-heading"
|
||||
style="cursor: pointer;"
|
||||
role="button">
|
||||
|
||||
## TODO: for some reason buefy will "reuse" the icon
|
||||
## element in such a way that its display does not
|
||||
## refresh. so to work around that, we use different
|
||||
## structure for the two icons, so buefy is forced to
|
||||
## re-draw
|
||||
|
||||
<b-icon v-if="props.open"
|
||||
pack="fas"
|
||||
icon="angle-down">
|
||||
</b-icon>
|
||||
|
||||
<span v-if="!props.open">
|
||||
<b-icon pack="fas"
|
||||
icon="angle-right">
|
||||
</b-icon>
|
||||
</span>
|
||||
|
||||
<span>Configuration Files</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="panel-block">
|
||||
<div style="width: 100%;">
|
||||
<${b}-table :data="configFiles">
|
||||
|
||||
<${b}-table-column field="priority"
|
||||
label="Priority"
|
||||
v-slot="props">
|
||||
{{ props.row.priority }}
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column field="path"
|
||||
label="File Path"
|
||||
v-slot="props">
|
||||
{{ props.row.path }}
|
||||
</${b}-table-column>
|
||||
|
||||
</${b}-table>
|
||||
</div>
|
||||
</div>
|
||||
</${b}-collapse>
|
||||
|
||||
<${b}-collapse class="panel"
|
||||
:open="false">
|
||||
|
||||
<template #trigger="props">
|
||||
<div class="panel-heading"
|
||||
style="cursor: pointer;"
|
||||
role="button">
|
||||
|
||||
## TODO: for some reason buefy will "reuse" the icon
|
||||
## element in such a way that its display does not
|
||||
## refresh. so to work around that, we use different
|
||||
## structure for the two icons, so buefy is forced to
|
||||
## re-draw
|
||||
|
||||
<b-icon v-if="props.open"
|
||||
pack="fas"
|
||||
icon="angle-down">
|
||||
</b-icon>
|
||||
|
||||
<span v-if="!props.open">
|
||||
<b-icon pack="fas"
|
||||
icon="angle-right">
|
||||
</b-icon>
|
||||
</span>
|
||||
|
||||
<strong>Installed Packages</strong>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="panel-block">
|
||||
<div style="width: 100%;">
|
||||
${grid.render_vue_tag()}
|
||||
</div>
|
||||
</div>
|
||||
</${b}-collapse>
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
${parent.modify_vue_vars()}
|
||||
<script>
|
||||
ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(request.rattail_config.prioritized_files, 1)])|n}
|
||||
</script>
|
||||
</%def>
|
||||
|
|
|
@ -632,23 +632,9 @@
|
|||
% endif
|
||||
<div class="navbar-dropdown">
|
||||
% if request.is_root:
|
||||
${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()}
|
||||
${h.link_to("Stop being root", url('stop_root'), class_='navbar-item root-user')}
|
||||
% elif request.is_admin:
|
||||
${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()}
|
||||
${h.link_to("Become root", url('become_root'), class_='navbar-item root-user')}
|
||||
% endif
|
||||
% if messaging_enabled:
|
||||
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
|
||||
|
@ -656,11 +642,7 @@
|
|||
% if request.is_root or not request.user.prevent_password_change:
|
||||
${h.link_to("Change Password", url('change_password'), class_='navbar-item')}
|
||||
% endif
|
||||
% try:
|
||||
## nb. does not exist yet for wuttaweb
|
||||
${h.link_to("Edit Preferences", url('my.preferences'), class_='navbar-item')}
|
||||
% except:
|
||||
% endtry
|
||||
${h.link_to("Logout", url('logout'), class_='navbar-item')}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -686,7 +668,7 @@
|
|||
text="Edit This">
|
||||
</once-button>
|
||||
% endif
|
||||
% if getattr(master, 'cloneable', False) and not master.cloning and master.has_perm('clone'):
|
||||
% if getattr(master, 'cloneable', False) and master.has_perm('clone'):
|
||||
<once-button tag="a" href="${master.get_action_url('clone', instance)}"
|
||||
icon-left="object-ungroup"
|
||||
text="Clone This">
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="wuttaweb:templates/base_meta.mako" />
|
||||
|
||||
<%def name="app_title()">${app.get_node_title()}</%def>
|
||||
<%def name="app_title()">${rattail_app.get_node_title()}</%def>
|
||||
|
||||
<%def name="global_title()">${"[STAGE] " if not request.rattail_config.production() else ''}${self.app_title()}</%def>
|
||||
|
||||
<%def name="extra_styles()"></%def>
|
||||
|
||||
<%def name="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'))}" />
|
||||
|
@ -10,3 +13,9 @@
|
|||
<%def name="header_logo()">
|
||||
${h.image(request.rattail_config.get('tailbone', 'header_image_url', default=request.static_url('tailbone:static/img/rattail.ico')), "Header Logo", style="height: 49px;")}
|
||||
</%def>
|
||||
|
||||
<%def name="footer()">
|
||||
<p class="has-text-centered">
|
||||
powered by ${h.link_to("Rattail", url('about'))}
|
||||
</p>
|
||||
</%def>
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
<b-select name="${tmpl['setting_file']}"
|
||||
v-model="inputFileTemplateSettings['${tmpl['setting_file']}']"
|
||||
@input="settingsNeedSaved = true">
|
||||
<option value="">-new-</option>
|
||||
<option :value="null">-new-</option>
|
||||
<option v-for="option in inputFileTemplateFileOptions['${tmpl['key']}']"
|
||||
:key="option"
|
||||
:value="option">
|
||||
|
@ -104,23 +104,6 @@
|
|||
<b-field label="Upload"
|
||||
v-show="inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !inputFileTemplateSettings['${tmpl['setting_file']}']">
|
||||
|
||||
% if request.use_oruga:
|
||||
<o-field class="file">
|
||||
<o-upload name="${tmpl['setting_file']}.upload"
|
||||
v-model="inputFileTemplateUploads['${tmpl['key']}']"
|
||||
v-slot="{ onclick }"
|
||||
@input="settingsNeedSaved = true">
|
||||
<o-button variant="primary"
|
||||
@click="onclick">
|
||||
<o-icon icon="upload" />
|
||||
<span>Click to upload</span>
|
||||
</o-button>
|
||||
<span class="file-name" v-if="inputFileTemplateUploads['${tmpl['key']}']">
|
||||
{{ inputFileTemplateUploads['${tmpl['key']}'].name }}
|
||||
</span>
|
||||
</o-upload>
|
||||
</o-field>
|
||||
% else:
|
||||
<b-field class="file is-primary"
|
||||
:class="{'has-name': !!inputFileTemplateSettings['${tmpl['setting_file']}']}">
|
||||
<b-upload name="${tmpl['setting_file']}.upload"
|
||||
|
@ -137,7 +120,6 @@
|
|||
{{ inputFileTemplateUploads['${tmpl['key']}'].name }}
|
||||
</span>
|
||||
</b-field>
|
||||
% endif
|
||||
|
||||
</b-field>
|
||||
|
||||
|
@ -161,85 +143,6 @@
|
|||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="output_file_template_field(key)">
|
||||
<% tmpl = output_file_templates[key] %>
|
||||
<b-field grouped>
|
||||
|
||||
<b-field label="${tmpl['label']}">
|
||||
<b-select name="${tmpl['setting_mode']}"
|
||||
v-model="outputFileTemplateSettings['${tmpl['setting_mode']}']"
|
||||
@input="settingsNeedSaved = true">
|
||||
<option value="default">use default</option>
|
||||
<option value="hosted">use uploaded file</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field label="File"
|
||||
v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted'"
|
||||
:message="outputFileTemplateSettings['${tmpl['setting_file']}'] ? 'This file lives on disk at: ${output_file_option_dirs[tmpl['key']]}' : null">
|
||||
<b-select name="${tmpl['setting_file']}"
|
||||
v-model="outputFileTemplateSettings['${tmpl['setting_file']}']"
|
||||
@input="settingsNeedSaved = true">
|
||||
<option value="">-new-</option>
|
||||
<option v-for="option in outputFileTemplateFileOptions['${tmpl['key']}']"
|
||||
:key="option"
|
||||
:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Upload"
|
||||
v-show="outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted' && !outputFileTemplateSettings['${tmpl['setting_file']}']">
|
||||
|
||||
% if request.use_oruga:
|
||||
<o-field class="file">
|
||||
<o-upload name="${tmpl['setting_file']}.upload"
|
||||
v-model="outputFileTemplateUploads['${tmpl['key']}']"
|
||||
v-slot="{ onclick }"
|
||||
@input="settingsNeedSaved = true">
|
||||
<o-button variant="primary"
|
||||
@click="onclick">
|
||||
<o-icon icon="upload" />
|
||||
<span>Click to upload</span>
|
||||
</o-button>
|
||||
<span class="file-name" v-if="outputFileTemplateUploads['${tmpl['key']}']">
|
||||
{{ outputFileTemplateUploads['${tmpl['key']}'].name }}
|
||||
</span>
|
||||
</o-upload>
|
||||
</o-field>
|
||||
% else:
|
||||
<b-field class="file is-primary"
|
||||
:class="{'has-name': !!outputFileTemplateSettings['${tmpl['setting_file']}']}">
|
||||
<b-upload name="${tmpl['setting_file']}.upload"
|
||||
v-model="outputFileTemplateUploads['${tmpl['key']}']"
|
||||
class="file-label"
|
||||
@input="settingsNeedSaved = true">
|
||||
<span class="file-cta">
|
||||
<b-icon class="file-icon" pack="fas" icon="upload"></b-icon>
|
||||
<span class="file-label">Click to upload</span>
|
||||
</span>
|
||||
</b-upload>
|
||||
<span v-if="outputFileTemplateUploads['${tmpl['key']}']"
|
||||
class="file-name">
|
||||
{{ outputFileTemplateUploads['${tmpl['key']}'].name }}
|
||||
</span>
|
||||
</b-field>
|
||||
% endif
|
||||
</b-field>
|
||||
|
||||
</b-field>
|
||||
</%def>
|
||||
|
||||
<%def name="output_file_templates_section()">
|
||||
<h3 class="block is-size-3">Output File Templates</h3>
|
||||
<div class="block" style="padding-left: 2rem;">
|
||||
% for key in output_file_templates:
|
||||
${self.output_file_template_field(key)}
|
||||
% endfor
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="form_content()"></%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
|
@ -280,14 +183,15 @@
|
|||
<b-button @click="purgeSettingsShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
${h.form(request.current_route_url(), **{'@submit': 'purgingSettings = true'})}
|
||||
${h.form(request.current_route_url())}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('remove_settings', 'true')}
|
||||
<b-button type="is-danger"
|
||||
native-type="submit"
|
||||
:disabled="purgingSettings"
|
||||
icon-pack="fas"
|
||||
icon-left="trash">
|
||||
icon-left="trash"
|
||||
@click="purgingSettings = true">
|
||||
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }}
|
||||
</b-button>
|
||||
${h.end_form()}
|
||||
|
@ -309,34 +213,54 @@
|
|||
ThisPageData.simpleSettings = ${json.dumps(simple_settings)|n}
|
||||
% endif
|
||||
|
||||
% if input_file_template_settings is not Undefined:
|
||||
ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n}
|
||||
ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n}
|
||||
ThisPageData.inputFileTemplateUploads = {
|
||||
% for key in input_file_templates:
|
||||
'${key}': null,
|
||||
% endfor
|
||||
}
|
||||
% endif
|
||||
|
||||
ThisPageData.purgeSettingsShowDialog = false
|
||||
ThisPageData.purgingSettings = false
|
||||
|
||||
ThisPageData.settingsNeedSaved = false
|
||||
ThisPageData.undoChanges = false
|
||||
ThisPageData.savingSettings = false
|
||||
ThisPageData.validators = []
|
||||
|
||||
ThisPage.methods.purgeSettingsInit = function() {
|
||||
this.purgeSettingsShowDialog = true
|
||||
}
|
||||
|
||||
ThisPage.methods.validateSettings = function() {}
|
||||
% if input_file_template_settings is not Undefined:
|
||||
ThisPage.methods.validateInputFileTemplateSettings = function() {
|
||||
% for tmpl in input_file_templates.values():
|
||||
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
|
||||
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
|
||||
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
|
||||
return "You must provide a file to upload for the ${tmpl['label']} template."
|
||||
}
|
||||
}
|
||||
}
|
||||
% endfor
|
||||
}
|
||||
% endif
|
||||
|
||||
ThisPage.methods.saveSettings = function() {
|
||||
ThisPage.methods.validateSettings = function() {
|
||||
let msg
|
||||
|
||||
// nb. this is the future
|
||||
for (let validator of this.validators) {
|
||||
msg = validator.call(this)
|
||||
% if input_file_template_settings is not Undefined:
|
||||
msg = this.validateInputFileTemplateSettings()
|
||||
if (msg) {
|
||||
alert(msg)
|
||||
return
|
||||
return msg
|
||||
}
|
||||
% endif
|
||||
}
|
||||
|
||||
// nb. legacy method
|
||||
msg = this.validateSettings()
|
||||
ThisPage.methods.saveSettings = function() {
|
||||
let msg = this.validateSettings()
|
||||
if (msg) {
|
||||
alert(msg)
|
||||
return
|
||||
|
@ -367,65 +291,5 @@
|
|||
window.addEventListener('beforeunload', this.beforeWindowUnload)
|
||||
}
|
||||
|
||||
##############################
|
||||
## input file templates
|
||||
##############################
|
||||
|
||||
% if input_file_template_settings is not Undefined:
|
||||
|
||||
ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n}
|
||||
ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n}
|
||||
ThisPageData.inputFileTemplateUploads = {
|
||||
% for key in input_file_templates:
|
||||
'${key}': null,
|
||||
% endfor
|
||||
}
|
||||
|
||||
ThisPage.methods.validateInputFileTemplateSettings = function() {
|
||||
% for tmpl in input_file_templates.values():
|
||||
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
|
||||
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
|
||||
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
|
||||
return "You must provide a file to upload for the ${tmpl['label']} template."
|
||||
}
|
||||
}
|
||||
}
|
||||
% endfor
|
||||
}
|
||||
|
||||
ThisPageData.validators.push(ThisPage.methods.validateInputFileTemplateSettings)
|
||||
|
||||
% endif
|
||||
|
||||
##############################
|
||||
## output file templates
|
||||
##############################
|
||||
|
||||
% if output_file_template_settings is not Undefined:
|
||||
|
||||
ThisPageData.outputFileTemplateSettings = ${json.dumps(output_file_template_settings)|n}
|
||||
ThisPageData.outputFileTemplateFileOptions = ${json.dumps(output_file_options)|n}
|
||||
ThisPageData.outputFileTemplateUploads = {
|
||||
% for key in output_file_templates:
|
||||
'${key}': null,
|
||||
% endfor
|
||||
}
|
||||
|
||||
ThisPage.methods.validateOutputFileTemplateSettings = function() {
|
||||
% for tmpl in output_file_templates.values():
|
||||
if (this.outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
|
||||
if (!this.outputFileTemplateSettings['${tmpl['setting_file']}']) {
|
||||
if (!this.outputFileTemplateUploads['${tmpl['key']}']) {
|
||||
return "You must provide a file to upload for the ${tmpl['label']} template."
|
||||
}
|
||||
}
|
||||
}
|
||||
% endfor
|
||||
}
|
||||
|
||||
ThisPageData.validators.push(ThisPage.methods.validateOutputFileTemplateSettings)
|
||||
|
||||
% endif
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
|
|
@ -83,8 +83,8 @@
|
|||
</b-notification>
|
||||
|
||||
<b-field>
|
||||
<b-checkbox name="rattail.datasync.use_profile_settings"
|
||||
v-model="simpleSettings['rattail.datasync.use_profile_settings']"
|
||||
<b-checkbox name="use_profile_settings"
|
||||
v-model="useProfileSettings"
|
||||
native-value="true"
|
||||
@input="settingsNeedSaved = true">
|
||||
Use these Settings to configure watchers and consumers
|
||||
|
@ -99,7 +99,7 @@
|
|||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item"
|
||||
v-show="simpleSettings['rattail.datasync.use_profile_settings']">
|
||||
v-show="useProfileSettings">
|
||||
<b-button type="is-primary"
|
||||
@click="newProfile()"
|
||||
icon-pack="fas"
|
||||
|
@ -162,7 +162,7 @@
|
|||
</${b}-table-column>
|
||||
<${b}-table-column label="Actions"
|
||||
v-slot="props"
|
||||
v-if="simpleSettings['rattail.datasync.use_profile_settings']">
|
||||
v-if="useProfileSettings">
|
||||
<a href="#"
|
||||
class="grid-action"
|
||||
@click.prevent="editProfile(props.row)">
|
||||
|
@ -580,27 +580,18 @@
|
|||
<b-field label="Supervisor Process Name"
|
||||
message="This should be the complete name, including group - e.g. poser:poser_datasync"
|
||||
expanded>
|
||||
<b-input name="rattail.datasync.supervisor_process_name"
|
||||
v-model="simpleSettings['rattail.datasync.supervisor_process_name']"
|
||||
<b-input name="supervisor_process_name"
|
||||
v-model="supervisorProcessName"
|
||||
@input="settingsNeedSaved = true"
|
||||
expanded>
|
||||
</b-input>
|
||||
</b-field>
|
||||
|
||||
<b-field label="Consumer Batch Size"
|
||||
message="Max number of changes to be consumed at once."
|
||||
expanded>
|
||||
<numeric-input name="rattail.datasync.batch_size_limit"
|
||||
v-model="simpleSettings['rattail.datasync.batch_size_limit']"
|
||||
@input="settingsNeedSaved = true" />
|
||||
</b-field>
|
||||
|
||||
<h3 class="is-size-3">Legacy</h3>
|
||||
<b-field label="Restart Command"
|
||||
message="This will run as '${system_user}' system user - please configure sudoers as needed. Typical command is like: sudo supervisorctl restart poser:poser_datasync"
|
||||
expanded>
|
||||
<b-input name="tailbone.datasync.restart"
|
||||
v-model="simpleSettings['tailbone.datasync.restart']"
|
||||
<b-input name="restart_command"
|
||||
v-model="restartCommand"
|
||||
@input="settingsNeedSaved = true"
|
||||
expanded>
|
||||
</b-input>
|
||||
|
@ -615,6 +606,7 @@
|
|||
ThisPageData.showConfigFilesNote = false
|
||||
ThisPageData.profilesData = ${json.dumps(profiles_data)|n}
|
||||
ThisPageData.showDisabledProfiles = false
|
||||
ThisPageData.useProfileSettings = ${json.dumps(use_profile_settings)|n}
|
||||
|
||||
ThisPageData.editProfileShowDialog = false
|
||||
ThisPageData.editingProfile = null
|
||||
|
@ -639,6 +631,9 @@
|
|||
ThisPageData.editingConsumerRunas = null
|
||||
ThisPageData.editingConsumerEnabled = true
|
||||
|
||||
ThisPageData.supervisorProcessName = ${json.dumps(supervisor_process_name)|n}
|
||||
ThisPageData.restartCommand = ${json.dumps(restart_command)|n}
|
||||
|
||||
ThisPage.computed.updateConsumerDisabled = function() {
|
||||
if (!this.editingConsumerKey) {
|
||||
return true
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<div i18n:domain="deform" tal:omit-tag=""
|
||||
tal:define="oid oid|field.oid;
|
||||
name name|field.name;
|
||||
vmodel vmodel|'field_model_' + name;
|
||||
css_class css_class|field.widget.css_class;
|
||||
style style|field.widget.style;">
|
||||
|
||||
|
@ -9,7 +8,7 @@
|
|||
${field.start_mapping()}
|
||||
<b-input type="password"
|
||||
name="${name}"
|
||||
v-model="${vmodel}"
|
||||
value="${field.widget.redisplay and cstruct or ''}"
|
||||
tal:attributes="class string: form-control ${css_class or ''};
|
||||
style style;
|
||||
attributes|field.widget.attributes|{};"
|
||||
|
@ -19,6 +18,7 @@
|
|||
</b-input>
|
||||
<b-input type="password"
|
||||
name="${name}-confirm"
|
||||
value="${field.widget.redisplay and confirm or ''}"
|
||||
tal:attributes="class string: form-control ${css_class or ''};
|
||||
style style;
|
||||
confirm_attributes|field.widget.confirm_attributes|{};"
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
|
||||
simplePOST(action, params, success, failure) {
|
||||
|
||||
let csrftoken = ${json.dumps(h.get_csrf_token(request))|n}
|
||||
let csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}
|
||||
|
||||
let headers = {
|
||||
'${csrf_header_name}': csrftoken,
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
native-type="submit"
|
||||
:disabled="${form.vue_component}Submitting"
|
||||
icon-pack="fas"
|
||||
icon-left="${form.button_icon_submit}">
|
||||
icon-left="save">
|
||||
{{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }}
|
||||
</b-button>
|
||||
% else:
|
||||
|
@ -180,7 +180,7 @@
|
|||
let ${form.vue_component}Data = {
|
||||
|
||||
## TODO: should find a better way to handle CSRF token
|
||||
csrftoken: ${json.dumps(h.get_csrf_token(request))|n},
|
||||
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
|
||||
|
||||
% if can_edit_help:
|
||||
fieldLabels: ${json.dumps(field_labels)|n},
|
||||
|
|
|
@ -10,70 +10,8 @@
|
|||
<div style="display: flex; flex-direction: column; justify-content: end;">
|
||||
<div class="filters">
|
||||
% if getattr(grid, 'filterable', False):
|
||||
<form method="GET" @submit.prevent="applyFilters()">
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
<grid-filter v-for="key in filtersSequence"
|
||||
:key="key"
|
||||
:filter="filters[key]"
|
||||
ref="gridFilters">
|
||||
</grid-filter>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
|
||||
<b-button type="is-primary"
|
||||
native-type="submit"
|
||||
icon-pack="fas"
|
||||
icon-left="check">
|
||||
Apply Filters
|
||||
</b-button>
|
||||
|
||||
<b-button v-if="!addFilterShow"
|
||||
icon-pack="fas"
|
||||
icon-left="plus"
|
||||
@click="addFilterInit()">
|
||||
Add Filter
|
||||
</b-button>
|
||||
|
||||
<b-autocomplete v-if="addFilterShow"
|
||||
ref="addFilterAutocomplete"
|
||||
:data="addFilterChoices"
|
||||
v-model="addFilterTerm"
|
||||
placeholder="Add Filter"
|
||||
field="key"
|
||||
:custom-formatter="formatAddFilterItem"
|
||||
open-on-focus
|
||||
keep-first
|
||||
icon-pack="fas"
|
||||
clearable
|
||||
clear-on-select
|
||||
@select="addFilterSelect">
|
||||
</b-autocomplete>
|
||||
|
||||
<b-button @click="resetView()"
|
||||
icon-pack="fas"
|
||||
icon-left="home">
|
||||
Default View
|
||||
</b-button>
|
||||
|
||||
<b-button @click="clearFilters()"
|
||||
icon-pack="fas"
|
||||
icon-left="trash">
|
||||
No Filters
|
||||
</b-button>
|
||||
|
||||
% if allow_save_defaults and request.user:
|
||||
<b-button @click="saveDefaults()"
|
||||
icon-pack="fas"
|
||||
icon-left="save"
|
||||
:disabled="savingDefaults">
|
||||
{{ savingDefaults ? "Working, please wait..." : "Save Defaults" }}
|
||||
</b-button>
|
||||
% endif
|
||||
|
||||
</div>
|
||||
</form>
|
||||
## TODO: stop using |n filter
|
||||
${grid.render_filters(allow_save_defaults=allow_save_defaults)|n}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
|
@ -198,8 +136,10 @@
|
|||
<${b}-table-column field="${column['field']}"
|
||||
label="${column['label']}"
|
||||
v-slot="props"
|
||||
:sortable="${json.dumps(column.get('sortable', False))|n}"
|
||||
:searchable="${json.dumps(column.get('searchable', False))|n}"
|
||||
:sortable="${json.dumps(column.get('sortable', False))}"
|
||||
% if hasattr(grid, 'is_searchable') and grid.is_searchable(column['field']):
|
||||
searchable
|
||||
% endif
|
||||
cell-class="c_${column['field']}"
|
||||
:visible="${json.dumps(column.get('visible', True))}">
|
||||
% if hasattr(grid, 'raw_renderers') and column['field'] in grid.raw_renderers:
|
||||
|
@ -311,16 +251,12 @@
|
|||
|
||||
<script type="text/javascript">
|
||||
|
||||
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}CurrentData = ${json.dumps(grid.get_vue_data())|n}
|
||||
|
||||
let ${grid.vue_component}Data = {
|
||||
loading: false,
|
||||
ajaxDataUrl: ${json.dumps(getattr(grid, 'ajax_data_url', request.path_url))|n},
|
||||
|
||||
## nb. this tracks whether grid.fetchFirstData() happened
|
||||
fetchedFirstData: false,
|
||||
|
||||
savingDefaults: false,
|
||||
|
||||
data: ${grid.vue_component}CurrentData,
|
||||
|
@ -583,17 +519,6 @@
|
|||
...this.getFilterParams()}
|
||||
},
|
||||
|
||||
## nb. this is meant to call for a grid which is hidden at
|
||||
## first, when it is first being shown to the user. and if
|
||||
## it was initialized with empty data set.
|
||||
async fetchFirstData() {
|
||||
if (this.fetchedFirstData) {
|
||||
return
|
||||
}
|
||||
await this.loadAsyncData()
|
||||
this.fetchedFirstData = true
|
||||
},
|
||||
|
||||
## TODO: i noticed buefy docs show using `async` keyword here,
|
||||
## so now i am too. knowing nothing at all of if/how this is
|
||||
## supposed to improve anything. we shall see i guess
|
||||
|
@ -746,7 +671,7 @@
|
|||
this.loading = true
|
||||
|
||||
// use current url proper, plus reset param
|
||||
let url = '?reset-view=true'
|
||||
let url = '?reset-to-default-filters=true'
|
||||
|
||||
// add current hash, to preserve that in redirect
|
||||
if (location.hash) {
|
||||
|
|
67
tailbone/templates/grids/filters.mako
Normal file
67
tailbone/templates/grids/filters.mako
Normal file
|
@ -0,0 +1,67 @@
|
|||
## -*- 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,7 +1,33 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="wuttaweb:templates/home.mako" />
|
||||
<%inherit file="/page.mako" />
|
||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
||||
|
||||
<%def name="title()">Home</%def>
|
||||
|
||||
<%def name="extra_styles()">
|
||||
${parent.extra_styles()}
|
||||
<style type="text/css">
|
||||
.logo {
|
||||
text-align: center;
|
||||
}
|
||||
.logo img {
|
||||
margin: 3em auto;
|
||||
max-height: 350px;
|
||||
max-width: 800px;
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
## DEPRECATED; remains for back-compat
|
||||
<%def name="render_this_page()">
|
||||
${self.page_content()}
|
||||
</%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
<div class="logo">
|
||||
${h.image(image_url, "{} logo".format(capture(base_meta.app_title)))}
|
||||
<h1>Welcome to ${base_meta.app_title()}</h1>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
||||
|
|
|
@ -1,17 +1,84 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="wuttaweb:templates/auth/login.mako" />
|
||||
<%inherit file="/form.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()">
|
||||
${parent.extra_styles()}
|
||||
<style>
|
||||
.card-content .buttons {
|
||||
<style type="text/css">
|
||||
.logo img {
|
||||
display: block;
|
||||
margin: 3rem auto;
|
||||
max-height: 350px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
/* must force a particular label with, in order to make sure */
|
||||
/* the username and password inputs are the same size */
|
||||
.field.is-horizontal .field-label .label {
|
||||
text-align: left;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: right;
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
## DEPRECATED; remains for back-compat
|
||||
<%def name="logo()">
|
||||
${h.image(image_url, "{} logo".format(capture(base_meta.app_title)))}
|
||||
</%def>
|
||||
|
||||
<%def name="login_form()">
|
||||
<div class="form">
|
||||
${form.render_deform(form_kwargs={'data-ajax': 'false'})|n}
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="render_this_page()">
|
||||
${self.page_content()}
|
||||
</%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
<div class="logo">
|
||||
${self.logo()}
|
||||
</div>
|
||||
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-narrow">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<tailbone-form></tailbone-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
${parent.modify_vue_vars()}
|
||||
<script>
|
||||
|
||||
${form.vue_component}Data.usernameInput = null
|
||||
|
||||
${form.vue_component}.mounted = function() {
|
||||
this.$refs.username.focus()
|
||||
this.usernameInput = this.$refs.username.$el.querySelector('input')
|
||||
this.usernameInput.addEventListener('keydown', this.usernameKeydown)
|
||||
}
|
||||
|
||||
${form.vue_component}.beforeDestroy = function() {
|
||||
this.usernameInput.removeEventListener('keydown', this.usernameKeydown)
|
||||
}
|
||||
|
||||
${form.vue_component}.methods.usernameKeydown = function(event) {
|
||||
if (event.which == 13) {
|
||||
event.preventDefault()
|
||||
this.$refs.password.focus()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
|
|
@ -196,7 +196,6 @@
|
|||
|
||||
<p class="block has-text-weight-bold">
|
||||
{{ version.model_title }}
|
||||
({{ version.operation }})
|
||||
</p>
|
||||
|
||||
<table class="diff monospace is-size-7"
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
## -*- 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,
|
||||
|
||||
## TODO: should find a better way to handle CSRF token
|
||||
csrftoken: ${json.dumps(h.get_csrf_token(request))|n},
|
||||
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -250,7 +250,7 @@
|
|||
submitting: false,
|
||||
|
||||
## TODO: should find a better way to handle CSRF token
|
||||
csrftoken: ${json.dumps(h.get_csrf_token(request))|n},
|
||||
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
|
||||
const ThisPageData = {
|
||||
## TODO: should find a better way to handle CSRF token
|
||||
csrftoken: ${json.dumps(h.get_csrf_token(request))|n},
|
||||
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
@ -55,20 +55,19 @@
|
|||
</%def>
|
||||
|
||||
<%def name="render_form_template()">
|
||||
<script type="text/x-template" id="${form.vue_tagname}-template">
|
||||
<script type="text/x-template" id="${form.component}-template">
|
||||
${self.render_form_innards()}
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
${parent.modify_vue_vars()}
|
||||
<% request.register_component(form.vue_tagname, form.vue_component) %>
|
||||
<script>
|
||||
|
||||
## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako)
|
||||
|
||||
let ${form.vue_component} = {
|
||||
template: '#${form.vue_tagname}-template',
|
||||
template: '#${form.component}-template',
|
||||
methods: {
|
||||
|
||||
## TODO: deprecate / remove the latter option here
|
||||
|
|
|
@ -69,12 +69,12 @@
|
|||
<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_receiving_any_vendor"
|
||||
v-model="simpleSettings['rattail.batch.purchase.allow_receiving_any_vendor']"
|
||||
<b-field message="If set, user must choose a "supported" vendor; otherwise they may choose "any" vendor.">
|
||||
<b-checkbox name="rattail.batch.purchase.supported_vendors_only"
|
||||
v-model="simpleSettings['rattail.batch.purchase.supported_vendors_only']"
|
||||
native-value="true"
|
||||
@input="settingsNeedSaved = true">
|
||||
Allow receiving for <span class="has-text-weight-bold">any</span> vendor
|
||||
Only allow batch for "supported" vendors
|
||||
</b-checkbox>
|
||||
</b-field>
|
||||
|
||||
|
|
|
@ -45,10 +45,11 @@
|
|||
<b-button @click="runReportShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
${h.form(master.get_action_url('execute', instance), **{'@submit': 'runReportSubmitting = true'})}
|
||||
${h.form(master.get_action_url('execute', instance))}
|
||||
${h.csrf_token(request)}
|
||||
<b-button type="is-primary"
|
||||
native-type="submit"
|
||||
@click="runReportSubmitting = true"
|
||||
:disabled="runReportSubmitting"
|
||||
icon-pack="fas"
|
||||
icon-left="arrow-circle-right">
|
||||
|
|
|
@ -909,7 +909,7 @@
|
|||
${h.form(url('stop_root'), ref='stopBeingRootForm')}
|
||||
${h.csrf_token(request)}
|
||||
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
||||
<a @click="$refs.stopBeingRootForm.submit()"
|
||||
<a @click="stopBeingRoot()"
|
||||
class="navbar-item has-background-danger has-text-white">
|
||||
Stop being root
|
||||
</a>
|
||||
|
@ -918,7 +918,7 @@
|
|||
${h.form(url('become_root'), ref='startBeingRootForm')}
|
||||
${h.csrf_token(request)}
|
||||
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
||||
<a @click="$refs.startBeingRootForm.submit()"
|
||||
<a @click="startBeingRoot()"
|
||||
class="navbar-item has-background-danger has-text-white">
|
||||
Become root
|
||||
</a>
|
||||
|
@ -1103,6 +1103,18 @@
|
|||
const key = 'menu_' + hash + '_shown'
|
||||
this[key] = !this[key]
|
||||
},
|
||||
|
||||
% if request.is_admin:
|
||||
|
||||
startBeingRoot() {
|
||||
this.$refs.startBeingRootForm.submit()
|
||||
},
|
||||
|
||||
stopBeingRoot() {
|
||||
this.$refs.stopBeingRootForm.submit()
|
||||
},
|
||||
|
||||
% endif
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -666,7 +666,6 @@
|
|||
<%def name="make_b_tooltip_component()">
|
||||
<script type="text/x-template" id="b-tooltip-template">
|
||||
<o-tooltip :label="label"
|
||||
:position="orugaPosition"
|
||||
:multiline="multilined">
|
||||
<slot />
|
||||
</o-tooltip>
|
||||
|
@ -677,14 +676,6 @@
|
|||
props: {
|
||||
label: String,
|
||||
multilined: Boolean,
|
||||
position: String,
|
||||
},
|
||||
computed: {
|
||||
orugaPosition() {
|
||||
if (this.position) {
|
||||
return this.position.replace(/^is-/, '')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
<%def name="base_styles()">
|
||||
${parent.base_styles()}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
<style>
|
||||
|
||||
.filters .filter-fieldname .field,
|
||||
|
@ -51,12 +50,10 @@
|
|||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="${url('home')}"
|
||||
v-show="!menuSearchActive">
|
||||
<div style="display: flex; align-items: center;">
|
||||
${base_meta.header_logo()}
|
||||
<div id="navbar-brand-title">
|
||||
<div id="global-header-title">
|
||||
${base_meta.global_title()}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div v-show="menuSearchActive"
|
||||
class="navbar-item">
|
||||
|
@ -164,88 +161,11 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
${parent.render_feedback_button()}
|
||||
</%def>
|
||||
|
||||
<%def name="render_crud_header_buttons()">
|
||||
% if master:
|
||||
% if master.viewing:
|
||||
% if instance_editable and master.has_perm('edit'):
|
||||
<wutta-button once
|
||||
tag="a" href="${master.get_action_url('edit', instance)}"
|
||||
icon-left="edit"
|
||||
label="Edit This" />
|
||||
% endif
|
||||
% if getattr(master, 'cloneable', False) and not master.cloning and master.has_perm('clone'):
|
||||
<wutta-button once
|
||||
tag="a" href="${master.get_action_url('clone', instance)}"
|
||||
icon-left="object-ungroup"
|
||||
label="Clone This" />
|
||||
% endif
|
||||
% if instance_deletable and master.has_perm('delete'):
|
||||
<wutta-button once type="is-danger"
|
||||
tag="a" href="${master.get_action_url('delete', instance)}"
|
||||
icon-left="trash"
|
||||
label="Delete This" />
|
||||
% endif
|
||||
% elif master.editing:
|
||||
% if master.has_perm('view'):
|
||||
<wutta-button once
|
||||
tag="a" href="${master.get_action_url('view', instance)}"
|
||||
icon-left="eye"
|
||||
label="View This" />
|
||||
% endif
|
||||
% if instance_deletable and master.has_perm('delete'):
|
||||
<wutta-button once type="is-danger"
|
||||
tag="a" href="${master.get_action_url('delete', instance)}"
|
||||
icon-left="trash"
|
||||
label="Delete This" />
|
||||
% endif
|
||||
% elif master.deleting:
|
||||
% if master.has_perm('view'):
|
||||
<wutta-button once
|
||||
tag="a" href="${master.get_action_url('view', instance)}"
|
||||
icon-left="eye"
|
||||
label="View This" />
|
||||
% endif
|
||||
% if instance_editable and master.has_perm('edit'):
|
||||
<wutta-button once
|
||||
tag="a" href="${master.get_action_url('edit', instance)}"
|
||||
icon-left="edit"
|
||||
label="Edit This" />
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="render_prevnext_header_buttons()">
|
||||
% if show_prev_next is not Undefined and show_prev_next:
|
||||
% if prev_url:
|
||||
<wutta-button once
|
||||
tag="a" href="${prev_url}"
|
||||
icon-left="arrow-left"
|
||||
label="Older" />
|
||||
% else:
|
||||
<b-button tag="a" href="#"
|
||||
disabled
|
||||
icon-pack="fas"
|
||||
icon-left="arrow-left">
|
||||
Older
|
||||
</b-button>
|
||||
% endif
|
||||
% if next_url:
|
||||
<wutta-button once
|
||||
tag="a" href="${next_url}"
|
||||
icon-left="arrow-right"
|
||||
label="Newer" />
|
||||
% else:
|
||||
<b-button tag="a" href="#"
|
||||
disabled
|
||||
icon-pack="fas"
|
||||
icon-left="arrow-right">
|
||||
Newer
|
||||
</b-button>
|
||||
% endif
|
||||
% if request.has_perm('common.feedback'):
|
||||
<feedback-form
|
||||
action="${url('feedback')}"
|
||||
:message="feedbackMessage">
|
||||
</feedback-form>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
|
@ -257,7 +177,13 @@
|
|||
/>
|
||||
</%def>
|
||||
|
||||
<%def name="render_vue_template_feedback()">
|
||||
<%def name="render_vue_templates()">
|
||||
${parent.render_vue_templates()}
|
||||
|
||||
${page_help.render_template()}
|
||||
${page_help.declare_vars()}
|
||||
|
||||
% if request.has_perm('common.feedback'):
|
||||
<script type="text/x-template" id="feedback-template">
|
||||
<div>
|
||||
|
||||
|
@ -336,7 +262,7 @@
|
|||
icon-pack="fas"
|
||||
icon-left="paper-plane"
|
||||
@click="sendFeedback()"
|
||||
:disabled="sendingFeedback || !message || !message.trim()">
|
||||
:disabled="sendingFeedback || !message.trim()">
|
||||
{{ sendingFeedback ? "Working, please wait..." : "Send Message" }}
|
||||
</b-button>
|
||||
</footer>
|
||||
|
@ -345,45 +271,80 @@
|
|||
|
||||
</div>
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="render_vue_script_feedback()">
|
||||
${parent.render_vue_script_feedback()}
|
||||
<script>
|
||||
|
||||
WuttaFeedbackForm.template = '#feedback-template'
|
||||
WuttaFeedbackForm.props.message = String
|
||||
const FeedbackForm = {
|
||||
template: '#feedback-template',
|
||||
mixins: [SimpleRequestMixin],
|
||||
props: [
|
||||
'action',
|
||||
'message',
|
||||
],
|
||||
methods: {
|
||||
|
||||
showFeedback() {
|
||||
this.referrer = location.href
|
||||
this.showDialog = true
|
||||
this.$nextTick(function() {
|
||||
this.$refs.textarea.focus()
|
||||
})
|
||||
},
|
||||
|
||||
% if config.get_bool('tailbone.feedback_allows_reply'):
|
||||
|
||||
WuttaFeedbackFormData.pleaseReply = false
|
||||
WuttaFeedbackFormData.userEmail = null
|
||||
|
||||
WuttaFeedbackForm.methods.pleaseReplyChanged = function(value) {
|
||||
pleaseReplyChanged(value) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.userEmail.focus()
|
||||
})
|
||||
}
|
||||
|
||||
WuttaFeedbackForm.methods.getExtraParams = function() {
|
||||
return {
|
||||
please_reply_to: this.pleaseReply ? this.userEmail : null,
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
% endif
|
||||
|
||||
// TODO: deprecate / remove these
|
||||
const FeedbackForm = WuttaFeedbackForm
|
||||
const FeedbackFormData = WuttaFeedbackFormData
|
||||
sendFeedback() {
|
||||
this.sendingFeedback = true
|
||||
|
||||
const params = {
|
||||
referrer: this.referrer,
|
||||
user: this.userUUID,
|
||||
user_name: this.userName,
|
||||
% if config.get_bool('tailbone.feedback_allows_reply'):
|
||||
please_reply_to: this.pleaseReply ? this.userEmail : null,
|
||||
% endif
|
||||
message: this.message.trim(),
|
||||
}
|
||||
|
||||
this.simplePOST(this.action, params, response => {
|
||||
|
||||
this.$buefy.toast.open({
|
||||
message: "Message sent! Thank you for your feedback.",
|
||||
type: 'is-info',
|
||||
duration: 4000, // 4 seconds
|
||||
})
|
||||
|
||||
this.showDialog = false
|
||||
// clear out message, in case they need to send another
|
||||
this.message = ""
|
||||
this.sendingFeedback = false
|
||||
|
||||
}, response => { // failure
|
||||
this.sendingFeedback = false
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const FeedbackFormData = {
|
||||
referrer: null,
|
||||
userUUID: null,
|
||||
userName: null,
|
||||
userEmail: null,
|
||||
% if config.get_bool('tailbone.feedback_allows_reply'):
|
||||
pleaseReply: false,
|
||||
% endif
|
||||
showDialog: false,
|
||||
sendingFeedback: false,
|
||||
}
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="render_vue_templates()">
|
||||
${parent.render_vue_templates()}
|
||||
${page_help.render_template()}
|
||||
${page_help.declare_vars()}
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
|
@ -482,6 +443,21 @@
|
|||
|
||||
% 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
|
||||
##############################
|
||||
|
@ -501,4 +477,10 @@
|
|||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.timepicker.js') + f'?ver={tailbone.__version__}')}
|
||||
${make_grid_filter_components()}
|
||||
${page_help.make_component()}
|
||||
% if request.has_perm('common.feedback'):
|
||||
<script>
|
||||
FeedbackForm.data = function() { return FeedbackFormData }
|
||||
Vue.component('feedback-form', FeedbackForm)
|
||||
</script>
|
||||
% endif
|
||||
</%def>
|
||||
|
|
|
@ -1,78 +1,2 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="wuttaweb:templates/configure.mako" />
|
||||
<%namespace name="tailbone_base" file="tailbone:templates/configure.mako" />
|
||||
|
||||
<%def name="input_file_templates_section()">
|
||||
${tailbone_base.input_file_templates_section()}
|
||||
</%def>
|
||||
|
||||
<%def name="output_file_templates_section()">
|
||||
${tailbone_base.output_file_templates_section()}
|
||||
</%def>
|
||||
|
||||
<%def name="modify_vue_vars()">
|
||||
${parent.modify_vue_vars()}
|
||||
<script>
|
||||
|
||||
##############################
|
||||
## input file templates
|
||||
##############################
|
||||
|
||||
% if input_file_template_settings is not Undefined:
|
||||
|
||||
ThisPageData.inputFileTemplateSettings = ${json.dumps(input_file_template_settings)|n}
|
||||
ThisPageData.inputFileTemplateFileOptions = ${json.dumps(input_file_options)|n}
|
||||
ThisPageData.inputFileTemplateUploads = {
|
||||
% for key in input_file_templates:
|
||||
'${key}': null,
|
||||
% endfor
|
||||
}
|
||||
|
||||
ThisPage.methods.validateInputFileTemplateSettings = function() {
|
||||
% for tmpl in input_file_templates.values():
|
||||
if (this.inputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
|
||||
if (!this.inputFileTemplateSettings['${tmpl['setting_file']}']) {
|
||||
if (!this.inputFileTemplateUploads['${tmpl['key']}']) {
|
||||
return "You must provide a file to upload for the ${tmpl['label']} template."
|
||||
}
|
||||
}
|
||||
}
|
||||
% endfor
|
||||
}
|
||||
|
||||
ThisPageData.validators.push(ThisPage.methods.validateInputFileTemplateSettings)
|
||||
|
||||
% endif
|
||||
|
||||
##############################
|
||||
## output file templates
|
||||
##############################
|
||||
|
||||
% if output_file_template_settings is not Undefined:
|
||||
|
||||
ThisPageData.outputFileTemplateSettings = ${json.dumps(output_file_template_settings)|n}
|
||||
ThisPageData.outputFileTemplateFileOptions = ${json.dumps(output_file_options)|n}
|
||||
ThisPageData.outputFileTemplateUploads = {
|
||||
% for key in output_file_templates:
|
||||
'${key}': null,
|
||||
% endfor
|
||||
}
|
||||
|
||||
ThisPage.methods.validateOutputFileTemplateSettings = function() {
|
||||
% for tmpl in output_file_templates.values():
|
||||
if (this.outputFileTemplateSettings['${tmpl['setting_mode']}'] == 'hosted') {
|
||||
if (!this.outputFileTemplateSettings['${tmpl['setting_file']}']) {
|
||||
if (!this.outputFileTemplateUploads['${tmpl['key']}']) {
|
||||
return "You must provide a file to upload for the ${tmpl['label']} template."
|
||||
}
|
||||
}
|
||||
}
|
||||
% endfor
|
||||
}
|
||||
|
||||
ThisPageData.validators.push(ThisPage.methods.validateOutputFileTemplateSettings)
|
||||
|
||||
% endif
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
|
|
@ -1,10 +1,2 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="wuttaweb:templates/form.mako" />
|
||||
|
||||
<%def name="render_vue_template_form()">
|
||||
% if form is not Undefined:
|
||||
${form.render_vue_template(buttons=capture(self.render_form_buttons))}
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="render_form_buttons()"></%def>
|
||||
|
|
|
@ -254,11 +254,6 @@
|
|||
|
||||
</%def>
|
||||
|
||||
## DEPRECATED; remains for back-compat
|
||||
<%def name="render_this_page()">
|
||||
${self.page_content()}
|
||||
</%def>
|
||||
|
||||
<%def name="render_vue_template_grid()">
|
||||
${grid.render_vue_template(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())}
|
||||
</%def>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
${parent.modify_vue_vars()}
|
||||
<script>
|
||||
|
||||
ThisPageData.csrftoken = ${json.dumps(h.get_csrf_token(request))|n}
|
||||
ThisPageData.csrftoken = ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n}
|
||||
|
||||
% if can_edit_help:
|
||||
ThisPage.props.configureFieldsHelp = Boolean
|
||||
|
|
|
@ -41,9 +41,7 @@ from webhelpers2.html import HTML, tags
|
|||
|
||||
from wuttaweb.util import (get_form_data as wutta_get_form_data,
|
||||
get_libver as wutta_get_libver,
|
||||
get_liburl as wutta_get_liburl,
|
||||
get_csrf_token as wutta_get_csrf_token,
|
||||
render_csrf_token)
|
||||
get_liburl as wutta_get_liburl)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -61,19 +59,22 @@ class SortColumn(object):
|
|||
|
||||
|
||||
def get_csrf_token(request):
|
||||
""" """
|
||||
warnings.warn("tailbone.util.get_csrf_token() is deprecated; "
|
||||
"please use wuttaweb.util.get_csrf_token() instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return wutta_get_csrf_token(request)
|
||||
"""
|
||||
Convenience function to retrieve the effective CSRF token for the given
|
||||
request.
|
||||
"""
|
||||
token = request.session.get_csrf_token()
|
||||
if token is None:
|
||||
token = request.session.new_csrf_token()
|
||||
return token
|
||||
|
||||
|
||||
def csrf_token(request, name='_csrf'):
|
||||
""" """
|
||||
warnings.warn("tailbone.util.csrf_token() is deprecated; "
|
||||
"please use wuttaweb.util.render_csrf_token() instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return render_csrf_token(request, name=name)
|
||||
"""
|
||||
Convenience function. Returns CSRF hidden tag inside hidden DIV.
|
||||
"""
|
||||
token = get_csrf_token(request)
|
||||
return HTML.tag("div", tags.hidden(name, value=token), style="display:none;")
|
||||
|
||||
|
||||
def get_form_data(request):
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
Auth Views
|
||||
"""
|
||||
|
||||
from rattail.db.auth import set_user_password
|
||||
|
||||
import colander
|
||||
from deform import widget as dfwidget
|
||||
from pyramid.httpexceptions import HTTPForbidden
|
||||
|
@ -44,6 +46,28 @@ class UserLogin(colander.MappingSchema):
|
|||
widget=dfwidget.PasswordWidget())
|
||||
|
||||
|
||||
@colander.deferred
|
||||
def current_password_correct(node, kw):
|
||||
request = kw['request']
|
||||
app = request.rattail_config.get_app()
|
||||
auth = app.get_auth_handler()
|
||||
user = kw['user']
|
||||
def validate(node, value):
|
||||
if not auth.authenticate_user(Session(), user.username, value):
|
||||
raise colander.Invalid(node, "The password is incorrect")
|
||||
return validate
|
||||
|
||||
|
||||
class ChangePassword(colander.MappingSchema):
|
||||
|
||||
current_password = colander.SchemaNode(colander.String(),
|
||||
widget=dfwidget.PasswordWidget(),
|
||||
validator=current_password_correct)
|
||||
|
||||
new_password = colander.SchemaNode(colander.String(),
|
||||
widget=dfwidget.CheckedPasswordWidget())
|
||||
|
||||
|
||||
class AuthenticationView(View):
|
||||
|
||||
def forbidden(self):
|
||||
|
@ -80,7 +104,6 @@ class AuthenticationView(View):
|
|||
form.save_label = "Login"
|
||||
form.show_reset = True
|
||||
form.show_cancel = False
|
||||
form.button_icon_submit = 'user'
|
||||
if form.validate():
|
||||
user = self.authenticate_user(form.validated['username'],
|
||||
form.validated['password'])
|
||||
|
@ -94,6 +117,10 @@ class AuthenticationView(View):
|
|||
else:
|
||||
self.request.session.flash("Invalid username or password", 'error')
|
||||
|
||||
image_url = self.rattail_config.get(
|
||||
'tailbone', 'main_image_url',
|
||||
default=self.request.static_url('tailbone:static/img/home_logo.png'))
|
||||
|
||||
# nb. hacky..but necessary, to add the refs, for autofocus
|
||||
# (also add key handler, so ENTER acts like TAB)
|
||||
dform = form.make_deform_form()
|
||||
|
@ -106,6 +133,7 @@ class AuthenticationView(View):
|
|||
return {
|
||||
'form': form,
|
||||
'referrer': referrer,
|
||||
'image_url': image_url,
|
||||
'index_title': app.get_node_title(),
|
||||
'help_url': global_help_url(self.rattail_config),
|
||||
}
|
||||
|
@ -154,27 +182,10 @@ class AuthenticationView(View):
|
|||
self.request.user))
|
||||
return self.redirect(self.request.get_referrer())
|
||||
|
||||
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()))
|
||||
|
||||
schema = ChangePassword().bind(user=self.request.user, request=self.request)
|
||||
form = forms.Form(schema=schema, request=self.request)
|
||||
if form.validate():
|
||||
auth = self.app.get_auth_handler()
|
||||
auth.set_user_password(self.request.user, form.validated['new_password'])
|
||||
set_user_password(self.request.user, form.validated['new_password'])
|
||||
self.request.session.flash("Your password has been changed.")
|
||||
return self.redirect(self.request.get_referrer())
|
||||
|
||||
|
|
|
@ -46,11 +46,10 @@ import colander
|
|||
from deform import widget as dfwidget
|
||||
from webhelpers2.html import HTML, tags
|
||||
|
||||
from wuttaweb.util import render_csrf_token
|
||||
|
||||
from tailbone import forms, grids
|
||||
from tailbone.db import Session
|
||||
from tailbone.views import MasterView
|
||||
from tailbone.util import csrf_token
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -384,7 +383,7 @@ class BatchMasterView(MasterView):
|
|||
f.set_label('executed_by', "Executed by")
|
||||
|
||||
# notes
|
||||
f.set_type('notes', 'text_wrapped')
|
||||
f.set_type('notes', 'text')
|
||||
|
||||
# if self.creating and self.request.user:
|
||||
# batch = fs.model
|
||||
|
@ -442,7 +441,7 @@ class BatchMasterView(MasterView):
|
|||
|
||||
form = [
|
||||
begin_form,
|
||||
render_csrf_token(self.request),
|
||||
csrf_token(self.request),
|
||||
tags.hidden('complete', value=value),
|
||||
submit,
|
||||
tags.end_form(),
|
||||
|
@ -862,7 +861,7 @@ class BatchMasterView(MasterView):
|
|||
if not schema:
|
||||
schema = colander.Schema()
|
||||
|
||||
kwargs['vue_tagname'] = 'execute-form'
|
||||
kwargs['component'] = 'execute-form'
|
||||
form = forms.Form(schema=schema, request=self.request, defaults=defaults, **kwargs)
|
||||
self.configure_execute_form(form)
|
||||
return form
|
||||
|
|
|
@ -25,7 +25,6 @@ Various common views
|
|||
"""
|
||||
|
||||
import os
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
from rattail.batch import consume_batch_id
|
||||
|
@ -51,31 +50,13 @@ class CommonView(View):
|
|||
Home page view.
|
||||
"""
|
||||
app = self.get_rattail_app()
|
||||
|
||||
# maybe auto-redirect anons to login
|
||||
if not self.request.user:
|
||||
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'))
|
||||
if self.rattail_config.getbool('tailbone', 'login_is_home', default=True):
|
||||
raise 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')
|
||||
image_url = self.rattail_config.get(
|
||||
'tailbone', 'main_image_url',
|
||||
default=self.request.static_url('tailbone:static/img/home_logo.png'))
|
||||
|
||||
context = {
|
||||
'image_url': image_url,
|
||||
|
|
|
@ -202,36 +202,10 @@ class DataSyncThreadView(MasterView):
|
|||
return self.redirect(self.request.get_referrer(
|
||||
default=self.request.route_url('datasyncchanges')))
|
||||
|
||||
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)
|
||||
|
||||
def configure_get_context(self):
|
||||
profiles = self.datasync_handler.get_configured_profiles(
|
||||
include_disabled=True,
|
||||
ignore_problems=True)
|
||||
context['profiles'] = profiles
|
||||
|
||||
profiles_data = []
|
||||
for profile in sorted(profiles.values(), key=lambda p: p.key):
|
||||
|
@ -269,16 +243,26 @@ class DataSyncThreadView(MasterView):
|
|||
data['consumers_data'] = consumers
|
||||
profiles_data.append(data)
|
||||
|
||||
context['profiles_data'] = profiles_data
|
||||
return context
|
||||
return {
|
||||
'profiles': profiles,
|
||||
'profiles_data': profiles_data,
|
||||
'use_profile_settings': self.datasync_handler.should_use_profile_settings(),
|
||||
'supervisor_process_name': self.rattail_config.get(
|
||||
'rattail.datasync', 'supervisor_process_name'),
|
||||
'restart_command': self.rattail_config.get(
|
||||
'tailbone', 'datasync.restart'),
|
||||
}
|
||||
|
||||
def configure_gather_settings(self, data, **kwargs):
|
||||
""" """
|
||||
settings = super().configure_gather_settings(data, **kwargs)
|
||||
|
||||
if data.get('rattail.datasync.use_profile_settings') == 'true':
|
||||
def configure_gather_settings(self, data):
|
||||
settings = []
|
||||
watch = []
|
||||
|
||||
use_profile_settings = data.get('use_profile_settings') == 'true'
|
||||
settings.append({'name': 'rattail.datasync.use_profile_settings',
|
||||
'value': 'true' if use_profile_settings else 'false'})
|
||||
|
||||
if use_profile_settings:
|
||||
|
||||
for profile in json.loads(data['profiles']):
|
||||
pkey = profile['key']
|
||||
if profile['enabled']:
|
||||
|
@ -339,12 +323,17 @@ class DataSyncThreadView(MasterView):
|
|||
settings.append({'name': 'rattail.datasync.watch',
|
||||
'value': ', '.join(watch)})
|
||||
|
||||
if data['supervisor_process_name']:
|
||||
settings.append({'name': 'rattail.datasync.supervisor_process_name',
|
||||
'value': data['supervisor_process_name']})
|
||||
|
||||
if data['restart_command']:
|
||||
settings.append({'name': 'tailbone.datasync.restart',
|
||||
'value': data['restart_command']})
|
||||
|
||||
return settings
|
||||
|
||||
def configure_remove_settings(self, **kwargs):
|
||||
""" """
|
||||
super().configure_remove_settings(**kwargs)
|
||||
|
||||
def configure_remove_settings(self):
|
||||
purge_datasync_settings(self.rattail_config, self.Session())
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -116,12 +116,11 @@ class EmailSettingView(MasterView):
|
|||
return data
|
||||
|
||||
def configure_grid(self, g):
|
||||
super().configure_grid(g)
|
||||
|
||||
g.sort_on_backend = False
|
||||
g.sort_multiple = False
|
||||
g.sorters['key'] = g.make_simple_sorter('key', foldcase=True)
|
||||
g.sorters['prefix'] = g.make_simple_sorter('prefix', foldcase=True)
|
||||
g.sorters['subject'] = g.make_simple_sorter('subject', foldcase=True)
|
||||
g.sorters['enabled'] = g.make_simple_sorter('enabled')
|
||||
g.set_sort_defaults('key')
|
||||
|
||||
g.set_type('enabled', 'boolean')
|
||||
g.set_link('key')
|
||||
g.set_link('subject')
|
||||
|
@ -131,9 +130,11 @@ class EmailSettingView(MasterView):
|
|||
|
||||
# to
|
||||
g.set_renderer('to', self.render_to_short)
|
||||
g.sorters['to'] = g.make_simple_sorter('to', foldcase=True)
|
||||
|
||||
# hidden
|
||||
if self.has_perm('configure'):
|
||||
g.sorters['hidden'] = g.make_simple_sorter('hidden')
|
||||
g.set_type('hidden', 'boolean')
|
||||
else:
|
||||
g.remove('hidden')
|
||||
|
|
|
@ -117,7 +117,6 @@ class MasterView(View):
|
|||
supports_prev_next = False
|
||||
supports_import_batch_from_file = False
|
||||
has_input_file_templates = False
|
||||
has_output_file_templates = False
|
||||
configurable = False
|
||||
|
||||
# set to True to add "View *global* Objects" permission, and
|
||||
|
@ -335,7 +334,7 @@ class MasterView(View):
|
|||
|
||||
# If user just refreshed the page with a reset instruction, issue a
|
||||
# redirect in order to clear out the query string.
|
||||
if self.request.GET.get('reset-view'):
|
||||
if self.request.GET.get('reset-to-default-filters') == 'true':
|
||||
kw = {'_query': None}
|
||||
hash_ = self.request.GET.get('hash')
|
||||
if hash_:
|
||||
|
@ -412,7 +411,7 @@ class MasterView(View):
|
|||
session = self.Session()
|
||||
kwargs.setdefault('paginated', False)
|
||||
grid = self.make_grid(session=session, **kwargs)
|
||||
return grid.get_visible_data()
|
||||
return grid.make_visible_data()
|
||||
|
||||
def get_grid_columns(self):
|
||||
"""
|
||||
|
@ -551,8 +550,7 @@ class MasterView(View):
|
|||
def get_quickie_result_url(self, obj):
|
||||
return self.get_action_url('view', obj)
|
||||
|
||||
def make_row_grid(self, factory=None, key=None, data=None, columns=None,
|
||||
session=None, **kwargs):
|
||||
def make_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
|
||||
"""
|
||||
Make and return a new (configured) rows grid instance.
|
||||
"""
|
||||
|
@ -613,9 +611,7 @@ class MasterView(View):
|
|||
|
||||
# delete action
|
||||
if self.rows_deletable and self.has_perm('delete_row'):
|
||||
actions.append(self.make_action('delete', icon='trash',
|
||||
url=self.row_delete_action_url,
|
||||
link_class='has-text-danger'))
|
||||
actions.append(self.make_action('delete', icon='trash', url=self.row_delete_action_url))
|
||||
defaults['delete_speedbump'] = self.rows_deletable_speedbump
|
||||
|
||||
defaults['actions'] = actions
|
||||
|
@ -903,7 +899,7 @@ class MasterView(View):
|
|||
|
||||
def valid_employee_uuid(self, node, value):
|
||||
if value:
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
employee = self.Session.get(model.Employee, value)
|
||||
if not employee:
|
||||
node.raise_invalid("Employee not found")
|
||||
|
@ -939,7 +935,7 @@ class MasterView(View):
|
|||
|
||||
def valid_vendor_uuid(self, node, value):
|
||||
if value:
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
vendor = self.Session.get(model.Vendor, value)
|
||||
if not vendor:
|
||||
node.raise_invalid("Vendor not found")
|
||||
|
@ -1187,7 +1183,7 @@ class MasterView(View):
|
|||
|
||||
# If user just refreshed the page with a reset instruction, issue a
|
||||
# redirect in order to clear out the query string.
|
||||
if self.request.GET.get('reset-view'):
|
||||
if self.request.GET.get('reset-to-default-filters') == 'true':
|
||||
kw = {'_query': None}
|
||||
hash_ = self.request.GET.get('hash')
|
||||
if hash_:
|
||||
|
@ -1382,7 +1378,7 @@ class MasterView(View):
|
|||
return classes
|
||||
|
||||
def make_revisions_grid(self, obj, empty_data=False):
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
route_prefix = self.get_route_prefix()
|
||||
row_url = lambda txn, i: self.request.route_url(f'{route_prefix}.version',
|
||||
uuid=obj.uuid,
|
||||
|
@ -1710,7 +1706,7 @@ class MasterView(View):
|
|||
kwargs.setdefault('paginated', False)
|
||||
kwargs.setdefault('sortable', sort)
|
||||
grid = self.make_row_grid(session=session, **kwargs)
|
||||
return grid.get_visible_data()
|
||||
return grid.make_visible_data()
|
||||
|
||||
@classmethod
|
||||
def get_row_url_prefix(cls):
|
||||
|
@ -1824,26 +1820,6 @@ class MasterView(View):
|
|||
path = os.path.join(basedir, filespec)
|
||||
return self.file_response(path)
|
||||
|
||||
def download_output_file_template(self):
|
||||
"""
|
||||
View for downloading an output file template.
|
||||
"""
|
||||
key = self.request.GET['key']
|
||||
filespec = self.request.GET['file']
|
||||
|
||||
matches = [tmpl for tmpl in self.get_output_file_templates()
|
||||
if tmpl['key'] == key]
|
||||
if not matches:
|
||||
raise self.notfound()
|
||||
|
||||
template = matches[0]
|
||||
templatesdir = os.path.join(self.rattail_config.datadir(),
|
||||
'templates', 'output_files',
|
||||
self.get_route_prefix())
|
||||
basedir = os.path.join(templatesdir, template['key'])
|
||||
path = os.path.join(basedir, filespec)
|
||||
return self.file_response(path)
|
||||
|
||||
def edit(self):
|
||||
"""
|
||||
View for editing an existing model record.
|
||||
|
@ -2153,7 +2129,7 @@ class MasterView(View):
|
|||
Thread target for executing an object.
|
||||
"""
|
||||
app = self.get_rattail_app()
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
session = app.make_session()
|
||||
obj = self.get_instance_for_key(key, session)
|
||||
user = session.get(model.User, user_uuid)
|
||||
|
@ -2594,7 +2570,7 @@ class MasterView(View):
|
|||
"""
|
||||
# nb. self.Session may differ, so use tailbone.db.Session
|
||||
session = Session()
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
route_prefix = self.get_route_prefix()
|
||||
|
||||
info = session.query(model.TailbonePageHelp)\
|
||||
|
@ -2617,7 +2593,7 @@ class MasterView(View):
|
|||
"""
|
||||
# nb. self.Session may differ, so use tailbone.db.Session
|
||||
session = Session()
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
route_prefix = self.get_route_prefix()
|
||||
|
||||
info = session.query(model.TailbonePageHelp)\
|
||||
|
@ -2639,7 +2615,7 @@ class MasterView(View):
|
|||
|
||||
# nb. self.Session may differ, so use tailbone.db.Session
|
||||
session = Session()
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
route_prefix = self.get_route_prefix()
|
||||
schema = colander.Schema()
|
||||
|
||||
|
@ -2673,7 +2649,7 @@ class MasterView(View):
|
|||
|
||||
# nb. self.Session may differ, so use tailbone.db.Session
|
||||
session = Session()
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
route_prefix = self.get_route_prefix()
|
||||
schema = colander.Schema()
|
||||
|
||||
|
@ -2872,12 +2848,6 @@ class MasterView(View):
|
|||
kwargs['input_file_templates'] = OrderedDict([(tmpl['key'], tmpl)
|
||||
for tmpl in templates])
|
||||
|
||||
# add info for downloadable output file templates, if any
|
||||
if self.has_output_file_templates:
|
||||
templates = self.normalize_output_file_templates()
|
||||
kwargs['output_file_templates'] = OrderedDict([(tmpl['key'], tmpl)
|
||||
for tmpl in templates])
|
||||
|
||||
return kwargs
|
||||
|
||||
def get_input_file_templates(self):
|
||||
|
@ -2952,81 +2922,6 @@ class MasterView(View):
|
|||
|
||||
return templates
|
||||
|
||||
def get_output_file_templates(self):
|
||||
return []
|
||||
|
||||
def normalize_output_file_templates(self, templates=None,
|
||||
include_file_options=False):
|
||||
if templates is None:
|
||||
templates = self.get_output_file_templates()
|
||||
|
||||
route_prefix = self.get_route_prefix()
|
||||
|
||||
if include_file_options:
|
||||
templatesdir = os.path.join(self.rattail_config.datadir(),
|
||||
'templates', 'output_files',
|
||||
route_prefix)
|
||||
|
||||
for template in templates:
|
||||
|
||||
if 'config_section' not in template:
|
||||
if hasattr(self, 'output_file_template_config_section'):
|
||||
template['config_section'] = self.output_file_template_config_section
|
||||
else:
|
||||
template['config_section'] = route_prefix
|
||||
section = template['config_section']
|
||||
|
||||
if 'config_prefix' not in template:
|
||||
template['config_prefix'] = '{}.{}'.format(
|
||||
self.output_file_template_config_prefix,
|
||||
template['key'])
|
||||
prefix = template['config_prefix']
|
||||
|
||||
for key in ('mode', 'file', 'url'):
|
||||
|
||||
if 'option_{}'.format(key) not in template:
|
||||
template['option_{}'.format(key)] = '{}.{}'.format(prefix, key)
|
||||
|
||||
if 'setting_{}'.format(key) not in template:
|
||||
template['setting_{}'.format(key)] = '{}.{}'.format(
|
||||
section,
|
||||
template['option_{}'.format(key)])
|
||||
|
||||
if key not in template:
|
||||
value = self.rattail_config.get(
|
||||
section,
|
||||
template['option_{}'.format(key)])
|
||||
if value is not None:
|
||||
template[key] = value
|
||||
|
||||
template.setdefault('mode', 'default')
|
||||
template.setdefault('file', None)
|
||||
template.setdefault('url', template['default_url'])
|
||||
|
||||
if include_file_options:
|
||||
options = []
|
||||
basedir = os.path.join(templatesdir, template['key'])
|
||||
if os.path.exists(basedir):
|
||||
for name in sorted(os.listdir(basedir)):
|
||||
if len(name) == 4 and name.isdigit():
|
||||
files = os.listdir(os.path.join(basedir, name))
|
||||
if len(files) == 1:
|
||||
options.append(os.path.join(name, files[0]))
|
||||
template['file_options'] = options
|
||||
template['file_options_dir'] = basedir
|
||||
|
||||
if template['mode'] == 'external':
|
||||
template['effective_url'] = template['url']
|
||||
elif template['mode'] == 'hosted':
|
||||
template['effective_url'] = self.request.route_url(
|
||||
'{}.download_output_file_template'.format(route_prefix),
|
||||
_query={'key': template['key'],
|
||||
'file': template['file']})
|
||||
else:
|
||||
template['effective_url'] = template['default_url']
|
||||
|
||||
return templates
|
||||
|
||||
def template_kwargs_index(self, **kwargs):
|
||||
"""
|
||||
Method stub, so subclass can always invoke super() for it.
|
||||
|
@ -3074,12 +2969,6 @@ class MasterView(View):
|
|||
items.append(tags.link_to(f"Download {template['label']} Template",
|
||||
template['effective_url']))
|
||||
|
||||
if self.has_output_file_templates and self.has_perm('configure'):
|
||||
templates = self.normalize_output_file_templates()
|
||||
for template in templates:
|
||||
items.append(tags.link_to(f"Download {template['label']} Template",
|
||||
template['effective_url']))
|
||||
|
||||
# if self.viewing:
|
||||
|
||||
# # # TODO: either make this configurable, or just lose it.
|
||||
|
@ -3325,7 +3214,7 @@ class MasterView(View):
|
|||
url=self.default_clone_url)
|
||||
|
||||
def make_grid_action_delete(self):
|
||||
kwargs = {'link_class': 'has-text-danger'}
|
||||
kwargs = {}
|
||||
if self.delete_confirm == 'simple':
|
||||
kwargs['click_handler'] = 'deleteObject'
|
||||
return self.make_action('delete', icon='trash', url=self.default_delete_url, **kwargs)
|
||||
|
@ -5315,39 +5204,6 @@ class MasterView(View):
|
|||
data[template['setting_file']] = os.path.join(numdir,
|
||||
info['filename'])
|
||||
|
||||
if self.has_output_file_templates:
|
||||
templatesdir = os.path.join(self.rattail_config.datadir(),
|
||||
'templates', 'output_files',
|
||||
self.get_route_prefix())
|
||||
|
||||
def get_next_filedir(basedir):
|
||||
nextid = 1
|
||||
while True:
|
||||
path = os.path.join(basedir, '{:04d}'.format(nextid))
|
||||
if not os.path.exists(path):
|
||||
# this should fail if there happens to be a race
|
||||
# condition and someone else got to this id first
|
||||
os.mkdir(path)
|
||||
return path
|
||||
nextid += 1
|
||||
|
||||
for template in self.normalize_output_file_templates():
|
||||
key = '{}.upload'.format(template['setting_file'])
|
||||
if key in uploads:
|
||||
assert self.request.POST[template['setting_mode']] == 'hosted'
|
||||
assert not self.request.POST[template['setting_file']]
|
||||
info = uploads[key]
|
||||
basedir = os.path.join(templatesdir, template['key'])
|
||||
if not os.path.exists(basedir):
|
||||
os.makedirs(basedir)
|
||||
filedir = get_next_filedir(basedir)
|
||||
filepath = os.path.join(filedir, info['filename'])
|
||||
shutil.copyfile(info['filepath'], filepath)
|
||||
shutil.rmtree(info['filedir'])
|
||||
numdir = os.path.basename(filedir)
|
||||
data[template['setting_file']] = os.path.join(numdir,
|
||||
info['filename'])
|
||||
|
||||
def configure_get_simple_settings(self):
|
||||
"""
|
||||
If you have some "simple" settings, each of which basically
|
||||
|
@ -5392,8 +5248,7 @@ class MasterView(View):
|
|||
simple['option'])
|
||||
|
||||
def configure_get_context(self, simple_settings=None,
|
||||
input_file_templates=True,
|
||||
output_file_templates=True):
|
||||
input_file_templates=True):
|
||||
"""
|
||||
Returns the full context dict, for rendering the configure
|
||||
page template.
|
||||
|
@ -5442,7 +5297,7 @@ class MasterView(View):
|
|||
for template in self.normalize_input_file_templates(
|
||||
include_file_options=True):
|
||||
settings[template['setting_mode']] = template['mode']
|
||||
settings[template['setting_file']] = template['file'] or ''
|
||||
settings[template['setting_file']] = template['file']
|
||||
settings[template['setting_url']] = template['url']
|
||||
file_options[template['key']] = template['file_options']
|
||||
file_option_dirs[template['key']] = template['file_options_dir']
|
||||
|
@ -5450,27 +5305,10 @@ class MasterView(View):
|
|||
context['input_file_options'] = file_options
|
||||
context['input_file_option_dirs'] = file_option_dirs
|
||||
|
||||
# add settings for output file templates, if any
|
||||
if output_file_templates and self.has_output_file_templates:
|
||||
settings = {}
|
||||
file_options = {}
|
||||
file_option_dirs = {}
|
||||
for template in self.normalize_output_file_templates(
|
||||
include_file_options=True):
|
||||
settings[template['setting_mode']] = template['mode']
|
||||
settings[template['setting_file']] = template['file'] 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
|
||||
|
||||
def configure_gather_settings(self, data, simple_settings=None,
|
||||
input_file_templates=True,
|
||||
output_file_templates=True):
|
||||
input_file_templates=True):
|
||||
settings = []
|
||||
|
||||
# maybe collect "simple" settings
|
||||
|
@ -5516,32 +5354,12 @@ class MasterView(View):
|
|||
settings.append({'name': template['setting_url'],
|
||||
'value': data.get(template['setting_url'])})
|
||||
|
||||
# maybe also collect output file template settings
|
||||
if output_file_templates and self.has_output_file_templates:
|
||||
for template in self.normalize_output_file_templates():
|
||||
|
||||
# mode
|
||||
settings.append({'name': template['setting_mode'],
|
||||
'value': data.get(template['setting_mode'])})
|
||||
|
||||
# file
|
||||
value = data.get(template['setting_file'])
|
||||
if value:
|
||||
# nb. avoid saving if empty, so can remain "null"
|
||||
settings.append({'name': template['setting_file'],
|
||||
'value': value})
|
||||
|
||||
# url
|
||||
settings.append({'name': template['setting_url'],
|
||||
'value': data.get(template['setting_url'])})
|
||||
|
||||
return settings
|
||||
|
||||
def configure_remove_settings(self, simple_settings=None,
|
||||
input_file_templates=True,
|
||||
output_file_templates=True):
|
||||
input_file_templates=True):
|
||||
app = self.get_rattail_app()
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
names = []
|
||||
|
||||
if simple_settings is None:
|
||||
|
@ -5558,14 +5376,6 @@ class MasterView(View):
|
|||
template['setting_url'],
|
||||
])
|
||||
|
||||
if output_file_templates and self.has_output_file_templates:
|
||||
for template in self.normalize_output_file_templates():
|
||||
names.extend([
|
||||
template['setting_mode'],
|
||||
template['setting_file'],
|
||||
template['setting_url'],
|
||||
])
|
||||
|
||||
if names:
|
||||
# nb. using thread-local session here; we do not use
|
||||
# self.Session b/c it may not point to Rattail
|
||||
|
@ -5828,15 +5638,6 @@ class MasterView(View):
|
|||
route_name='{}.download_input_file_template'.format(route_prefix),
|
||||
permission='{}.create'.format(permission_prefix))
|
||||
|
||||
# download output file template
|
||||
if cls.has_output_file_templates and cls.configurable:
|
||||
config.add_route(f'{route_prefix}.download_output_file_template',
|
||||
f'{url_prefix}/download-output-file-template')
|
||||
config.add_view(cls, attr='download_output_file_template',
|
||||
route_name=f'{route_prefix}.download_output_file_template',
|
||||
# TODO: this is different from input file, should change?
|
||||
permission=f'{permission_prefix}.configure')
|
||||
|
||||
# view
|
||||
if cls.viewable:
|
||||
cls._defaults_view(config)
|
||||
|
@ -6100,7 +5901,7 @@ class MasterView(View):
|
|||
renderer='json')
|
||||
|
||||
|
||||
class ViewSupplement:
|
||||
class ViewSupplement(object):
|
||||
"""
|
||||
Base class for view "supplements" - which are sort of like plugins
|
||||
which can "supplement" certain aspects of the view.
|
||||
|
@ -6127,7 +5928,6 @@ class ViewSupplement:
|
|||
def __init__(self, master):
|
||||
self.master = master
|
||||
self.request = master.request
|
||||
self.app = master.app
|
||||
self.model = master.model
|
||||
self.rattail_config = master.rattail_config
|
||||
self.Session = master.Session
|
||||
|
@ -6161,7 +5961,7 @@ class ViewSupplement:
|
|||
This is accomplished by subjecting the current base query to a
|
||||
join, e.g. something like::
|
||||
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
query = query.outerjoin(model.MyExtension)
|
||||
return query
|
||||
"""
|
||||
|
|
|
@ -564,19 +564,15 @@ class PersonView(MasterView):
|
|||
Method which must return the base query for the profile's POS
|
||||
Transactions grid data.
|
||||
"""
|
||||
customer = self.app.get_customer(person)
|
||||
app = self.get_rattail_app()
|
||||
customer = app.get_customer(person)
|
||||
|
||||
if customer:
|
||||
key_field = self.app.get_customer_key_field()
|
||||
key_field = app.get_customer_key_field()
|
||||
customer_key = getattr(customer, key_field)
|
||||
if customer_key is not None:
|
||||
customer_key = str(customer_key)
|
||||
else:
|
||||
# nb. this should *not* match anything, so query returns
|
||||
# no results..
|
||||
customer_key = person.uuid
|
||||
|
||||
trainwreck = self.app.get_trainwreck_handler()
|
||||
trainwreck = app.get_trainwreck_handler()
|
||||
model = trainwreck.get_model()
|
||||
query = TrainwreckSession.query(model.Transaction)\
|
||||
.filter(model.Transaction.customer_id == customer_key)
|
||||
|
@ -1386,7 +1382,7 @@ class PersonView(MasterView):
|
|||
}
|
||||
|
||||
if not context['users']:
|
||||
context['suggested_username'] = auth.make_unique_username(self.Session(),
|
||||
context['suggested_username'] = auth.generate_unique_username(self.Session(),
|
||||
person=person)
|
||||
|
||||
return context
|
||||
|
|
|
@ -34,7 +34,7 @@ import sqlalchemy_continuum as continuum
|
|||
|
||||
from rattail import enum, pod, sil
|
||||
from rattail.db import api, auth, Session as RattailSession
|
||||
from rattail.db.model import Product, PendingProduct, ProductCost, CustomerOrderItem
|
||||
from rattail.db.model import Product, PendingProduct, CustomerOrderItem
|
||||
from rattail.gpc import GPC
|
||||
from rattail.threads import Thread
|
||||
from rattail.exceptions import LabelPrintingError
|
||||
|
@ -1857,8 +1857,7 @@ class ProductView(MasterView):
|
|||
lookup_fields.append('alt_code')
|
||||
if lookup_fields:
|
||||
product = self.products_handler.locate_product_for_entry(
|
||||
session, term, lookup_fields=lookup_fields,
|
||||
first_if_multiple=True)
|
||||
session, term, lookup_fields=lookup_fields)
|
||||
if product:
|
||||
final_results.append(self.search_normalize_result(product))
|
||||
|
||||
|
@ -2669,78 +2668,6 @@ class PendingProductView(MasterView):
|
|||
permission=f'{permission_prefix}.ignore_product')
|
||||
|
||||
|
||||
class ProductCostView(MasterView):
|
||||
"""
|
||||
Master view for Product Costs
|
||||
"""
|
||||
model_class = ProductCost
|
||||
route_prefix = 'product_costs'
|
||||
url_prefix = '/products/costs'
|
||||
has_versions = True
|
||||
|
||||
grid_columns = [
|
||||
'_product_key_',
|
||||
'vendor',
|
||||
'preference',
|
||||
'code',
|
||||
'case_size',
|
||||
'case_cost',
|
||||
'pack_size',
|
||||
'pack_cost',
|
||||
'unit_cost',
|
||||
]
|
||||
|
||||
def query(self, session):
|
||||
""" """
|
||||
query = super().query(session)
|
||||
model = self.app.model
|
||||
|
||||
# always join on Product
|
||||
return query.join(model.Product)
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
model = self.app.model
|
||||
|
||||
# product key
|
||||
field = self.get_product_key_field()
|
||||
g.set_renderer(field, self.render_product_key)
|
||||
g.set_sorter(field, getattr(model.Product, field))
|
||||
g.set_sort_defaults(field)
|
||||
g.set_filter(field, getattr(model.Product, field))
|
||||
|
||||
# vendor
|
||||
g.set_joiner('vendor', lambda q: q.join(model.Vendor))
|
||||
g.set_sorter('vendor', model.Vendor.name)
|
||||
g.set_filter('vendor', model.Vendor.name, label="Vendor Name")
|
||||
|
||||
def render_product_key(self, cost, field):
|
||||
""" """
|
||||
handler = self.app.get_products_handler()
|
||||
return handler.render_product_key(cost.product)
|
||||
|
||||
def configure_form(self, f):
|
||||
""" """
|
||||
super().configure_form(f)
|
||||
|
||||
# product
|
||||
f.set_renderer('product', self.render_product)
|
||||
if 'product_uuid' in f and 'product' in f:
|
||||
f.remove('product')
|
||||
f.replace('product_uuid', 'product')
|
||||
|
||||
# vendor
|
||||
f.set_renderer('vendor', self.render_vendor)
|
||||
if 'vendor_uuid' in f and 'vendor' in f:
|
||||
f.remove('vendor')
|
||||
f.replace('vendor_uuid', 'vendor')
|
||||
|
||||
# futures
|
||||
# TODO: should eventually show a subgrid here?
|
||||
f.remove('futures')
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
|
@ -2750,9 +2677,6 @@ def defaults(config, **kwargs):
|
|||
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
|
||||
PendingProductView.defaults(config)
|
||||
|
||||
ProductCostView = kwargs.get('ProductCostView', base['ProductCostView'])
|
||||
ProductCostView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
||||
|
|
|
@ -24,8 +24,6 @@
|
|||
Base class for purchasing batch views
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
|
||||
|
||||
import colander
|
||||
|
@ -69,8 +67,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
'store',
|
||||
'buyer',
|
||||
'vendor',
|
||||
'description',
|
||||
'workflow',
|
||||
'department',
|
||||
'purchase',
|
||||
'vendor_email',
|
||||
|
@ -162,174 +158,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
def batch_mode(self):
|
||||
raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
|
||||
|
||||
def get_supported_workflows(self):
|
||||
"""
|
||||
Return the supported "create batch" workflows.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING:
|
||||
return self.batch_handler.supported_ordering_workflows()
|
||||
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING:
|
||||
return self.batch_handler.supported_receiving_workflows()
|
||||
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_COSTING:
|
||||
return self.batch_handler.supported_costing_workflows()
|
||||
raise ValueError("unknown batch mode")
|
||||
|
||||
def allow_any_vendor(self):
|
||||
"""
|
||||
Return boolean indicating whether creating a batch for "any"
|
||||
vendor is allowed, vs. only supported vendors.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
|
||||
if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING:
|
||||
return self.batch_handler.allow_ordering_any_vendor()
|
||||
|
||||
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING:
|
||||
value = self.config.get_bool('rattail.batch.purchase.allow_receiving_any_vendor')
|
||||
if value is not None:
|
||||
return value
|
||||
value = self.config.get_bool('rattail.batch.purchase.supported_vendors_only')
|
||||
if value is not None:
|
||||
warnings.warn("setting rattail.batch.purchase.supported_vendors_only is deprecated; "
|
||||
"please use rattail.batch.purchase.allow_receiving_any_vendor instead",
|
||||
DeprecationWarning)
|
||||
# nb. must negate this setting
|
||||
return not value
|
||||
return False
|
||||
|
||||
raise ValueError("unknown batch mode")
|
||||
|
||||
def get_supported_vendors(self):
|
||||
"""
|
||||
Return the supported vendors for creating a batch.
|
||||
"""
|
||||
return []
|
||||
|
||||
def create(self, form=None, **kwargs):
|
||||
"""
|
||||
Custom view for creating a new batch. We split the process
|
||||
into two steps, 1) choose workflow and 2) create batch. This
|
||||
is because the specific form details for creating a batch will
|
||||
depend on which "type" of batch creation is to be done, and
|
||||
it's much easier to keep conditional logic for that in the
|
||||
server instead of client-side etc.
|
||||
"""
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
route_prefix = self.get_route_prefix()
|
||||
|
||||
workflows = self.get_supported_workflows()
|
||||
valid_workflows = [workflow['workflow_key']
|
||||
for workflow in workflows]
|
||||
|
||||
# if user has already identified their desired workflow, then
|
||||
# we can just farm out to the default logic. we will of
|
||||
# course configure our form differently, based on workflow,
|
||||
# but this create() method at least will not need
|
||||
# customization for that.
|
||||
if self.request.matched_route.name.endswith('create_workflow'):
|
||||
|
||||
redirect = self.redirect(self.request.route_url(f'{route_prefix}.create'))
|
||||
|
||||
# however we do have one more thing to check - the workflow
|
||||
# requested must of course be valid!
|
||||
workflow_key = self.request.matchdict['workflow_key']
|
||||
if workflow_key not in valid_workflows:
|
||||
self.request.session.flash(f"Not a supported workflow: {workflow_key}", 'error')
|
||||
raise redirect
|
||||
|
||||
# also, we require vendor to be correctly identified. if
|
||||
# someone e.g. navigates to a URL by accident etc. we want
|
||||
# to gracefully handle and redirect
|
||||
uuid = self.request.matchdict['vendor_uuid']
|
||||
vendor = self.Session.get(model.Vendor, uuid)
|
||||
if not vendor:
|
||||
self.request.session.flash("Invalid vendor selection. "
|
||||
"Please choose an existing vendor.",
|
||||
'warning')
|
||||
raise redirect
|
||||
|
||||
# okay now do the normal thing, per workflow
|
||||
return super().create(**kwargs)
|
||||
|
||||
# on the other hand, if caller provided a form, that means we are in
|
||||
# the middle of some other custom workflow, e.g. "add child to truck
|
||||
# dump parent" or some such. in which case we also defer to the normal
|
||||
# logic, so as to not interfere with that.
|
||||
if form:
|
||||
return super().create(form=form, **kwargs)
|
||||
|
||||
# okay, at this point we need the user to select a vendor and workflow
|
||||
self.creating = True
|
||||
context = {}
|
||||
|
||||
# form to accept user choice of vendor/workflow
|
||||
schema = colander.Schema()
|
||||
schema.add(colander.SchemaNode(colander.String(), name='vendor'))
|
||||
schema.add(colander.SchemaNode(colander.String(), name='workflow',
|
||||
validator=colander.OneOf(valid_workflows)))
|
||||
factory = self.get_form_factory()
|
||||
form = factory(schema=schema, request=self.request)
|
||||
|
||||
# configure vendor field
|
||||
vendor_handler = self.app.get_vendor_handler()
|
||||
if self.allow_any_vendor():
|
||||
# user may choose *any* available vendor
|
||||
use_dropdown = vendor_handler.choice_uses_dropdown()
|
||||
if use_dropdown:
|
||||
vendors = self.Session.query(model.Vendor)\
|
||||
.order_by(model.Vendor.id)\
|
||||
.all()
|
||||
vendor_values = [(vendor.uuid, f"({vendor.id}) {vendor.name}")
|
||||
for vendor in vendors]
|
||||
form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values))
|
||||
if len(vendors) == 1:
|
||||
form.set_default('vendor', vendors[0].uuid)
|
||||
else:
|
||||
vendor_display = ""
|
||||
if self.request.method == 'POST':
|
||||
if self.request.POST.get('vendor'):
|
||||
vendor = self.Session.get(model.Vendor, self.request.POST['vendor'])
|
||||
if vendor:
|
||||
vendor_display = str(vendor)
|
||||
vendors_url = self.request.route_url('vendors.autocomplete')
|
||||
form.set_widget('vendor', forms.widgets.JQueryAutocompleteWidget(
|
||||
field_display=vendor_display, service_url=vendors_url))
|
||||
else: # only "supported" vendors allowed
|
||||
vendors = self.get_supported_vendors()
|
||||
vendor_values = [(vendor.uuid, vendor_handler.render_vendor(vendor))
|
||||
for vendor in vendors]
|
||||
form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values))
|
||||
form.set_validator('vendor', self.valid_vendor_uuid)
|
||||
|
||||
# configure workflow field
|
||||
values = [(workflow['workflow_key'], workflow['display'])
|
||||
for workflow in workflows]
|
||||
form.set_widget('workflow',
|
||||
dfwidget.SelectWidget(values=values))
|
||||
if len(workflows) == 1:
|
||||
form.set_default('workflow', workflows[0]['workflow_key'])
|
||||
|
||||
form.submit_label = "Continue"
|
||||
form.cancel_url = self.get_index_url()
|
||||
|
||||
# if form validates, that means user has chosen a creation
|
||||
# type, so we just redirect to the appropriate "new batch of
|
||||
# type X" page
|
||||
if form.validate():
|
||||
workflow_key = form.validated['workflow']
|
||||
vendor_uuid = form.validated['vendor']
|
||||
url = self.request.route_url(f'{route_prefix}.create_workflow',
|
||||
workflow_key=workflow_key,
|
||||
vendor_uuid=vendor_uuid)
|
||||
raise self.redirect(url)
|
||||
|
||||
context['form'] = form
|
||||
if hasattr(form, 'make_deform_form'):
|
||||
context['dform'] = form.make_deform_form()
|
||||
return self.render_to_response('create', context)
|
||||
|
||||
def query(self, session):
|
||||
model = self.model
|
||||
return session.query(model.PurchaseBatch)\
|
||||
|
@ -398,40 +226,20 @@ class PurchasingBatchView(BatchMasterView):
|
|||
|
||||
def configure_form(self, f):
|
||||
super().configure_form(f)
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
route_prefix = self.get_route_prefix()
|
||||
|
||||
today = self.app.today()
|
||||
model = self.model
|
||||
batch = f.model_instance
|
||||
workflow = self.request.matchdict.get('workflow_key')
|
||||
vendor_handler = self.app.get_vendor_handler()
|
||||
app = self.get_rattail_app()
|
||||
today = app.localtime().date()
|
||||
|
||||
# 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')
|
||||
f.set_enum('mode', self.enum.PURCHASE_BATCH_MODE)
|
||||
|
||||
# store
|
||||
single_store = self.config.single_store()
|
||||
single_store = self.rattail_config.single_store()
|
||||
if self.creating:
|
||||
f.replace('store', 'store_uuid')
|
||||
if single_store:
|
||||
store = self.config.get_store(self.Session())
|
||||
store = self.rattail_config.get_store(self.Session())
|
||||
f.set_widget('store_uuid', dfwidget.HiddenWidget())
|
||||
f.set_default('store_uuid', store.uuid)
|
||||
f.set_hidden('store_uuid')
|
||||
|
@ -455,6 +263,7 @@ class PurchasingBatchView(BatchMasterView):
|
|||
if self.creating:
|
||||
f.replace('vendor', 'vendor_uuid')
|
||||
f.set_label('vendor_uuid', "Vendor")
|
||||
vendor_handler = app.get_vendor_handler()
|
||||
use_dropdown = vendor_handler.choice_uses_dropdown()
|
||||
if use_dropdown:
|
||||
vendors = self.Session.query(model.Vendor)\
|
||||
|
@ -504,7 +313,7 @@ class PurchasingBatchView(BatchMasterView):
|
|||
if buyer:
|
||||
buyer_display = str(buyer)
|
||||
elif self.creating:
|
||||
buyer = self.app.get_employee(self.request.user)
|
||||
buyer = app.get_employee(self.request.user)
|
||||
if buyer:
|
||||
buyer_display = str(buyer)
|
||||
f.set_default('buyer_uuid', buyer.uuid)
|
||||
|
@ -515,30 +324,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
field_display=buyer_display, service_url=buyers_url))
|
||||
f.set_label('buyer_uuid', "Buyer")
|
||||
|
||||
# order_file
|
||||
if self.creating:
|
||||
f.set_type('order_file', 'file', required=False)
|
||||
else:
|
||||
f.set_readonly('order_file')
|
||||
f.set_renderer('order_file', self.render_downloadable_file)
|
||||
|
||||
# order_parser_key
|
||||
if self.creating:
|
||||
kwargs = {}
|
||||
if 'vendor_uuid' in self.request.matchdict:
|
||||
vendor = self.Session.get(model.Vendor,
|
||||
self.request.matchdict['vendor_uuid'])
|
||||
if vendor:
|
||||
kwargs['vendor'] = vendor
|
||||
parsers = vendor_handler.get_supported_order_parsers(**kwargs)
|
||||
parser_values = [(p.key, p.title) for p in parsers]
|
||||
if len(parsers) == 1:
|
||||
f.set_default('order_parser_key', parsers[0].key)
|
||||
f.set_widget('order_parser_key', dfwidget.SelectWidget(values=parser_values))
|
||||
f.set_label('order_parser_key', "Order Parser")
|
||||
else:
|
||||
f.remove_field('order_parser_key')
|
||||
|
||||
# invoice_file
|
||||
if self.creating:
|
||||
f.set_type('invoice_file', 'file', required=False)
|
||||
|
@ -556,7 +341,7 @@ class PurchasingBatchView(BatchMasterView):
|
|||
if vendor:
|
||||
kwargs['vendor'] = vendor
|
||||
|
||||
parsers = self.batch_handler.get_supported_invoice_parsers(**kwargs)
|
||||
parsers = self.handler.get_supported_invoice_parsers(**kwargs)
|
||||
parser_values = [(p.key, p.display) for p in parsers]
|
||||
if len(parsers) == 1:
|
||||
f.set_default('invoice_parser_key', parsers[0].key)
|
||||
|
@ -615,35 +400,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
'vendor_contact',
|
||||
'status_code')
|
||||
|
||||
# tweak some things if we are in "step 2" of creating new batch
|
||||
if self.creating and workflow:
|
||||
|
||||
# display vendor but do not allow changing
|
||||
vendor = self.Session.get(model.Vendor, self.request.matchdict['vendor_uuid'])
|
||||
if not vendor:
|
||||
raise ValueError(f"vendor not found: {self.request.matchdict['vendor_uuid']}")
|
||||
f.set_readonly('vendor_uuid')
|
||||
f.set_default('vendor_uuid', str(vendor))
|
||||
|
||||
# cancel should take us back to choosing a workflow
|
||||
f.cancel_url = self.request.route_url(f'{route_prefix}.create')
|
||||
|
||||
def render_workflow(self, batch, field):
|
||||
key = self.request.matchdict['workflow_key']
|
||||
info = self.get_workflow_info(key)
|
||||
if info:
|
||||
return info['display']
|
||||
|
||||
def get_workflow_info(self, key):
|
||||
enum = self.app.enum
|
||||
if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING:
|
||||
return self.batch_handler.ordering_workflow_info(key)
|
||||
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING:
|
||||
return self.batch_handler.receiving_workflow_info(key)
|
||||
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_COSTING:
|
||||
return self.batch_handler.costing_workflow_info(key)
|
||||
raise ValueError("unknown batch mode")
|
||||
|
||||
def render_store(self, batch, field):
|
||||
store = batch.store
|
||||
if not store:
|
||||
|
@ -759,12 +515,10 @@ class PurchasingBatchView(BatchMasterView):
|
|||
|
||||
def get_batch_kwargs(self, batch, **kwargs):
|
||||
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
||||
model = self.app.model
|
||||
model = self.model
|
||||
|
||||
kwargs['mode'] = self.batch_mode
|
||||
kwargs['workflow'] = self.request.POST['workflow']
|
||||
kwargs['truck_dump'] = batch.truck_dump
|
||||
kwargs['order_parser_key'] = batch.order_parser_key
|
||||
kwargs['invoice_parser_key'] = batch.invoice_parser_key
|
||||
|
||||
if batch.store:
|
||||
|
@ -782,11 +536,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
elif batch.vendor_uuid:
|
||||
kwargs['vendor_uuid'] = batch.vendor_uuid
|
||||
|
||||
# must pull vendor from URL if it was not in form data
|
||||
if 'vendor_uuid' not in kwargs and 'vendor' not in kwargs:
|
||||
if 'vendor_uuid' in self.request.matchdict:
|
||||
kwargs['vendor_uuid'] = self.request.matchdict['vendor_uuid']
|
||||
|
||||
if batch.department:
|
||||
kwargs['department'] = batch.department
|
||||
elif batch.department_uuid:
|
||||
|
@ -1170,25 +919,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
# # otherwise just view batch again
|
||||
# return self.get_action_url('view', batch)
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
cls._purchase_batch_defaults(config)
|
||||
cls._batch_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _purchase_batch_defaults(cls, config):
|
||||
route_prefix = cls.get_route_prefix()
|
||||
url_prefix = cls.get_url_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
|
||||
# new batch using workflow X
|
||||
config.add_route(f'{route_prefix}.create_workflow',
|
||||
f'{url_prefix}/new/{{workflow_key}}/{{vendor_uuid}}')
|
||||
config.add_view(cls, attr='create',
|
||||
route_name=f'{route_prefix}.create_workflow',
|
||||
permission=f'{permission_prefix}.create')
|
||||
|
||||
|
||||
class NewProduct(colander.Schema):
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2024 Lance Edgar
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -28,10 +28,14 @@ import os
|
|||
import json
|
||||
|
||||
import openpyxl
|
||||
from sqlalchemy import orm
|
||||
|
||||
from rattail.db import model, api
|
||||
from rattail.core import Object
|
||||
from rattail.time import localtime
|
||||
|
||||
from webhelpers2.html import tags
|
||||
|
||||
from tailbone.db import Session
|
||||
from tailbone.views.purchasing import PurchasingBatchView
|
||||
|
||||
|
||||
|
@ -47,8 +51,6 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
rows_editable = True
|
||||
has_worksheet = True
|
||||
default_help_url = 'https://rattailproject.org/docs/rattail-manual/features/purchasing/ordering/index.html'
|
||||
downloadable = True
|
||||
configurable = True
|
||||
|
||||
labels = {
|
||||
'po_total_calculated': "PO Total",
|
||||
|
@ -57,14 +59,9 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
form_fields = [
|
||||
'id',
|
||||
'store',
|
||||
'vendor',
|
||||
'description',
|
||||
'workflow',
|
||||
'order_file',
|
||||
'order_parser_key',
|
||||
'buyer',
|
||||
'vendor',
|
||||
'department',
|
||||
'params',
|
||||
'purchase',
|
||||
'vendor_email',
|
||||
'vendor_fax',
|
||||
|
@ -135,26 +132,15 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
return self.enum.PURCHASE_BATCH_MODE_ORDERING
|
||||
|
||||
def configure_form(self, f):
|
||||
super().configure_form(f)
|
||||
super(OrderingBatchView, self).configure_form(f)
|
||||
batch = f.model_instance
|
||||
workflow = self.request.matchdict.get('workflow_key')
|
||||
|
||||
# purchase
|
||||
if self.creating or not batch.executed or not batch.purchase:
|
||||
f.remove_field('purchase')
|
||||
|
||||
# now that all fields are setup, some final tweaks based on workflow
|
||||
if self.creating and workflow:
|
||||
|
||||
if workflow == 'from_scratch':
|
||||
f.remove('order_file',
|
||||
'order_parser_key')
|
||||
|
||||
elif workflow == 'from_file':
|
||||
f.set_required('order_file')
|
||||
|
||||
def get_batch_kwargs(self, batch, **kwargs):
|
||||
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
||||
kwargs = super(OrderingBatchView, self).get_batch_kwargs(batch, **kwargs)
|
||||
kwargs['ship_method'] = batch.ship_method
|
||||
kwargs['notes_to_vendor'] = batch.notes_to_vendor
|
||||
return kwargs
|
||||
|
@ -169,7 +155,7 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
* ``cases_ordered``
|
||||
* ``units_ordered``
|
||||
"""
|
||||
super().configure_row_form(f)
|
||||
super(OrderingBatchView, self).configure_row_form(f)
|
||||
|
||||
# when editing, only certain fields should allow changes
|
||||
if self.editing:
|
||||
|
@ -322,7 +308,7 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
title = self.get_instance_title(batch)
|
||||
order_date = batch.date_ordered
|
||||
if not order_date:
|
||||
order_date = self.app.today()
|
||||
order_date = localtime(self.rattail_config).date()
|
||||
|
||||
return self.render_to_response('worksheet', {
|
||||
'batch': batch,
|
||||
|
@ -383,7 +369,6 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
of being updated. If a matching row is not found, it will not be
|
||||
created.
|
||||
"""
|
||||
model = self.app.model
|
||||
batch = self.get_instance()
|
||||
|
||||
try:
|
||||
|
@ -493,75 +478,13 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
return self.file_response(path)
|
||||
|
||||
def get_execute_success_url(self, batch, result, **kwargs):
|
||||
model = self.app.model
|
||||
if isinstance(result, model.Purchase):
|
||||
return self.request.route_url('purchases.view', uuid=result.uuid)
|
||||
return super().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)
|
||||
return super(OrderingBatchView, self).get_execute_success_url(batch, result, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
cls._ordering_defaults(config)
|
||||
cls._purchase_batch_defaults(config)
|
||||
cls._batch_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
'store',
|
||||
'vendor',
|
||||
'description',
|
||||
'workflow',
|
||||
'receiving_workflow',
|
||||
'truck_dump',
|
||||
'truck_dump_children_first',
|
||||
'truck_dump_children',
|
||||
|
@ -235,9 +235,75 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
if not self.handler.allow_truck_dump_receiving():
|
||||
g.remove('truck_dump')
|
||||
|
||||
def get_supported_vendors(self):
|
||||
""" """
|
||||
vendor_handler = self.app.get_vendor_handler()
|
||||
def create(self, form=None, **kwargs):
|
||||
"""
|
||||
Custom view for creating a new receiving batch. We split the process
|
||||
into two steps, 1) choose and 2) create. This is because the specific
|
||||
form details for creating a batch will depend on which "type" of batch
|
||||
creation is to be done, and it's much easier to keep conditional logic
|
||||
for that in the server instead of client-side etc.
|
||||
|
||||
See also
|
||||
:meth:`tailbone.views.purchasing.costing:CostingBatchView.create()`
|
||||
which uses similar logic.
|
||||
"""
|
||||
model = self.model
|
||||
route_prefix = self.get_route_prefix()
|
||||
workflows = self.handler.supported_receiving_workflows()
|
||||
valid_workflows = [workflow['workflow_key']
|
||||
for workflow in workflows]
|
||||
|
||||
# if user has already identified their desired workflow, then we can
|
||||
# just farm out to the default logic. we will of course configure our
|
||||
# form differently, based on workflow, but this create() method at
|
||||
# least will not need customization for that.
|
||||
if self.request.matched_route.name.endswith('create_workflow'):
|
||||
|
||||
redirect = self.redirect(self.request.route_url('{}.create'.format(route_prefix)))
|
||||
|
||||
# however we do have one more thing to check - the workflow
|
||||
# requested must of course be valid!
|
||||
workflow_key = self.request.matchdict['workflow_key']
|
||||
if workflow_key not in valid_workflows:
|
||||
self.request.session.flash(
|
||||
"Not a supported workflow: {}".format(workflow_key),
|
||||
'error')
|
||||
raise redirect
|
||||
|
||||
# also, we require vendor to be correctly identified. if
|
||||
# someone e.g. navigates to a URL by accident etc. we want
|
||||
# to gracefully handle and redirect
|
||||
uuid = self.request.matchdict['vendor_uuid']
|
||||
vendor = self.Session.get(model.Vendor, uuid)
|
||||
if not vendor:
|
||||
self.request.session.flash("Invalid vendor selection. "
|
||||
"Please choose an existing vendor.",
|
||||
'warning')
|
||||
raise redirect
|
||||
|
||||
# okay now do the normal thing, per workflow
|
||||
return super().create(**kwargs)
|
||||
|
||||
# on the other hand, if caller provided a form, that means we are in
|
||||
# the middle of some other custom workflow, e.g. "add child to truck
|
||||
# dump parent" or some such. in which case we also defer to the normal
|
||||
# logic, so as to not interfere with that.
|
||||
if form:
|
||||
return super().create(form=form, **kwargs)
|
||||
|
||||
# okay, at this point we need the user to select a vendor and workflow
|
||||
self.creating = True
|
||||
context = {}
|
||||
|
||||
# form to accept user choice of vendor/workflow
|
||||
schema = NewReceivingBatch().bind(valid_workflows=valid_workflows)
|
||||
form = forms.Form(schema=schema, request=self.request)
|
||||
|
||||
# configure vendor field
|
||||
app = self.get_rattail_app()
|
||||
vendor_handler = app.get_vendor_handler()
|
||||
if self.rattail_config.getbool('rattail.batch', 'purchase.supported_vendors_only'):
|
||||
# only show vendors for which we have dedicated invoice parsers
|
||||
vendors = {}
|
||||
for parser in self.batch_handler.get_supported_invoice_parsers():
|
||||
if parser.vendor_key:
|
||||
|
@ -246,7 +312,58 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
if vendor:
|
||||
vendors[vendor.uuid] = vendor
|
||||
vendors = sorted(vendors.values(), key=lambda v: v.name)
|
||||
return vendors
|
||||
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):
|
||||
|
||||
|
@ -287,7 +404,13 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
# cancel should take us back to choosing a workflow
|
||||
f.cancel_url = self.request.route_url('{}.create'.format(route_prefix))
|
||||
|
||||
# TODO: remove this
|
||||
# receiving_workflow
|
||||
if self.creating and workflow:
|
||||
f.set_readonly('receiving_workflow')
|
||||
f.set_renderer('receiving_workflow', self.render_receiving_workflow)
|
||||
else:
|
||||
f.remove('receiving_workflow')
|
||||
|
||||
# batch_type
|
||||
if self.creating:
|
||||
f.set_widget('batch_type', dfwidget.HiddenWidget())
|
||||
|
@ -402,7 +525,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
|
||||
# multiple invoice files (if applicable)
|
||||
if (not self.creating
|
||||
and batch.get_param('workflow') == 'from_multi_invoice'):
|
||||
and batch.get_param('receiving_workflow') == 'from_multi_invoice'):
|
||||
|
||||
if 'invoice_files' not in f:
|
||||
f.insert_before('invoice_file', 'invoice_files')
|
||||
|
@ -501,6 +624,12 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
items.append(HTML.tag('li', c=[link]))
|
||||
return HTML.tag('ul', c=items)
|
||||
|
||||
def render_receiving_workflow(self, batch, field):
|
||||
key = self.request.matchdict['workflow_key']
|
||||
info = self.handler.receiving_workflow_info(key)
|
||||
if info:
|
||||
return info['display']
|
||||
|
||||
def get_visible_params(self, batch):
|
||||
params = super().get_visible_params(batch)
|
||||
|
||||
|
@ -525,40 +654,42 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
|
||||
def get_batch_kwargs(self, batch, **kwargs):
|
||||
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
||||
batch_type = self.request.POST['batch_type']
|
||||
|
||||
# must pull vendor from URL if it was not in form data
|
||||
if 'vendor_uuid' not in kwargs and 'vendor' not in kwargs:
|
||||
if 'vendor_uuid' in self.request.matchdict:
|
||||
kwargs['vendor_uuid'] = self.request.matchdict['vendor_uuid']
|
||||
|
||||
workflow = kwargs['workflow']
|
||||
if workflow == 'from_scratch':
|
||||
# TODO: ugh should just have workflow and no batch_type
|
||||
kwargs['receiving_workflow'] = batch_type
|
||||
if batch_type == 'from_scratch':
|
||||
kwargs.pop('truck_dump_batch', None)
|
||||
kwargs.pop('truck_dump_batch_uuid', None)
|
||||
elif workflow == 'from_invoice':
|
||||
elif batch_type == 'from_invoice':
|
||||
pass
|
||||
elif workflow == 'from_multi_invoice':
|
||||
elif batch_type == 'from_multi_invoice':
|
||||
pass
|
||||
elif workflow == 'from_po':
|
||||
elif batch_type == 'from_po':
|
||||
# TODO: how to best handle this field? this doesn't seem flexible
|
||||
kwargs['purchase_key'] = batch.purchase_uuid
|
||||
elif workflow == 'from_po_with_invoice':
|
||||
elif batch_type == 'from_po_with_invoice':
|
||||
# TODO: how to best handle this field? this doesn't seem flexible
|
||||
kwargs['purchase_key'] = batch.purchase_uuid
|
||||
elif workflow == 'truck_dump_children_first':
|
||||
elif batch_type == 'truck_dump_children_first':
|
||||
kwargs['truck_dump'] = True
|
||||
kwargs['truck_dump_children_first'] = True
|
||||
kwargs['order_quantities_known'] = True
|
||||
# TODO: this makes sense in some cases, but all?
|
||||
# (should just omit that field when not relevant)
|
||||
kwargs['date_ordered'] = None
|
||||
elif workflow == 'truck_dump_children_last':
|
||||
elif batch_type == 'truck_dump_children_last':
|
||||
kwargs['truck_dump'] = True
|
||||
kwargs['truck_dump_ready'] = True
|
||||
# TODO: this makes sense in some cases, but all?
|
||||
# (should just omit that field when not relevant)
|
||||
kwargs['date_ordered'] = None
|
||||
elif workflow.startswith('truck_dump_child'):
|
||||
elif batch_type.startswith('truck_dump_child'):
|
||||
truck_dump = self.get_instance()
|
||||
kwargs['store'] = truck_dump.store
|
||||
kwargs['vendor'] = truck_dump.vendor
|
||||
|
@ -1855,12 +1986,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
'type': bool},
|
||||
|
||||
# vendors
|
||||
{'section': 'rattail.batch',
|
||||
'option': 'purchase.allow_receiving_any_vendor',
|
||||
'type': bool},
|
||||
# TODO: deprecated; can remove this once all live config
|
||||
# is updated. but for now it remains so this setting is
|
||||
# auto-deleted
|
||||
{'section': 'rattail.batch',
|
||||
'option': 'purchase.supported_vendors_only',
|
||||
'type': bool},
|
||||
|
@ -1911,7 +2036,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
@classmethod
|
||||
def defaults(cls, config):
|
||||
cls._receiving_defaults(config)
|
||||
cls._purchase_batch_defaults(config)
|
||||
cls._batch_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
|
@ -1919,11 +2043,17 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
def _receiving_defaults(cls, config):
|
||||
rattail_config = config.registry.settings.get('rattail_config')
|
||||
route_prefix = cls.get_route_prefix()
|
||||
url_prefix = cls.get_url_prefix()
|
||||
instance_url_prefix = cls.get_instance_url_prefix()
|
||||
model_key = cls.get_model_key()
|
||||
model_title = cls.get_model_title()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
|
||||
# new receiving batch using workflow X
|
||||
config.add_route('{}.create_workflow'.format(route_prefix), '{}/new/{{workflow_key}}/{{vendor_uuid}}'.format(url_prefix))
|
||||
config.add_view(cls, attr='create', route_name='{}.create_workflow'.format(route_prefix),
|
||||
permission='{}.create'.format(permission_prefix))
|
||||
|
||||
# row-level receiving
|
||||
config.add_route('{}.receive_row'.format(route_prefix), '{}/rows/{{row_uuid}}/receive'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='receive_row', route_name='{}.receive_row'.format(route_prefix),
|
||||
|
@ -1976,6 +2106,33 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
permission='{}.auto_receive'.format(permission_prefix))
|
||||
|
||||
|
||||
@colander.deferred
|
||||
def valid_workflow(node, kw):
|
||||
"""
|
||||
Deferred validator for ``workflow`` field, for new batches.
|
||||
"""
|
||||
valid_workflows = kw['valid_workflows']
|
||||
|
||||
def validate(node, value):
|
||||
# we just need to provide possible values, and let stock validator
|
||||
# handle the rest
|
||||
oneof = colander.OneOf(valid_workflows)
|
||||
return oneof(node, value)
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
class NewReceivingBatch(colander.Schema):
|
||||
"""
|
||||
Schema for choosing which "type" of new receiving batch should be created.
|
||||
"""
|
||||
vendor = colander.SchemaNode(colander.String(),
|
||||
label="Vendor")
|
||||
|
||||
workflow = colander.SchemaNode(colander.String(),
|
||||
validator=valid_workflow)
|
||||
|
||||
|
||||
class ReceiveRowForm(colander.MappingSchema):
|
||||
|
||||
mode = colander.SchemaNode(colander.String(),
|
||||
|
|
|
@ -25,7 +25,11 @@ Settings Views
|
|||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
import colander
|
||||
|
||||
|
@ -33,159 +37,194 @@ from rattail.db.model import Setting
|
|||
from rattail.settings import Setting as AppSetting
|
||||
from rattail.util import import_module_path
|
||||
|
||||
from tailbone import forms, grids
|
||||
from tailbone import forms
|
||||
from tailbone.db import Session
|
||||
from tailbone.views import MasterView, View
|
||||
from wuttaweb.util import get_libver, get_liburl
|
||||
from wuttaweb.views.settings import AppInfoView as WuttaAppInfoView
|
||||
|
||||
|
||||
class AppInfoView(WuttaAppInfoView):
|
||||
""" """
|
||||
Session = Session
|
||||
weblib_config_prefix = 'tailbone'
|
||||
class AppInfoView(MasterView):
|
||||
"""
|
||||
Master view for the overall app, to show/edit config etc.
|
||||
"""
|
||||
route_prefix = 'appinfo'
|
||||
model_key = 'UNUSED'
|
||||
model_title = "UNUSED"
|
||||
model_title_plural = "App Details"
|
||||
creatable = False
|
||||
viewable = False
|
||||
editable = False
|
||||
deletable = False
|
||||
filterable = False
|
||||
pageable = False
|
||||
configurable = True
|
||||
|
||||
# TODO: for now we override to get tailbone searchable grid
|
||||
def make_grid(self, **kwargs):
|
||||
""" """
|
||||
return grids.Grid(self.request, **kwargs)
|
||||
grid_columns = [
|
||||
'name',
|
||||
'version',
|
||||
'editable_project_location',
|
||||
]
|
||||
|
||||
def get_index_title(self):
|
||||
app = self.get_rattail_app()
|
||||
return "{} for {}".format(self.get_model_title_plural(),
|
||||
app.get_title())
|
||||
|
||||
def get_data(self, session=None):
|
||||
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):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
|
||||
# sort on frontend
|
||||
g.sort_on_backend = False
|
||||
g.sort_multiple = False
|
||||
g.set_sort_defaults('name')
|
||||
|
||||
# name
|
||||
g.set_searchable('name')
|
||||
|
||||
# editable_project_location
|
||||
g.set_searchable('editable_project_location')
|
||||
|
||||
def template_kwargs_index(self, **kwargs):
|
||||
kwargs = super().template_kwargs_index(**kwargs)
|
||||
kwargs['configure_button_title'] = "Configure App"
|
||||
return kwargs
|
||||
|
||||
def get_weblibs(self):
|
||||
""" """
|
||||
return OrderedDict([
|
||||
('vue', "Vue"),
|
||||
('vue_resource', "vue-resource"),
|
||||
('buefy', "Buefy"),
|
||||
('buefy.css', "Buefy CSS"),
|
||||
('fontawesome', "FontAwesome"),
|
||||
('bb_vue', "(BB) vue"),
|
||||
('bb_oruga', "(BB) @oruga-ui/oruga-next"),
|
||||
('bb_oruga_bulma', "(BB) @oruga-ui/theme-bulma (JS)"),
|
||||
('bb_oruga_bulma_css', "(BB) @oruga-ui/theme-bulma (CSS)"),
|
||||
('bb_fontawesome_svg_core', "(BB) @fortawesome/fontawesome-svg-core"),
|
||||
('bb_free_solid_svg_icons', "(BB) @fortawesome/free-solid-svg-icons"),
|
||||
('bb_vue_fontawesome', "(BB) @fortawesome/vue-fontawesome"),
|
||||
])
|
||||
|
||||
def configure_get_context(self, **kwargs):
|
||||
""" """
|
||||
context = super().configure_get_context(**kwargs)
|
||||
simple_settings = context['simple_settings']
|
||||
weblibs = context['weblibs']
|
||||
weblibs = self.get_weblibs()
|
||||
|
||||
for weblib in weblibs:
|
||||
key = weblib['key']
|
||||
for key in weblibs:
|
||||
title = weblibs[key]
|
||||
weblibs[key] = {
|
||||
'key': key,
|
||||
'title': title,
|
||||
|
||||
# nb. these values are exactly as configured, and are
|
||||
# used for editing the settings
|
||||
'configured_version': get_libver(self.request, key,
|
||||
prefix='tailbone',
|
||||
configured_only=True),
|
||||
'configured_url': get_liburl(self.request, key,
|
||||
prefix='tailbone',
|
||||
configured_only=True),
|
||||
|
||||
# these are for informational purposes only
|
||||
'default_version': get_libver(self.request, key,
|
||||
prefix='tailbone',
|
||||
default_only=True),
|
||||
'live_url': get_liburl(self.request, key,
|
||||
prefix='tailbone'),
|
||||
}
|
||||
|
||||
# TODO: this is only needed to migrate legacy settings to
|
||||
# use the newer wuttaweb setting names
|
||||
# use the newer wutaweb setting names
|
||||
url = simple_settings[f'wuttaweb.liburl.{key}']
|
||||
if not url and weblib['configured_url']:
|
||||
simple_settings[f'wuttaweb.liburl.{key}'] = weblib['configured_url']
|
||||
if not url and weblibs[key]['configured_url']:
|
||||
simple_settings[f'wuttaweb.liburl.{key}'] = weblibs[key]['configured_url']
|
||||
|
||||
context['weblibs'] = list(weblibs.values())
|
||||
return context
|
||||
|
||||
# nb. these email settings require special handling below
|
||||
configure_profile_key_mismatches = [
|
||||
'default.subject',
|
||||
'default.to',
|
||||
'default.cc',
|
||||
'default.bcc',
|
||||
'feedback.subject',
|
||||
'feedback.to',
|
||||
]
|
||||
|
||||
def configure_get_simple_settings(self):
|
||||
""" """
|
||||
simple_settings = super().configure_get_simple_settings()
|
||||
simple_settings = [
|
||||
|
||||
# TODO:
|
||||
# there are several email config keys which differ between
|
||||
# wuttjamaican and rattail. basically all of the "profile" keys
|
||||
# have a different prefix.
|
||||
# basics
|
||||
{'section': 'rattail',
|
||||
'option': 'app_title'},
|
||||
{'section': 'rattail',
|
||||
'option': 'node_type'},
|
||||
{'section': 'rattail',
|
||||
'option': 'node_title'},
|
||||
{'section': 'rattail',
|
||||
'option': 'production',
|
||||
'type': bool},
|
||||
{'section': 'rattail',
|
||||
'option': 'running_from_source',
|
||||
'type': bool},
|
||||
{'section': 'rattail',
|
||||
'option': 'running_from_source.rootpkg'},
|
||||
|
||||
# after wuttaweb has declared its settings, we examine each and
|
||||
# overwrite the value if one is defined with rattail config key.
|
||||
# (nb. this happens even if wuttjamaican key has a value!)
|
||||
# display
|
||||
{'section': 'tailbone',
|
||||
'option': 'background_color'},
|
||||
|
||||
# note that we *do* declare the profile mismatch keys for
|
||||
# rattail, as part of simple settings. this ensures the
|
||||
# parent logic will always remove them when saving. however
|
||||
# we must also include them in gather_settings() to ensure
|
||||
# they are saved to match wuttjamaican values.
|
||||
|
||||
# there are also a couple of flags where rattail's default is the
|
||||
# opposite of wuttjamaican. so we overwrite those too as needed.
|
||||
|
||||
for setting in simple_settings:
|
||||
|
||||
# nb. the update home page redirect setting is off by
|
||||
# default for wuttaweb, but on for tailbone
|
||||
if setting['name'] == 'wuttaweb.home_redirect_to_login':
|
||||
value = self.config.get_bool('wuttaweb.home_redirect_to_login')
|
||||
if value is None:
|
||||
value = self.config.get_bool('tailbone.login_is_home', default=True)
|
||||
setting['value'] = value
|
||||
|
||||
# nb. sending email is off by default for wuttjamaican,
|
||||
# but on for rattail
|
||||
elif setting['name'] == 'rattail.mail.send_emails':
|
||||
value = self.config.get_bool('rattail.mail.send_emails', default=True)
|
||||
setting['value'] = value
|
||||
|
||||
# nb. this one is even more special, key is entirely different
|
||||
elif setting['name'] == 'rattail.email.default.sender':
|
||||
value = self.config.get('rattail.email.default.sender')
|
||||
if value is None:
|
||||
value = self.config.get('rattail.mail.default.from')
|
||||
setting['value'] = value
|
||||
|
||||
else:
|
||||
|
||||
# nb. fetch alternate value for profile key mismatch
|
||||
for key in self.configure_profile_key_mismatches:
|
||||
if setting['name'] == f'rattail.email.{key}':
|
||||
value = self.config.get(f'rattail.email.{key}')
|
||||
if value is None:
|
||||
value = self.config.get(f'rattail.mail.{key}')
|
||||
setting['value'] = value
|
||||
break
|
||||
# grids
|
||||
{'section': 'tailbone',
|
||||
'option': 'grid.default_pagesize',
|
||||
# TODO: seems like should enforce this, but validation is
|
||||
# not setup yet
|
||||
# 'type': int
|
||||
},
|
||||
|
||||
# nb. these are no longer used (deprecated), but we keep
|
||||
# them defined here so the tool auto-deletes them
|
||||
{'section': 'tailbone',
|
||||
'option': 'buefy_version'},
|
||||
{'section': 'tailbone',
|
||||
'option': 'vue_version'},
|
||||
|
||||
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}'})
|
||||
def getval(key):
|
||||
return self.config.get(f'tailbone.{key}')
|
||||
|
||||
for key in self.get_weblibs():
|
||||
simple_settings.extend([
|
||||
{'name': f'tailbone.libver.{key}'},
|
||||
{'name': f'tailbone.liburl.{key}'},
|
||||
])
|
||||
weblibs = self.get_weblibs()
|
||||
for key, title in weblibs.items():
|
||||
|
||||
simple_settings.append({
|
||||
'section': 'wuttaweb',
|
||||
'option': f"libver.{key}",
|
||||
'default': getval(f"libver.{key}"),
|
||||
})
|
||||
simple_settings.append({
|
||||
'section': 'wuttaweb',
|
||||
'option': f"liburl.{key}",
|
||||
'default': getval(f"liburl.{key}"),
|
||||
})
|
||||
|
||||
# nb. these are no longer used (deprecated), but we keep
|
||||
# them defined here so the tool auto-deletes them
|
||||
simple_settings.append({
|
||||
'section': 'tailbone',
|
||||
'option': f"libver.{key}",
|
||||
})
|
||||
simple_settings.append({
|
||||
'section': 'tailbone',
|
||||
'option': f"liburl.{key}",
|
||||
})
|
||||
|
||||
return simple_settings
|
||||
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -348,26 +348,55 @@ class UpgradeView(MasterView):
|
|||
commit_hash_pattern = re.compile(r'^.{40}$')
|
||||
|
||||
def get_changelog_projects(self):
|
||||
project_map = {
|
||||
'onager': 'onager',
|
||||
'pyCOREPOS': 'pycorepos',
|
||||
'rattail': 'rattail',
|
||||
'rattail_corepos': 'rattail-corepos',
|
||||
'rattail-onager': 'rattail-onager',
|
||||
'rattail_tempmon': 'rattail-tempmon',
|
||||
'rattail_woocommerce': 'rattail-woocommerce',
|
||||
'Tailbone': 'tailbone',
|
||||
'tailbone_corepos': 'tailbone-corepos',
|
||||
'tailbone-onager': 'tailbone-onager',
|
||||
'tailbone_theo': 'theo',
|
||||
'tailbone_woocommerce': 'tailbone-woocommerce',
|
||||
}
|
||||
|
||||
projects = {}
|
||||
for name, repo in project_map.items():
|
||||
projects[name] = {
|
||||
'commit_url': f'https://forgejo.wuttaproject.org/rattail/{repo}/compare/{{old_version}}...{{new_version}}',
|
||||
'release_url': f'https://forgejo.wuttaproject.org/rattail/{repo}/src/tag/v{{new_version}}/CHANGELOG.md',
|
||||
projects = {
|
||||
'rattail': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail/files/v{new_version}/CHANGES.rst',
|
||||
},
|
||||
'Tailbone': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone/files/v{new_version}/CHANGES.rst',
|
||||
},
|
||||
'pyCOREPOS': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/pycorepos/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/pycorepos/files/v{new_version}/CHANGES.rst',
|
||||
},
|
||||
'rattail_corepos': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-corepos/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-corepos/files/v{new_version}/CHANGES.rst',
|
||||
},
|
||||
'tailbone_corepos': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-corepos/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-corepos/files/v{new_version}/CHANGES.rst',
|
||||
},
|
||||
'onager': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/onager/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/onager/files/v{new_version}/CHANGES.rst',
|
||||
},
|
||||
'rattail-onager': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/rattail-onager/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/rattail-onager/files/v{new_version}/CHANGELOG.md',
|
||||
},
|
||||
'rattail_tempmon': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-tempmon/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-tempmon/files/v{new_version}/CHANGES.rst',
|
||||
},
|
||||
'tailbone-onager': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/tailbone-onager/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/tailbone-onager/files/v{new_version}/CHANGELOG.md',
|
||||
},
|
||||
'rattail_woocommerce': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-woocommerce/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-woocommerce/files/v{new_version}/CHANGES.rst',
|
||||
},
|
||||
'tailbone_woocommerce': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-woocommerce/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-woocommerce/files/v{new_version}/CHANGES.rst',
|
||||
},
|
||||
'tailbone_theo': {
|
||||
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/theo/changelog/{new_version}/?size=10',
|
||||
'release_url': 'https://kallithea.rattailproject.org/rattail-project/theo/files/v{new_version}/CHANGES.rst',
|
||||
},
|
||||
}
|
||||
return projects
|
||||
|
||||
|
|
|
@ -801,8 +801,4 @@ def defaults(config, **kwargs):
|
|||
|
||||
|
||||
def includeme(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)
|
||||
|
|
|
@ -32,7 +32,6 @@ from wuttaweb.views import people as wutta
|
|||
from tailbone.views import people as tailbone
|
||||
from tailbone.db import Session
|
||||
from rattail.db.model import Person
|
||||
from tailbone.grids import Grid
|
||||
|
||||
|
||||
class PersonView(wutta.PersonView):
|
||||
|
@ -45,6 +44,7 @@ class PersonView(wutta.PersonView):
|
|||
"""
|
||||
model_class = Person
|
||||
Session = Session
|
||||
sort_defaults = 'display_name'
|
||||
|
||||
labels = {
|
||||
'display_name': "Full Name",
|
||||
|
@ -59,11 +59,6 @@ class PersonView(wutta.PersonView):
|
|||
'merge_requested',
|
||||
]
|
||||
|
||||
filter_defaults = {
|
||||
'display_name': {'active': True, 'verb': 'contains'},
|
||||
}
|
||||
sort_defaults = 'display_name'
|
||||
|
||||
form_fields = [
|
||||
'first_name',
|
||||
'middle_name',
|
||||
|
@ -79,11 +74,6 @@ class PersonView(wutta.PersonView):
|
|||
# CRUD methods
|
||||
##############################
|
||||
|
||||
# TODO: must use older grid for now, to render filters correctly
|
||||
def make_grid(self, **kwargs):
|
||||
""" """
|
||||
return Grid(self.request, **kwargs)
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
# -*- 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,12 +57,6 @@ class TestGrid(WebTestCase):
|
|||
grid = self.make_grid(default_page=42)
|
||||
self.assertEqual(grid.page, 42)
|
||||
|
||||
# searchable
|
||||
grid = self.make_grid()
|
||||
self.assertEqual(grid.searchable_columns, set())
|
||||
grid = self.make_grid(searchable={'foo': True})
|
||||
self.assertEqual(grid.searchable_columns, {'foo'})
|
||||
|
||||
def test_vue_tagname(self):
|
||||
|
||||
# default
|
||||
|
@ -135,7 +129,7 @@ class TestGrid(WebTestCase):
|
|||
|
||||
def test_set_label(self):
|
||||
model = self.app.model
|
||||
grid = self.make_grid(model_class=model.Setting, filterable=True)
|
||||
grid = self.make_grid(model_class=model.Setting)
|
||||
self.assertEqual(grid.labels, {})
|
||||
|
||||
# basic
|
||||
|
|
|
@ -5,9 +5,12 @@ from unittest import TestCase
|
|||
|
||||
from pyramid.config import Configurator
|
||||
|
||||
from wuttjamaican.testing import FileConfigTestCase
|
||||
|
||||
from rattail.exceptions import ConfigurationError
|
||||
from rattail.testing import DataTestCase
|
||||
from rattail.config import RattailConfig
|
||||
from tailbone import app as mod
|
||||
from tests.util import DataTestCase
|
||||
|
||||
|
||||
class TestRattailConfig(TestCase):
|
||||
|
@ -27,7 +30,7 @@ class TestRattailConfig(TestCase):
|
|||
|
||||
class TestMakePyramidConfig(DataTestCase):
|
||||
|
||||
def make_config(self, **kwargs):
|
||||
def make_config(self):
|
||||
myconf = self.write_file('web.conf', """
|
||||
[rattail.db]
|
||||
default.url = sqlite://
|
||||
|
|
|
@ -38,7 +38,7 @@ class TestPersonView(WebTestCase):
|
|||
|
||||
def test_configure_form(self):
|
||||
model = self.app.model
|
||||
barney = model.Person(display_name="Barney Rubble")
|
||||
barney = model.User(username='barney')
|
||||
self.session.add(barney)
|
||||
self.session.commit()
|
||||
view = self.make_view()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue