Compare commits

..

No commits in common. "master" and "v0.22.1" have entirely different histories.

15 changed files with 63 additions and 216 deletions

View file

@ -5,51 +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

View file

@ -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

View file

@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "Tailbone"
version = "0.22.7"
version = "0.22.1"
description = "Backoffice Web Application for Rattail"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
@ -53,13 +53,13 @@ dependencies = [
"pyramid_mako",
"pyramid_retry",
"pyramid_tm",
"rattail[db,bouncer]>=0.20.1",
"rattail[db,bouncer]>=0.18.5",
"sa-filters",
"simplejson",
"transaction",
"waitress",
"WebHelpers2",
"WuttaWeb>=0.21.0",
"WuttaWeb>=0.14.0",
"zope.sqlalchemy>=1.5",
]

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -235,7 +235,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'))
@ -1223,7 +1223,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 +1239,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):
""" """

View file

@ -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",

View file

@ -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|{};"

View file

@ -196,7 +196,6 @@
<p class="block has-text-weight-bold">
{{ version.model_title }}
({{ version.operation }})
</p>
<table class="diff monospace is-size-7"

View file

@ -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

View file

@ -44,6 +44,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):
@ -94,6 +116,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 +132,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,23 +181,7 @@ 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()

View file

@ -903,7 +903,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 +939,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")
@ -1382,7 +1382,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,
@ -2153,7 +2153,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 +2594,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 +2617,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 +2639,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 +2673,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()
@ -5541,7 +5541,7 @@ class MasterView(View):
input_file_templates=True,
output_file_templates=True):
app = self.get_rattail_app()
model = self.app.model
model = self.model
names = []
if simple_settings is None:
@ -6100,7 +6100,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 +6127,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 +6160,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
"""

View file

@ -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()
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
key_field = app.get_customer_key_field()
customer_key = getattr(customer, key_field)
if customer_key is not None:
customer_key = str(customer_key)
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)

View file

@ -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)