diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8017445..c974b3a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,181 @@ All notable changes to Tailbone will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+## v0.22.7 (2025-02-19)
+
+### Fix
+
+- stop using old config for logo image url on login page
+- fix warning msg for deprecated Grid param
+
+## v0.22.6 (2025-02-01)
+
+### Fix
+
+- register vue3 form component for products -> make batch
+
+## v0.22.5 (2024-12-16)
+
+### Fix
+
+- whoops this is latest rattail
+- require newer rattail lib
+- require newer wuttaweb
+- let caller request safe HTML literal for rendered grid table
+
+## v0.22.4 (2024-11-22)
+
+### Fix
+
+- avoid error in product search for duplicated key
+- use vmodel for confirm password widget input
+
+## v0.22.3 (2024-11-19)
+
+### Fix
+
+- avoid error for trainwreck query when not a customer
+
+## v0.22.2 (2024-11-18)
+
+### Fix
+
+- use local/custom enum for continuum operations
+- add basic master view for Product Costs
+- show continuum operation type when viewing version history
+- always define `app` attr for ViewSupplement
+- avoid deprecated import
+
+## v0.22.1 (2024-11-02)
+
+### Fix
+
+- fix submit button for running problem report
+- avoid deprecated grid method
+
+## v0.22.0 (2024-10-22)
+
+### Feat
+
+- add support for new ordering batch from parsed file
+
+### Fix
+
+- avoid deprecated method to suggest username
+
+## v0.21.11 (2024-10-03)
+
+### Fix
+
+- custom method for adding grid action
+- become/stop root should redirect to previous url
+
+## v0.21.10 (2024-09-15)
+
+### Fix
+
+- update project repo links, kallithea -> forgejo
+- use better icon for submit button on login page
+- wrap notes text for batch view
+- expose datasync consumer batch size via configure page
+
+## v0.21.9 (2024-08-28)
+
+### Fix
+
+- render custom attrs in form component tag
+
+## v0.21.8 (2024-08-28)
+
+### Fix
+
+- ignore session kwarg for `MasterView.make_row_grid()`
+
+## v0.21.7 (2024-08-28)
+
+### Fix
+
+- avoid error when form value cannot be obtained
+
+## v0.21.6 (2024-08-28)
+
+### Fix
+
+- avoid error when grid value cannot be obtained
+
+## v0.21.5 (2024-08-28)
+
+### Fix
+
+- set empty string for "-new-" file configure option
+
+## v0.21.4 (2024-08-26)
+
+### Fix
+
+- handle differing email profile keys for appinfo/configure
+
+## v0.21.3 (2024-08-26)
+
+### Fix
+
+- show non-standard config values for app info configure email
+
+## v0.21.2 (2024-08-26)
+
+### Fix
+
+- refactor waterpark base template to use wutta feedback component
+- fix input/output file upload feature for configure pages, per oruga
+- tweak how grid data translates to Vue template context
+- merge filters into main grid template
+- add basic wutta view for users
+- some fixes for wutta people view
+- various fixes for waterpark theme
+- avoid deprecated `component` form kwarg
+
+## v0.21.1 (2024-08-22)
+
+### Fix
+
+- misc. bugfixes per recent changes
+
+## v0.21.0 (2024-08-22)
+
+### Feat
+
+- move "most" filtering logic for grid class to wuttaweb
+- inherit from wuttaweb templates for home, login pages
+- inherit from wuttaweb for AppInfoView, appinfo/configure template
+- add "has output file templates" config option for master view
+
+### Fix
+
+- change grid reset-view param name to match wuttaweb
+- move "searchable columns" grid feature to wuttaweb
+- use wuttaweb to get/render csrf token
+- inherit from wuttaweb for appinfo/index template
+- prefer wuttaweb config for "home redirect to login" feature
+- fix master/index template rendering for waterpark theme
+- fix spacing for navbar logo/title in waterpark theme
+
+## v0.20.1 (2024-08-20)
+
+### Fix
+
+- fix default filter verbs logic for workorder status
+
+## v0.20.0 (2024-08-20)
+
+### Feat
+
+- add new 'waterpark' theme, based on wuttaweb w/ vue2 + buefy
+- refactor templates to simplify base/page/form structure
+
+### Fix
+
+- avoid deprecated reference to app db engine
+
## v0.19.3 (2024-08-19)
### Fix
diff --git a/README.rst b/README.md
similarity index 56%
rename from README.rst
rename to README.md
index 0cffc62d..74c007f6 100644
--- a/README.rst
+++ b/README.md
@@ -1,10 +1,8 @@
-Tailbone
-========
+# Tailbone
Tailbone is an extensible web application based on Rattail. It provides a
"back-office network environment" (BONE) for use in managing retail data.
-Please see Rattail's `home page`_ for more information.
-
-.. _home page: http://rattailproject.org/
+Please see Rattail's [home page](http://rattailproject.org/) for more
+information.
diff --git a/docs/conf.py b/docs/conf.py
index 52e384f5..ade4c92a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -27,10 +27,10 @@ templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
intersphinx_mapping = {
- 'rattail': ('https://rattailproject.org/docs/rattail/', None),
+ 'rattail': ('https://docs.wuttaproject.org/rattail/', None),
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
- 'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None),
- 'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
+ 'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None),
+ 'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
}
# allow todo entries to show up
diff --git a/pyproject.toml b/pyproject.toml
index 3e07abaa..a7214a8e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,9 +6,9 @@ build-backend = "hatchling.build"
[project]
name = "Tailbone"
-version = "0.19.3"
+version = "0.22.7"
description = "Backoffice Web Application for Rattail"
-readme = "README.rst"
+readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
license = {text = "GNU GPL v3+"}
classifiers = [
@@ -53,13 +53,13 @@ dependencies = [
"pyramid_mako",
"pyramid_retry",
"pyramid_tm",
- "rattail[db,bouncer]>=0.18.1",
+ "rattail[db,bouncer]>=0.20.1",
"sa-filters",
"simplejson",
"transaction",
"waitress",
"WebHelpers2",
- "WuttaWeb>=0.10.2",
+ "WuttaWeb>=0.21.0",
"zope.sqlalchemy>=1.5",
]
@@ -84,9 +84,9 @@ tailbone = "tailbone.config:ConfigExtension"
[project.urls]
Homepage = "https://rattailproject.org"
-Repository = "https://kallithea.rattailproject.org/rattail-project/tailbone"
-Issues = "https://redmine.rattailproject.org/projects/tailbone/issues"
-Changelog = "https://kallithea.rattailproject.org/rattail-project/tailbone/files/master/CHANGELOG.md"
+Repository = "https://forgejo.wuttaproject.org/rattail/tailbone"
+Issues = "https://forgejo.wuttaproject.org/rattail/tailbone/issues"
+Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone/src/branch/master/CHANGELOG.md"
[tool.commitizen]
diff --git a/tailbone/api/batch/receiving.py b/tailbone/api/batch/receiving.py
index daa4290f..b23bff55 100644
--- a/tailbone/api/batch/receiving.py
+++ b/tailbone/api/batch/receiving.py
@@ -29,8 +29,7 @@ import logging
import humanize
import sqlalchemy as sa
-from rattail.db import model
-from rattail.util import pretty_quantity
+from rattail.db.model import PurchaseBatch, PurchaseBatchRow
from cornice import Service
from deform import widget as dfwidget
@@ -45,7 +44,7 @@ log = logging.getLogger(__name__)
class ReceivingBatchViews(APIBatchView):
- model_class = model.PurchaseBatch
+ model_class = PurchaseBatch
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'receivingbatchviews'
permission_prefix = 'receiving'
@@ -55,7 +54,8 @@ class ReceivingBatchViews(APIBatchView):
supports_execute = True
def base_query(self):
- query = super(ReceivingBatchViews, self).base_query()
+ model = self.app.model
+ query = super().base_query()
query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING)
return query
@@ -85,7 +85,7 @@ class ReceivingBatchViews(APIBatchView):
# assume "receive from PO" if given a PO key
if data.get('purchase_key'):
- data['receiving_workflow'] = 'from_po'
+ data['workflow'] = 'from_po'
return super().create_object(data)
@@ -120,6 +120,7 @@ class ReceivingBatchViews(APIBatchView):
return self._get(obj=batch)
def eligible_purchases(self):
+ model = self.app.model
uuid = self.request.params.get('vendor_uuid')
vendor = self.Session.get(model.Vendor, uuid) if uuid else None
if not vendor:
@@ -176,7 +177,7 @@ class ReceivingBatchViews(APIBatchView):
class ReceivingBatchRowViews(APIBatchRowView):
- model_class = model.PurchaseBatchRow
+ model_class = PurchaseBatchRow
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'receiving.rows'
permission_prefix = 'receiving'
@@ -185,7 +186,8 @@ class ReceivingBatchRowViews(APIBatchRowView):
supports_quick_entry = True
def make_filter_spec(self):
- filters = super(ReceivingBatchRowViews, self).make_filter_spec()
+ model = self.app.model
+ filters = super().make_filter_spec()
if filters:
# must translate certain convenience filters
@@ -296,11 +298,11 @@ class ReceivingBatchRowViews(APIBatchRowView):
return filters
def normalize(self, row):
- data = super(ReceivingBatchRowViews, self).normalize(row)
+ data = super().normalize(row)
+ model = self.app.model
batch = row.batch
- app = self.get_rattail_app()
- prodder = app.get_products_handler()
+ prodder = self.app.get_products_handler()
data['product_uuid'] = row.product_uuid
data['item_id'] = row.item_id
@@ -375,7 +377,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
if accounted_for:
# some product accounted for; button should receive "remainder" only
if remainder:
- remainder = pretty_quantity(remainder)
+ remainder = self.app.render_quantity(remainder)
data['quick_receive_quantity'] = remainder
data['quick_receive_text'] = "Receive Remainder ({} {})".format(
remainder, data['unit_uom'])
@@ -386,7 +388,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
else: # nothing yet accounted for, button should receive "all"
if not remainder:
log.warning("quick receive remainder is empty for row %s", row.uuid)
- remainder = pretty_quantity(remainder)
+ remainder = self.app.render_quantity(remainder)
data['quick_receive_quantity'] = remainder
data['quick_receive_text'] = "Receive ALL ({} {})".format(
remainder, data['unit_uom'])
@@ -414,7 +416,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
data['received_alert'] = None
if self.batch_handler.get_units_confirmed(row):
msg = "You have already received some of this product; last update was {}.".format(
- humanize.naturaltime(app.make_utc() - row.modified))
+ humanize.naturaltime(self.app.make_utc() - row.modified))
data['received_alert'] = msg
return data
@@ -423,6 +425,8 @@ class ReceivingBatchRowViews(APIBatchRowView):
"""
View which handles "receiving" against a particular batch row.
"""
+ model = self.app.model
+
# first do basic input validation
schema = ReceiveRow().bind(session=self.Session())
form = forms.Form(schema=schema, request=self.request)
diff --git a/tailbone/api/master.py b/tailbone/api/master.py
index 2d17339e..551d6428 100644
--- a/tailbone/api/master.py
+++ b/tailbone/api/master.py
@@ -26,7 +26,6 @@ Tailbone Web API - Master View
import json
-from rattail.config import parse_bool
from rattail.db.util import get_fieldnames
from cornice import resource, Service
@@ -185,7 +184,7 @@ class APIMasterView(APIView):
if sortcol:
spec = {
'field': sortcol.field_name,
- 'direction': 'asc' if parse_bool(self.request.params['ascending']) else 'desc',
+ 'direction': 'asc' if self.config.parse_bool(self.request.params['ascending']) else 'desc',
}
if sortcol.model_name:
spec['model'] = sortcol.model_name
diff --git a/tailbone/app.py b/tailbone/app.py
index 626c9206..d2d0c5ef 100644
--- a/tailbone/app.py
+++ b/tailbone/app.py
@@ -62,9 +62,20 @@ def make_rattail_config(settings):
# nb. this is for compaibility with wuttaweb
settings['wutta_config'] = rattail_config
+ # must import all sqlalchemy models before things get rolling,
+ # otherwise can have errors about continuum TransactionMeta class
+ # not yet mapped, when relevant pages are first requested...
+ # cf. https://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/database/sqlalchemy.html#importing-all-sqlalchemy-models
+ # hat tip to https://stackoverflow.com/a/59241485
+ if getattr(rattail_config, 'tempmon_engine', None):
+ from rattail_tempmon.db import model as tempmon_model, Session as TempmonSession
+ tempmon_session = TempmonSession()
+ tempmon_session.query(tempmon_model.Appliance).first()
+ tempmon_session.close()
+
# configure database sessions
- if hasattr(rattail_config, 'rattail_engine'):
- tailbone.db.Session.configure(bind=rattail_config.rattail_engine)
+ if hasattr(rattail_config, 'appdb_engine'):
+ tailbone.db.Session.configure(bind=rattail_config.appdb_engine)
if hasattr(rattail_config, 'trainwreck_engine'):
tailbone.db.TrainwreckSession.configure(bind=rattail_config.trainwreck_engine)
if hasattr(rattail_config, 'tempmon_engine'):
@@ -321,7 +332,8 @@ def main(global_config, **settings):
"""
This function returns a Pyramid WSGI application.
"""
- settings.setdefault('mako.directories', ['tailbone:templates'])
+ settings.setdefault('mako.directories', ['tailbone:templates',
+ 'wuttaweb:templates'])
rattail_config = make_rattail_config(settings)
pyramid_config = make_pyramid_config(settings)
pyramid_config.include('tailbone')
diff --git a/tailbone/diffs.py b/tailbone/diffs.py
index 98253c57..2e582b15 100644
--- a/tailbone/diffs.py
+++ b/tailbone/diffs.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2023 Lance Edgar
+# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@@ -270,9 +270,21 @@ class VersionDiff(Diff):
for field in self.fields:
values[field] = {'before': self.render_old_value(field),
'after': self.render_new_value(field)}
+
+ operation = None
+ if self.version.operation_type == continuum.Operation.INSERT:
+ operation = 'INSERT'
+ elif self.version.operation_type == continuum.Operation.UPDATE:
+ operation = 'UPDATE'
+ elif self.version.operation_type == continuum.Operation.DELETE:
+ operation = 'DELETE'
+ else:
+ operation = self.version.operation_type
+
return {
'key': id(self.version),
'model_title': self.title,
+ 'operation': operation,
'diff_class': self.nature,
'fields': self.fields,
'values': values,
diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py
index 2f1c9370..4024557b 100644
--- a/tailbone/forms/core.py
+++ b/tailbone/forms/core.py
@@ -401,6 +401,8 @@ 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)
@@ -905,7 +907,8 @@ class Form(object):
def render_vue_template(self, template='/forms/deform.mako', **context):
""" """
- return self.render_deform(template=template, **context)
+ output = self.render_deform(template=template, **context)
+ return HTML.literal(output)
def render_deform(self, dform=None, template=None, **kwargs):
if not template:
@@ -1036,9 +1039,9 @@ class Form(object):
def render_vue_tag(self, **kwargs):
""" """
- return self.render_vuejs_component()
+ return self.render_vuejs_component(**kwargs)
- def render_vuejs_component(self):
+ def render_vuejs_component(self, **kwargs):
"""
Render the Vue.js component HTML for the form.
@@ -1049,10 +1052,11 @@ class Form(object):
"""
- kwargs = dict(self.vuejs_component_kwargs)
+ kw = dict(self.vuejs_component_kwargs)
+ kw.update(kwargs)
if self.can_edit_help:
- kwargs.setdefault(':configure-fields-help', 'configureFieldsHelp')
- return HTML.tag(self.vue_tagname, **kwargs)
+ kw.setdefault(':configure-fields-help', 'configureFieldsHelp')
+ return HTML.tag(self.vue_tagname, **kw)
def set_json_data(self, key, value):
"""
@@ -1220,6 +1224,18 @@ class Form(object):
# TODO: again, why does serialize() not return literal?
return HTML.literal(field.serialize())
+ # TODO: this was copied from wuttaweb; can remove when we align
+ # Form class structure
+ def render_vue_finalize(self):
+ """ """
+ set_data = f"{self.vue_component}.data = function() {{ return {self.vue_component}Data }}"
+ make_component = f"Vue.component('{self.vue_tagname}', {self.vue_component})"
+ return HTML.tag('script', c=['\n',
+ HTML.literal(set_data),
+ '\n',
+ HTML.literal(make_component),
+ '\n'])
+
def render_field_readonly(self, field_name, **kwargs):
"""
Render the given field completely, but in read-only fashion.
@@ -1367,7 +1383,11 @@ class Form(object):
return getattr(record, field_name)
except AttributeError:
pass
- return record[field_name]
+
+ try:
+ return record[field_name]
+ except TypeError:
+ pass
# TODO: is this always safe to do?
elif self.defaults and field_name in self.defaults:
diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py
index 6ec55987..56b97b86 100644
--- a/tailbone/grids/core.py
+++ b/tailbone/grids/core.py
@@ -24,9 +24,10 @@
Core Grid Classes
"""
-from urllib.parse import urlencode
-import warnings
+import inspect
import logging
+import warnings
+from urllib.parse import urlencode
import sqlalchemy as sa
from sqlalchemy import orm
@@ -196,11 +197,7 @@ 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,
@@ -216,48 +213,59 @@ class Grid(WuttaGrid):
expose_direct_link=False,
**kwargs,
):
- if kwargs.get('component'):
+ if 'component' in kwargs:
warnings.warn("component param is deprecated for Grid(); "
"please use vue_tagname param instead",
DeprecationWarning, stacklevel=2)
kwargs.setdefault('vue_tagname', kwargs.pop('component'))
- if kwargs.get('default_sortkey'):
+ if 'default_sortkey' in kwargs:
warnings.warn("default_sortkey param is deprecated for Grid(); "
"please use sort_defaults param instead",
DeprecationWarning, stacklevel=2)
- if kwargs.get('default_sortdir'):
+ if 'default_sortdir' in kwargs:
warnings.warn("default_sortdir param is deprecated for Grid(); "
"please use sort_defaults param instead",
DeprecationWarning, stacklevel=2)
- if kwargs.get('default_sortkey') or kwargs.get('default_sortdir'):
+ if 'default_sortkey' in kwargs or 'default_sortdir' in kwargs:
sortkey = kwargs.pop('default_sortkey', None)
sortdir = kwargs.pop('default_sortdir', 'asc')
if sortkey:
kwargs.setdefault('sort_defaults', [(sortkey, sortdir)])
- if kwargs.get('pageable'):
+ if 'pageable' in kwargs:
warnings.warn("pageable param is deprecated for Grid(); "
- "please use vue_tagname param instead",
+ "please use paginated param instead",
DeprecationWarning, stacklevel=2)
kwargs.setdefault('paginated', kwargs.pop('pageable'))
- if kwargs.get('default_pagesize'):
+ if 'default_pagesize' in kwargs:
warnings.warn("default_pagesize param is deprecated for Grid(); "
"please use pagesize param instead",
DeprecationWarning, stacklevel=2)
kwargs.setdefault('pagesize', kwargs.pop('default_pagesize'))
- if kwargs.get('default_page'):
+ if 'default_page' in kwargs:
warnings.warn("default_page param is deprecated for Grid(); "
"please use page param instead",
DeprecationWarning, stacklevel=2)
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)
@@ -275,19 +283,11 @@ 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,10 +443,14 @@ class Grid(WuttaGrid):
self.remove(oldfield)
def set_joiner(self, key, joiner):
+ """ """
if joiner is None:
- self.joiners.pop(key, None)
+ warnings.warn("specifying None is deprecated for Grid.set_joiner(); "
+ "please use Grid.remove_joiner() instead",
+ DeprecationWarning, stacklevel=2)
+ self.remove_joiner(key)
else:
- self.joiners[key] = joiner
+ super().set_joiner(key, joiner)
def set_sorter(self, key, *args, **kwargs):
""" """
@@ -474,41 +478,19 @@ class Grid(WuttaGrid):
self.sorters[key] = self.make_sorter(*args, **kwargs)
def set_filter(self, key, *args, **kwargs):
- if len(args) == 1 and args[0] is None:
- self.remove_filter(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)
+ """ """
+ 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)
+ self.remove_filter(key)
+ return
- 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
+ # 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))
+ self.filters[key] = self.make_filter(key, *args, **kwargs)
def set_click_handler(self, key, handler):
if handler:
@@ -593,7 +575,11 @@ class Grid(WuttaGrid):
return getattr(obj, column_name)
except AttributeError:
pass
- return obj[column_name]
+
+ try:
+ return obj[column_name]
+ except TypeError:
+ pass
def render_currency(self, obj, column_name):
value = self.obtain_value(obj, column_name)
@@ -708,6 +694,14 @@ class Grid(WuttaGrid):
def actions_column_format(self, column_number, row_number, item):
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.
@@ -732,16 +726,6 @@ 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.
@@ -879,9 +863,13 @@ class Grid(WuttaGrid):
settings['page'] = self.page
if self.filterable:
for filtr in self.iter_filters():
- 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
+ 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)
# If user has default settings on file, apply those first.
if self.user_has_defaults():
@@ -889,13 +877,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-to-default-filters') == 'true':
+ if self.request.GET.get('reset-view'):
pass
# If request has filter settings, grab those, then grab sort/pager
# settings from request or session.
- elif self.filterable and self.request_has_settings('filter'):
- self.update_filter_settings(settings, 'request')
+ elif self.request_has_settings('filter'):
+ self.update_filter_settings(settings, src='request')
if self.request_has_settings('sort'):
self.update_sort_settings(settings, src='request')
else:
@@ -907,7 +895,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, 'session')
+ self.update_filter_settings(settings, src='session')
self.update_page_settings(settings)
# NOTE: These next two are functionally equivalent, but are kept
@@ -917,12 +905,12 @@ class Grid(WuttaGrid):
# grab those, then grab filter/sort settings from session.
elif self.request_has_settings('page'):
self.update_page_settings(settings)
- self.update_filter_settings(settings, 'session')
+ self.update_filter_settings(settings, src='session')
self.update_sort_settings(settings, src='session')
# If request has no settings, grab all from session.
elif self.session_has_settings():
- self.update_filter_settings(settings, 'session')
+ self.update_filter_settings(settings, src='session')
self.update_sort_settings(settings, src='session')
self.update_page_settings(settings)
@@ -1062,18 +1050,11 @@ class Grid(WuttaGrid):
merge('page', int)
def request_has_settings(self, 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 super().request_has_settings(type_):
+ return True
- elif type_ == 'sort':
+ if type_ == 'sort':
# TODO: remove this eventually, but some links in the wild
# may still include these params, so leave it for now
@@ -1081,14 +1062,6 @@ 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):
@@ -1104,72 +1077,6 @@ 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'):
@@ -1257,89 +1164,12 @@ class Grid(WuttaGrid):
return data
- def sort_data(self, data, sorters=None):
- """ """
- if sorters is None:
- sorters = self.active_sorters
- if not sorters:
- return data
-
- # nb. when data is a query, we want to apply sorters in the
- # requested order, so the final query has order_by() in the
- # correct "as-is" sequence. however when data is a list we
- # must do the opposite, applying in the reverse order, so the
- # final list has the most "important" sort(s) applied last.
- if not isinstance(data, orm.Query):
- sorters = reversed(sorters)
-
- for sorter in sorters:
- sortkey = sorter['key']
- sortdir = sorter['dir']
-
- # cannot sort unless we have a sorter callable
- sortfunc = self.sorters.get(sortkey)
- if not sortfunc:
- return data
-
- # join appropriate model if needed
- if sortkey in self.joiners and sortkey not in self.joined:
- data = self.joiners[sortkey](data)
- self.joined.add(sortkey)
-
- # invoke the sorter
- data = sortfunc(data, sortdir)
-
- return data
-
- def paginate_data(self, data):
- """
- Paginate the given data set according to current settings, and return
- the result.
- """
- # we of course assume our current page is correct, at first
- pager = self.make_pager(data)
-
- # if pager has detected that our current page is outside the valid
- # range, we must re-orient ourself around the "new" (valid) page
- if pager.page != self.page:
- self.page = pager.page
- self.request.session['grid.{}.page'.format(self.key)] = self.page
- pager = self.make_pager(data)
-
- return pager
-
- def make_pager(self, data):
-
- # TODO: this seems hacky..normally we expect `data` to be a
- # query of course, but in some cases it may be a list instead.
- # if so then we can't use ORM pager
- if isinstance(data, list):
- import paginate
- return paginate.Page(data,
- items_per_page=self.pagesize,
- page=self.page)
-
- return SqlalchemyOrmPage(data,
- items_per_page=self.pagesize,
- page=self.page,
- url_maker=URLMaker(self.request))
-
def make_visible_data(self):
- """
- 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
+ """ """
+ warnings.warn("grid.make_visible_data() method is deprecated; "
+ "please use grid.get_visible_data() instead",
+ DeprecationWarning, stacklevel=2)
+ return self.get_visible_data()
def render_vue_tag(self, master=None, **kwargs):
""" """
@@ -1362,7 +1192,7 @@ class Grid(WuttaGrid):
includes the context menu items and grid tools.
"""
if 'grid_columns' not in kwargs:
- kwargs['grid_columns'] = self.get_table_columns()
+ kwargs['grid_columns'] = self.get_vue_columns()
if 'grid_data' not in kwargs:
kwargs['grid_data'] = self.get_table_data()
@@ -1385,6 +1215,7 @@ 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)
@@ -1392,6 +1223,7 @@ 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
@@ -1403,12 +1235,15 @@ class Grid(WuttaGrid):
context['data_prop'] = data_prop
context['empty_labels'] = empty_labels
if 'grid_columns' not in context:
- context['grid_columns'] = self.get_table_columns()
+ context['grid_columns'] = self.get_vue_columns()
context.setdefault('paginated', False)
if context['paginated']:
context.setdefault('per_page', 20)
context['view_click_handler'] = self.get_view_click_handler()
- return render(template, context)
+ result = render(template, context)
+ if literal:
+ result = HTML.literal(result)
+ return result
def get_view_click_handler(self):
""" """
@@ -1417,7 +1252,7 @@ class Grid(WuttaGrid):
view = None
for action in self.actions:
if action.key == 'view':
- return action.click_handler
+ return getattr(action, 'click_handler', None)
def set_filters_sequence(self, filters, only=False):
"""
@@ -1491,28 +1326,6 @@ 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!",
@@ -1574,22 +1387,19 @@ class Grid(WuttaGrid):
def get_vue_columns(self):
""" """
- return self.get_table_columns()
+ columns = super().get_vue_columns()
+
+ for column in columns:
+ column['visible'] = column['field'] not in self.invisible
+
+ return columns
def get_table_columns(self):
- """
- 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
+ """ """
+ warnings.warn("grid.get_table_columns() method is deprecated; "
+ "please use grid.get_vue_columns() instead",
+ DeprecationWarning, stacklevel=2)
+ return self.get_vue_columns()
def get_uuid_for_row(self, rowobj):
@@ -1601,6 +1411,10 @@ 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()
@@ -1615,7 +1429,7 @@ class Grid(WuttaGrid):
return self._table_data
# filter / sort / paginate to get "visible" data
- raw_data = self.make_visible_data()
+ raw_data = self.get_visible_data()
data = []
status_map = {}
checked = []
@@ -1656,10 +1470,22 @@ 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:
- value = self.renderers[name](rowobj, name)
- else:
- value = self.obtain_value(rowobj, name)
+ 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)
+
if value is None:
value = ""
@@ -1692,6 +1518,8 @@ class Grid(WuttaGrid):
results = {
'data': data,
+ 'row_classes': status_map,
+ # TODO: deprecate / remove this
'row_status_map': status_map,
}
@@ -1720,6 +1548,11 @@ 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
diff --git a/tailbone/helpers.py b/tailbone/helpers.py
index 23988423..50b38c30 100644
--- a/tailbone/helpers.py
+++ b/tailbone/helpers.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2023 Lance Edgar
+# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@@ -24,6 +24,9 @@
Template Context Helpers
"""
+# start off with all from wuttaweb
+from wuttaweb.helpers import *
+
import os
import datetime
from decimal import Decimal
@@ -33,12 +36,7 @@ from rattail.time import localtime, make_utc
from rattail.util import pretty_quantity, pretty_hours, hours_as_decimal
from rattail.db.util import maxlen
-from webhelpers2.html import *
-from webhelpers2.html.tags import *
-
-from wuttaweb.util import get_liburl
-from tailbone.util import (csrf_token, get_csrf_token,
- pretty_datetime, raw_datetime,
+from tailbone.util import (pretty_datetime, raw_datetime,
render_markdown,
route_exists)
diff --git a/tailbone/menus.py b/tailbone/menus.py
index abd0b58b..09d6f3f0 100644
--- a/tailbone/menus.py
+++ b/tailbone/menus.py
@@ -394,6 +394,11 @@ class TailboneMenuHandler(WuttaMenuHandler):
'route': 'products',
'perm': 'products.list',
},
+ {
+ 'title': "Product Costs",
+ 'route': 'product_costs',
+ 'perm': 'product_costs.list',
+ },
{
'title': "Departments",
'route': 'departments',
@@ -451,6 +456,11 @@ class TailboneMenuHandler(WuttaMenuHandler):
'route': 'vendors',
'perm': 'vendors.list',
},
+ {
+ 'title': "Product Costs",
+ 'route': 'product_costs',
+ 'perm': 'product_costs.list',
+ },
{'type': 'sep'},
{
'title': "Ordering",
@@ -703,7 +713,7 @@ class TailboneMenuHandler(WuttaMenuHandler):
},
{'type': 'sep'},
{
- 'title': "App Details",
+ 'title': "App Info",
'route': 'appinfo',
'perm': 'appinfo.list',
},
diff --git a/tailbone/static/__init__.py b/tailbone/static/__init__.py
index 2ad5161a..57700b80 100644
--- a/tailbone/static/__init__.py
+++ b/tailbone/static/__init__.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2017 Lance Edgar
+# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@@ -24,9 +24,8 @@
Static Assets
"""
-from __future__ import unicode_literals, absolute_import
-
def includeme(config):
+ config.include('wuttaweb.static')
config.add_static_view('tailbone', 'tailbone:static')
config.add_static_view('deform', 'deform:static')
diff --git a/tailbone/templates/appinfo/configure.mako b/tailbone/templates/appinfo/configure.mako
index aab180c4..9d866cea 100644
--- a/tailbone/templates/appinfo/configure.mako
+++ b/tailbone/templates/appinfo/configure.mako
@@ -1,250 +1,2 @@
## -*- coding: utf-8; -*-
-<%inherit file="/configure.mako" />
-
-<%def name="form_content()">
-
-
Basics
-
-
-
-
-
-
-
-
-
-
- ## TODO: should be a dropdown, app handler defines choices
-
-
-
-
-
-
-
-
-
-
-
-
-
- Production Mode
-
-
-
-
-
-
-
- Running from Source
-
-
-
-
-
-
-
-
-
-
-
-
-
- Display
-
-
-
-
-
-
-
-
-
-
-
-
-
- Grids
-
-
-
-
-
-
-
-
-
-
-
-
-
- Web Libraries
-
-
- <${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">
-
- save settings and refresh page to see new URL
-
-
- {{ props.row.live_url }}
-
- ${b}-table-column>
-
- <${b}-table-column field="actions"
- label="Actions"
- v-slot="props">
-
- % if request.use_oruga:
-
- % else:
-
- % endif
- Edit
-
- ${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
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${b}-modal>
-
-
-%def>
-
-<%def name="modify_this_page_vars()">
- ${parent.modify_this_page_vars()}
-
-%def>
-
-
-${parent.body()}
+<%inherit file="wuttaweb:templates/appinfo/configure.mako" />
diff --git a/tailbone/templates/appinfo/index.mako b/tailbone/templates/appinfo/index.mako
index 73f53920..faaea935 100644
--- a/tailbone/templates/appinfo/index.mako
+++ b/tailbone/templates/appinfo/index.mako
@@ -1,8 +1,7 @@
## -*- coding: utf-8; -*-
-<%inherit file="/master/index.mako" />
-
-<%def name="render_grid_component()">
+<%inherit file="wuttaweb:templates/appinfo/index.mako" />
+<%def name="page_content()">