diff --git a/CHANGELOG.md b/CHANGELOG.md
index c974b3a6..3bcbc6ec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,139 +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
diff --git a/README.md b/README.rst
similarity index 56%
rename from README.md
rename to README.rst
index 74c007f6..0cffc62d 100644
--- a/README.md
+++ b/README.rst
@@ -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/
diff --git a/docs/conf.py b/docs/conf.py
index ade4c92a..52e384f5 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://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
diff --git a/pyproject.toml b/pyproject.toml
index a7214a8e..2db880ad 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,9 +6,9 @@ build-backend = "hatchling.build"
[project]
name = "Tailbone"
-version = "0.22.7"
+version = "0.21.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.4",
"sa-filters",
"simplejson",
"transaction",
"waitress",
"WebHelpers2",
- "WuttaWeb>=0.21.0",
+ "WuttaWeb>=0.12.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]
diff --git a/tailbone/api/batch/receiving.py b/tailbone/api/batch/receiving.py
index b23bff55..daa4290f 100644
--- a/tailbone/api/batch/receiving.py
+++ b/tailbone/api/batch/receiving.py
@@ -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)
diff --git a/tailbone/api/master.py b/tailbone/api/master.py
index 551d6428..2d17339e 100644
--- a/tailbone/api/master.py
+++ b/tailbone/api/master.py
@@ -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
diff --git a/tailbone/app.py b/tailbone/app.py
index d2d0c5ef..b7262866 100644
--- a/tailbone/app.py
+++ b/tailbone/app.py
@@ -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)
diff --git a/tailbone/diffs.py b/tailbone/diffs.py
index 2e582b15..98253c57 100644
--- a/tailbone/diffs.py
+++ b/tailbone/diffs.py
@@ -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,
diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py
index 4024557b..059b212a 100644
--- a/tailbone/forms/core.py
+++ b/tailbone/forms/core.py
@@ -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):
"""
- 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
+ return record[field_name]
# 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 56b97b86..754868bc 100644
--- a/tailbone/grids/core.py
+++ b/tailbone/grids/core.py
@@ -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
@@ -235,7 +234,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'))
@@ -575,11 +574,7 @@ class Grid(WuttaGrid):
return getattr(obj, column_name)
except AttributeError:
pass
-
- try:
- return obj[column_name]
- except TypeError:
- pass
+ return obj[column_name]
def render_currency(self, obj, column_name):
value = self.obtain_value(obj, column_name)
@@ -863,13 +858,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():
@@ -1223,7 +1214,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
@@ -1240,10 +1230,7 @@ class Grid(WuttaGrid):
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 +1239,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 +1313,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!",
@@ -1411,10 +1420,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()
@@ -1470,22 +1475,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 +1511,6 @@ class Grid(WuttaGrid):
results = {
'data': data,
- 'row_classes': status_map,
- # TODO: deprecate / remove this
'row_status_map': status_map,
}
@@ -1548,11 +1539,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
diff --git a/tailbone/menus.py b/tailbone/menus.py
index 09d6f3f0..3ddee095 100644
--- a/tailbone/menus.py
+++ b/tailbone/menus.py
@@ -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",
diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako
index 8228f823..eb950011 100644
--- a/tailbone/templates/base.mako
+++ b/tailbone/templates/base.mako
@@ -632,23 +632,9 @@
% endif
% if request.is_root:
- ${h.form(url('stop_root'), ref='stopBeingRootForm')}
- ${h.csrf_token(request)}
-
-
- Stop being root
-
- ${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)}
-
-
- Become root
-
- ${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("Edit Preferences", url('my.preferences'), class_='navbar-item')}
${h.link_to("Logout", url('logout'), class_='navbar-item')}
@@ -686,7 +668,7 @@
text="Edit This">
% 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'):
diff --git a/tailbone/templates/configure.mako b/tailbone/templates/configure.mako
index e6b128fc..6d9c2261 100644
--- a/tailbone/templates/configure.mako
+++ b/tailbone/templates/configure.mako
@@ -92,7 +92,7 @@
-
+