From 8ea379bbff29c570b701b98bbaec74cc99d34b64 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Thu, 30 Jul 2020 16:38:03 -0500
Subject: [PATCH 0001/1524] Add more customization hooks for making grid
actions in master view
---
tailbone/views/master.py | 45 +++++++++++++++++++++++++++++-----------
1 file changed, 33 insertions(+), 12 deletions(-)
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index 7f52d9c4..d4850ef4 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -1139,7 +1139,7 @@ class MasterView(View):
self.request.session.flash("{} has been cloned: {}".format(
self.get_model_title(), self.get_instance_title(instance)))
self.request.session.flash("(NOTE, you are now viewing the clone!)")
- return self.redirect(self.get_action_url('view', cloned))
+ return self.redirect_after_clone(cloned)
return self.render_to_response('clone', {
'instance': instance,
'instance_title': self.get_instance_title(instance),
@@ -1167,6 +1167,9 @@ class MasterView(View):
self.Session.flush()
return cloned
+ def redirect_after_clone(self, instance, mobile=False):
+ return self.redirect(self.get_action_url('view', instance, mobile=mobile))
+
def touch(self):
"""
View for "touching" an object so as to trigger datasync logic for it.
@@ -2522,13 +2525,16 @@ class MasterView(View):
Return a list of 'main' actions for the grid.
"""
actions = []
- use_buefy = self.get_use_buefy()
if self.viewable and self.has_perm('view'):
- url = self.get_view_index_url if self.use_index_links else None
- icon = 'eye' if use_buefy else 'zoomin'
- actions.append(self.make_action('view', icon=icon, url=url))
+ actions.append(self.make_grid_action_view())
return actions
+ def make_grid_action_view(self):
+ use_buefy = self.get_use_buefy()
+ url = self.get_view_index_url if self.use_index_links else None
+ icon = 'eye' if use_buefy else 'zoomin'
+ return self.make_action('view', icon=icon, url=url)
+
def get_view_index_url(self, row, i):
route = '{}.view_index'.format(self.get_route_prefix())
return '{}?index={}'.format(self.request.route_url(route), self.first_visible_grid_index + i - 1)
@@ -2538,27 +2544,42 @@ class MasterView(View):
Return a list of 'more' actions for the grid.
"""
actions = []
- use_buefy = self.get_use_buefy()
# Edit
if self.editable and self.has_perm('edit'):
- icon = 'edit' if use_buefy else 'pencil'
- actions.append(self.make_action('edit', icon=icon, url=self.default_edit_url))
+ actions.append(self.make_grid_action_edit())
# Delete
if self.deletable and self.has_perm('delete'):
- kwargs = {}
- if use_buefy and self.delete_confirm == 'simple':
- kwargs['click_handler'] = 'deleteObject'
- actions.append(self.make_action('delete', icon='trash', url=self.default_delete_url, **kwargs))
+ actions.append(self.make_grid_action_delete())
return actions
+ def make_grid_action_edit(self):
+ use_buefy = self.get_use_buefy()
+ icon = 'edit' if use_buefy else 'pencil'
+ return self.make_action('edit', icon=icon, url=self.default_edit_url)
+
+ def make_grid_action_clone(self):
+ return self.make_action('clone', icon='object-ungroup',
+ url=self.default_clone_url)
+
+ def make_grid_action_delete(self):
+ use_buefy = self.get_use_buefy()
+ kwargs = {}
+ if use_buefy and self.delete_confirm == 'simple':
+ kwargs['click_handler'] = 'deleteObject'
+ return self.make_action('delete', icon='trash', url=self.default_delete_url, **kwargs)
+
def default_edit_url(self, row, i=None):
if self.editable_instance(row):
return self.request.route_url('{}.edit'.format(self.get_route_prefix()),
**self.get_action_route_kwargs(row))
+ def default_clone_url(self, row, i=None):
+ return self.request.route_url('{}.clone'.format(self.get_route_prefix()),
+ **self.get_action_route_kwargs(row))
+
def default_delete_url(self, row, i=None):
if self.deletable_instance(row):
return self.request.route_url('{}.delete'.format(self.get_route_prefix()),
From 6bd049e0bbbf6d93e9d43787be97b399097fe19c Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Thu, 30 Jul 2020 16:39:44 -0500
Subject: [PATCH 0002/1524] Update changelog
---
CHANGES.rst | 6 ++++++
tailbone/_version.py | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index e68bb48c..6457e598 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,12 @@
CHANGELOG
=========
+0.8.100 (2020-07-30)
+--------------------
+
+* Add more customization hooks for making grid actions in master view.
+
+
0.8.99 (2020-07-29)
-------------------
diff --git a/tailbone/_version.py b/tailbone/_version.py
index 4218b25a..0745e4ce 100644
--- a/tailbone/_version.py
+++ b/tailbone/_version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8; -*-
-__version__ = '0.8.99'
+__version__ = '0.8.100'
From 9a2a6bbc9f7ae979c0cbbc9260923b70b1d93c28 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Sat, 1 Aug 2020 22:18:54 -0500
Subject: [PATCH 0003/1524] Fix missing scrollbar when version diff table is
too wide for screen
at least, this seems to fix. not sure if/why we shouldn't apply this style
globally always, but playing it safe for now
---
tailbone/templates/master/view_version.mako | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/tailbone/templates/master/view_version.mako b/tailbone/templates/master/view_version.mako
index 71b51d39..13c87ae6 100644
--- a/tailbone/templates/master/view_version.mako
+++ b/tailbone/templates/master/view_version.mako
@@ -3,6 +3,17 @@
<%def name="title()">changes @ ver ${transaction.id}%def>
+<%def name="extra_styles()">
+ ${parent.extra_styles()}
+
+%def>
+
<%def name="page_content()">
## TODO: this was basically copied from Revel diff template..need to abstract
From 493785591cc6f6b5a037216c3e8004397370ad0d Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Sun, 2 Aug 2020 15:27:10 -0500
Subject: [PATCH 0004/1524] Add basic web views for "new customer order"
batches
---
tailbone/views/custorders/__init__.py | 5 +--
tailbone/views/custorders/batch.py | 41 +++++++++++++++++++++++++
tailbone/views/custorders/creating.py | 44 +++++++++++++++++++++++++++
3 files changed, 88 insertions(+), 2 deletions(-)
create mode 100644 tailbone/views/custorders/batch.py
create mode 100644 tailbone/views/custorders/creating.py
diff --git a/tailbone/views/custorders/__init__.py b/tailbone/views/custorders/__init__.py
index d2b4c5ed..78a3d3ab 100644
--- a/tailbone/views/custorders/__init__.py
+++ b/tailbone/views/custorders/__init__.py
@@ -1,8 +1,8 @@
-# -*- coding: utf-8 -*-
+# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2017 Lance Edgar
+# Copyright © 2010-2020 Lance Edgar
#
# This file is part of Rattail.
#
@@ -28,5 +28,6 @@ from __future__ import unicode_literals, absolute_import
def includeme(config):
+ config.include('tailbone.views.custorders.creating')
config.include('tailbone.views.custorders.orders')
config.include('tailbone.views.custorders.items')
diff --git a/tailbone/views/custorders/batch.py b/tailbone/views/custorders/batch.py
new file mode 100644
index 00000000..a49be977
--- /dev/null
+++ b/tailbone/views/custorders/batch.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2020 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 .
+#
+################################################################################
+"""
+Base class for customer order batch views
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+from rattail.db import model
+
+from tailbone.views.batch import BatchMasterView
+
+
+class CustomerOrderBatchView(BatchMasterView):
+ """
+ Master view base class, for customer order batches. The views for the
+ various mode/workflow batches will derive from this.
+ """
+ model_class = model.CustomerOrderBatch
+ model_row_class = model.CustomerOrderBatchRow
+ default_handler_spec = 'rattail.batch.custorder:CustomerOrderBatchHandler'
diff --git a/tailbone/views/custorders/creating.py b/tailbone/views/custorders/creating.py
new file mode 100644
index 00000000..29dc5b35
--- /dev/null
+++ b/tailbone/views/custorders/creating.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2020 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 .
+#
+################################################################################
+"""
+Views for 'creating' customer order batches
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+from tailbone.views.custorders.batch import CustomerOrderBatchView
+
+
+class CreateCustomerOrderBatchView(CustomerOrderBatchView):
+ """
+ Master view for "creating customer order" batches.
+ """
+ route_prefix = 'new_custorders'
+ url_prefix = '/new-customer-orders'
+ model_title = "New Customer Order"
+ model_title_plural = "New Customer Orders"
+ creatable = False
+
+
+def includeme(config):
+ CreateCustomerOrderBatchView.defaults(config)
From c32f47ba9533fe4c27625629fe6b5f27a4377873 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Sun, 2 Aug 2020 19:13:40 -0500
Subject: [PATCH 0005/1524] Tweak the buefy autocomplete component a bit
to better support staying in sync w/ data on the caller/parent side
---
.../static/js/tailbone.buefy.autocomplete.js | 35 +++++++++++++++----
tailbone/templates/autocomplete.mako | 2 +-
2 files changed, 29 insertions(+), 8 deletions(-)
diff --git a/tailbone/static/js/tailbone.buefy.autocomplete.js b/tailbone/static/js/tailbone.buefy.autocomplete.js
index 669b3c1f..fc64a073 100644
--- a/tailbone/static/js/tailbone.buefy.autocomplete.js
+++ b/tailbone/static/js/tailbone.buefy.autocomplete.js
@@ -22,23 +22,42 @@ const TailboneAutocomplete = {
data: [],
selected: selected,
isFetching: false,
- autocompleteValue: this.value,
}
},
+ watch: {
+ value(to, from) {
+ if (from && !to) {
+ this.clearSelection(false)
+ }
+ },
+ },
+
methods: {
- clearSelection() {
+ clearSelection(focus) {
+ if (focus === undefined) {
+ focus = true
+ }
this.selected = null
- this.autocompleteValue = null
- this.$nextTick(function() {
- this.$refs.autocomplete.focus()
- })
+ this.value = null
+ if (focus) {
+ this.$nextTick(function() {
+ this.$refs.autocomplete.focus()
+ })
+ }
// TODO: should emit event for caller logic (can they cancel?)
// $('#' + oid + '-textbox').trigger('autocompletevaluecleared');
},
+ getDisplayText() {
+ if (this.selected) {
+ return this.selected.label
+ }
+ return ""
+ },
+
// TODO: should we allow custom callback? or is event enough?
// function (oid) {
// $('#' + oid + '-textbox').on('autocompletevaluecleared', function() {
@@ -62,7 +81,9 @@ const TailboneAutocomplete = {
// }
itemSelected(value) {
- this.$emit('input', value)
+ if (this.selected || !value) {
+ this.$emit('input', value)
+ }
},
// TODO: buefy example uses `debounce()` here and perhaps we should too?
diff --git a/tailbone/templates/autocomplete.mako b/tailbone/templates/autocomplete.mako
index 7ec61f4c..c9de4507 100644
--- a/tailbone/templates/autocomplete.mako
+++ b/tailbone/templates/autocomplete.mako
@@ -65,7 +65,7 @@
Date: Sun, 2 Aug 2020 20:59:16 -0500
Subject: [PATCH 0006/1524] Add basic/unfinished "new customer order"
page/feature
so far creates the order batch, and can set some customer info
---
tailbone/templates/custorders/create.mako | 426 ++++++++++++++++++++++
tailbone/views/custorders/batch.py | 81 ++++
tailbone/views/custorders/creating.py | 5 +
tailbone/views/custorders/orders.py | 129 ++++++-
4 files changed, 639 insertions(+), 2 deletions(-)
create mode 100644 tailbone/templates/custorders/create.mako
diff --git a/tailbone/templates/custorders/create.mako b/tailbone/templates/custorders/create.mako
new file mode 100644
index 00000000..24245b7a
--- /dev/null
+++ b/tailbone/templates/custorders/create.mako
@@ -0,0 +1,426 @@
+## -*- coding: utf-8; -*-
+<%inherit file="/master/create.mako" />
+
+<%def name="extra_styles()">
+ ${parent.extra_styles()}
+ % if use_buefy:
+
+ % endif
+%def>
+
+<%def name="page_content()">
+
+ % if use_buefy:
+
+ % else:
+ Sorry, but this page is not supported by your current theme configuration.
+ % endif
+%def>
+
+<%def name="order_form_buttons()">
+
+
+ Submit this Order
+
+
+ Start Over Entirely
+
+
+ Cancel this Order
+
+
+%def>
+
+<%def name="render_this_page_template()">
+ ${parent.render_this_page_template()}
+
+
+%def>
+
+<%def name="make_this_page_component()">
+ ${parent.make_this_page_component()}
+
+%def>
+
+
+${parent.body()}
diff --git a/tailbone/views/custorders/batch.py b/tailbone/views/custorders/batch.py
index a49be977..c8b6280f 100644
--- a/tailbone/views/custorders/batch.py
+++ b/tailbone/views/custorders/batch.py
@@ -26,8 +26,13 @@ Base class for customer order batch views
from __future__ import unicode_literals, absolute_import
+import six
+
from rattail.db import model
+import colander
+
+from tailbone import forms
from tailbone.views.batch import BatchMasterView
@@ -39,3 +44,79 @@ class CustomerOrderBatchView(BatchMasterView):
model_class = model.CustomerOrderBatch
model_row_class = model.CustomerOrderBatchRow
default_handler_spec = 'rattail.batch.custorder:CustomerOrderBatchHandler'
+
+ grid_columns = [
+ 'id',
+ 'customer',
+ 'rows',
+ 'created',
+ 'created_by',
+ ]
+
+ form_fields = [
+ 'id',
+ 'customer',
+ 'person',
+ 'phone_number',
+ 'email_address',
+ 'created',
+ 'created_by',
+ 'rows',
+ 'status_code',
+ ]
+
+ def configure_grid(self, g):
+ super(CustomerOrderBatchView, self).configure_grid(g)
+
+ g.set_link('customer')
+ g.set_link('created')
+ g.set_link('created_by')
+
+ def configure_form(self, f):
+ super(CustomerOrderBatchView, self).configure_form(f)
+ order = f.model_instance
+ model = self.rattail_config.get_model()
+
+ # readonly fields
+ f.set_readonly('rows')
+ f.set_readonly('status_code')
+
+ # customer
+ if 'customer' in f.fields and self.editing:
+ f.replace('customer', 'customer_uuid')
+ f.set_node('customer_uuid', colander.String(), missing=colander.null)
+ customer_display = ""
+ if self.request.method == 'POST':
+ if self.request.POST.get('customer_uuid'):
+ customer = self.Session.query(model.Customer)\
+ .get(self.request.POST['customer_uuid'])
+ if customer:
+ customer_display = six.text_type(customer)
+ elif self.editing:
+ customer_display = six.text_type(order.customer or "")
+ customers_url = self.request.route_url('customers.autocomplete')
+ f.set_widget('customer_uuid', forms.widgets.JQueryAutocompleteWidget(
+ field_display=customer_display, service_url=customers_url))
+ f.set_label('customer_uuid', "Customer")
+ else:
+ f.set_renderer('customer', self.render_customer)
+
+ # person
+ if 'person' in f.fields and self.editing:
+ f.replace('person', 'person_uuid')
+ f.set_node('person_uuid', colander.String(), missing=colander.null)
+ person_display = ""
+ if self.request.method == 'POST':
+ if self.request.POST.get('person_uuid'):
+ person = self.Session.query(model.Person)\
+ .get(self.request.POST['person_uuid'])
+ if person:
+ person_display = six.text_type(person)
+ elif self.editing:
+ person_display = six.text_type(order.person or "")
+ people_url = self.request.route_url('people.autocomplete')
+ f.set_widget('person_uuid', forms.widgets.JQueryAutocompleteWidget(
+ field_display=person_display, service_url=people_url))
+ f.set_label('person_uuid', "Person")
+ else:
+ f.set_renderer('person', self.render_person)
diff --git a/tailbone/views/custorders/creating.py b/tailbone/views/custorders/creating.py
index 29dc5b35..c14448eb 100644
--- a/tailbone/views/custorders/creating.py
+++ b/tailbone/views/custorders/creating.py
@@ -22,6 +22,11 @@
################################################################################
"""
Views for 'creating' customer order batches
+
+Note that this provides only the "direct" or "raw" table views for these
+batches. This does *not* provide a way to create a new batch; you should see
+:meth:`tailbone.views.custorders.orders.CustomerOrdersView.create()` for that
+logic.
"""
from __future__ import unicode_literals, absolute_import
diff --git a/tailbone/views/custorders/orders.py b/tailbone/views/custorders/orders.py
index dee21f15..9ffc06c8 100644
--- a/tailbone/views/custorders/orders.py
+++ b/tailbone/views/custorders/orders.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2018 Lance Edgar
+# Copyright © 2010-2020 Lance Edgar
#
# This file is part of Rattail.
#
@@ -43,7 +43,6 @@ class CustomerOrdersView(MasterView):
"""
model_class = model.CustomerOrder
route_prefix = 'custorders'
- creatable = False
editable = False
deletable = False
@@ -59,6 +58,8 @@ class CustomerOrdersView(MasterView):
'id',
'customer',
'person',
+ 'phone_number',
+ 'email_address',
'created',
'status_code',
]
@@ -115,6 +116,130 @@ class CustomerOrdersView(MasterView):
url = self.request.route_url('people.view', uuid=person.uuid)
return tags.link_to(text, url)
+ def create(self, form=None, template='create'):
+ """
+ View for creating a new customer order. Note that it does so by way of
+ maintaining a "new customer order" batch, until the user finally
+ submits the order, at which point the batch is converted to a proper
+ order.
+ """
+ batch = self.get_current_batch()
+
+ if self.request.method == 'POST':
+
+ # first we check for traditional form post
+ action = self.request.POST.get('action')
+ post_actions = [
+ 'start_over_entirely',
+ 'delete_batch',
+ ]
+ if action in post_actions:
+ return getattr(self, action)(batch)
+
+ # okay then, we'll assume newer JSON-style post params
+ data = dict(self.request.json_body)
+ action = data.get('action')
+ json_actions = [
+ 'get_customer_info',
+ 'set_customer_data',
+ 'submit_new_order',
+ ]
+ if action in json_actions:
+ result = getattr(self, action)(batch, data)
+ return self.json_response(result)
+
+ context = {'batch': batch}
+ return self.render_to_response(template, context)
+
+ def get_current_batch(self):
+ user = self.request.user
+ if not user:
+ raise RuntimeError("this feature requires a user to be logged in")
+
+ try:
+ # there should be at most *one* new batch per user
+ batch = self.Session.query(model.CustomerOrderBatch)\
+ .filter(model.CustomerOrderBatch.mode == self.enum.CUSTORDER_BATCH_MODE_CREATING)\
+ .filter(model.CustomerOrderBatch.created_by == user)\
+ .one()
+
+ except orm.exc.NoResultFound:
+ # no batch yet for this user, so make one
+ batch = model.CustomerOrderBatch()
+ batch.mode = self.enum.CUSTORDER_BATCH_MODE_CREATING
+ batch.created_by = user
+ self.Session.add(batch)
+ self.Session.flush()
+
+ return batch
+
+ def start_over_entirely(self, batch):
+ # just delete current batch outright
+ # TODO: should use self.handler.do_delete() instead?
+ self.Session.delete(batch)
+ self.Session.flush()
+
+ # send user back to normal "create" page; a new batch will be generated
+ # for them automatically
+ route_prefix = self.get_route_prefix()
+ url = self.request.route_url('{}.create'.format(route_prefix))
+ return self.redirect(url)
+
+ def delete_batch(self, batch):
+ # just delete current batch outright
+ # TODO: should use self.handler.do_delete() instead?
+ self.Session.delete(batch)
+ self.Session.flush()
+
+ # set flash msg just to be more obvious
+ self.request.session.flash("New customer order has been deleted.")
+
+ # send user back to customer orders page, w/ no new batch generated
+ route_prefix = self.get_route_prefix()
+ url = self.request.route_url(route_prefix)
+ return self.redirect(url)
+
+ def get_customer_info(self, batch, data):
+ uuid = data.get('uuid')
+ if not uuid:
+ return {'error': "Must specify a customer UUID"}
+
+ customer = self.Session.query(model.Customer).get(uuid)
+ if not customer:
+ return {'error': "Customer not found"}
+
+ return self.info_for_customer(batch, data, customer)
+
+ def info_for_customer(self, batch, data, customer):
+ phone = customer.first_phone()
+ email = customer.first_email()
+ return {
+ 'uuid': customer.uuid,
+ 'phone_number': phone.number if phone else None,
+ 'email_address': email.address if email else None,
+ }
+
+ def set_customer_data(self, batch, data):
+ if 'customer_uuid' in data:
+ batch.customer_uuid = data['customer_uuid']
+ if 'person_uuid' in data:
+ batch.person_uuid = data['person_uuid']
+ elif batch.customer_uuid:
+ self.Session.flush()
+ batch.person = batch.customer.first_person()
+ else: # no customer set
+ batch.person_uuid = None
+ if 'phone_number' in data:
+ batch.phone_number = data['phone_number']
+ if 'email_address' in data:
+ batch.email_address = data['email_address']
+ self.Session.flush()
+ return {'success': True}
+
+ def submit_new_order(self, batch, data):
+ # TODO
+ return {'success': True}
+
def includeme(config):
CustomerOrdersView.defaults(config)
From 7d158e58b5f6420ce5c9130ed9493fa754966384 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Thu, 6 Aug 2020 02:04:17 -0500
Subject: [PATCH 0007/1524] Add `protected_usernames()` config function
---
tailbone/config.py | 3 +++
tailbone/views/users.py | 16 ++++++++--------
2 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/tailbone/config.py b/tailbone/config.py
index 875bc25b..ebd8899e 100644
--- a/tailbone/config.py
+++ b/tailbone/config.py
@@ -56,3 +56,6 @@ class ConfigExtension(BaseExtension):
def legacy_mobile_enabled(config):
return config.getbool('tailbone', 'legacy_mobile.enabled',
default=True)
+
+def protected_usernames(config):
+ return config.getlist('tailbone', 'protected_usernames')
diff --git a/tailbone/views/users.py b/tailbone/views/users.py
index 79e2590c..93869d5d 100644
--- a/tailbone/views/users.py
+++ b/tailbone/views/users.py
@@ -42,6 +42,7 @@ from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.views.principal import PrincipalMasterView, PermissionsRenderer
+from tailbone.config import protected_usernames
class UsersView(PrincipalMasterView):
@@ -139,9 +140,9 @@ class UsersView(PrincipalMasterView):
user is "root". But if the given user is not protected, this simply
returns ``True``.
"""
- if self.user_is_protected(user):
- return self.request.is_root
- return True
+ if self.request.is_root:
+ return True
+ return not self.user_is_protected(user)
def deletable_instance(self, user):
"""
@@ -149,9 +150,9 @@ class UsersView(PrincipalMasterView):
user is "root". But if the given user is not protected, this simply
returns ``True``.
"""
- if self.user_is_protected(user):
- return self.request.is_root
- return True
+ if self.request.is_root:
+ return True
+ return not self.user_is_protected(user)
def user_is_protected(self, user):
"""
@@ -165,8 +166,7 @@ class UsersView(PrincipalMasterView):
"root", otherwise will return ``False``.
"""
if not hasattr(self, 'protected_usernames'):
- self.protected_usernames = self.rattail_config.getlist(
- 'tailbone', 'protected_usernames')
+ self.protected_usernames = protected_usernames(self.rattail_config)
if self.protected_usernames and user.username in self.protected_usernames:
return True
return False
From 437157440308179ef6171fc26155fad2876b5a72 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Sun, 9 Aug 2020 14:03:28 -0500
Subject: [PATCH 0008/1524] Add `model` to global template context, plus
`h.maxlen()`
sometimes it's nice to just add a `maxlength="100"` or whatever to an input tag
within some random template. that should "just be possible" with no extra
effort
---
tailbone/helpers.py | 1 +
tailbone/subscribers.py | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/tailbone/helpers.py b/tailbone/helpers.py
index 3a3d8365..46a30dec 100644
--- a/tailbone/helpers.py
+++ b/tailbone/helpers.py
@@ -32,6 +32,7 @@ from decimal import Decimal
from rattail.time import localtime, make_utc
from rattail.util import (pretty_quantity, pretty_hours, hours_as_decimal,
OrderedDict)
+from rattail.db.util import maxlen
from webhelpers2.html import *
from webhelpers2.html.tags import *
diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py
index 90930e60..af88f7a7 100644
--- a/tailbone/subscribers.py
+++ b/tailbone/subscribers.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2019 Lance Edgar
+# Copyright © 2010-2020 Lance Edgar
#
# This file is part of Rattail.
#
@@ -99,6 +99,7 @@ def before_render(event):
renderer_globals['url'] = request.route_url
renderer_globals['rattail'] = rattail
renderer_globals['tailbone'] = tailbone
+ renderer_globals['model'] = request.rattail_config.get_model()
renderer_globals['enum'] = request.rattail_config.get_enum()
renderer_globals['six'] = six
renderer_globals['json'] = json
From 163134326aaab45620a71d72795ba33d5b35fec9 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Sun, 9 Aug 2020 14:32:16 -0500
Subject: [PATCH 0009/1524] Coalesce on `User.active` when merging
---
tailbone/views/users.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tailbone/views/users.py b/tailbone/views/users.py
index 93869d5d..078e99ca 100644
--- a/tailbone/views/users.py
+++ b/tailbone/views/users.py
@@ -86,6 +86,7 @@ class UsersView(PrincipalMasterView):
merge_coalesce_fields = [
'person_uuid',
'person_name',
+ 'active',
]
merge_fields = merge_additive_fields + [
'uuid',
@@ -93,7 +94,6 @@ class UsersView(PrincipalMasterView):
'person_uuid',
'person_name',
'role_count',
- 'active',
]
def query(self, session):
From ca31af196f6b456baf01914f48f687ec0d9415eb Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Sun, 9 Aug 2020 14:39:31 -0500
Subject: [PATCH 0010/1524] Expose user reference(s) for employees
---
tailbone/views/employees.py | 23 +++++++++++++++++++++--
tailbone/views/master.py | 12 ++++++++++++
2 files changed, 33 insertions(+), 2 deletions(-)
diff --git a/tailbone/views/employees.py b/tailbone/views/employees.py
index 488e1f65..dac93e67 100644
--- a/tailbone/views/employees.py
+++ b/tailbone/views/employees.py
@@ -60,6 +60,7 @@ class EmployeesView(MasterView):
'phone',
'email',
'status',
+ 'username',
]
form_fields = [
@@ -73,6 +74,7 @@ class EmployeesView(MasterView):
'full_time',
'full_time_start',
'id',
+ 'users',
'stores',
'departments',
]
@@ -90,15 +92,25 @@ class EmployeesView(MasterView):
factory=grids.filters.AlchemyPhoneNumberFilter)
g.set_sorter('phone', model.EmployeePhoneNumber.number)
+ # email
g.joiners['email'] = lambda q: q.outerjoin(model.EmployeeEmailAddress, sa.and_(
model.EmployeeEmailAddress.parent_uuid == model.Employee.uuid,
model.EmployeeEmailAddress.preference == 1))
+ g.filters['email'] = g.make_filter('email', model.EmployeeEmailAddress.address,
+ label="Email Address")
+ # first/last name
g.filters['first_name'] = g.make_filter('first_name', model.Person.first_name)
g.filters['last_name'] = g.make_filter('last_name', model.Person.last_name)
- g.filters['email'] = g.make_filter('email', model.EmployeeEmailAddress.address,
- label="Email Address")
+ # username
+ if self.request.has_perm('users.view'):
+ g.set_joiner('username', lambda q: q.outerjoin(model.User))
+ g.set_filter('username', model.User.username)
+ g.set_sorter('username', model.User.username)
+ g.set_renderer('username', self.grid_render_username)
+ else:
+ g.hide_column('username')
# id
if self.request.has_perm('{}.edit'.format(route_prefix)):
@@ -142,6 +154,12 @@ class EmployeesView(MasterView):
q = q.filter(model.Employee.status == self.enum.EMPLOYEE_STATUS_CURRENT)
return q
+ def grid_render_username(self, employee, field):
+ person = employee.person if employee else None
+ if not person:
+ return ""
+ return ", ".join([u.username for u in person.users])
+
def grid_extra_class(self, employee, i):
if employee.status == self.enum.EMPLOYEE_STATUS_FORMER:
return 'warning'
@@ -161,6 +179,7 @@ class EmployeesView(MasterView):
employee = f.model_instance
f.set_renderer('person', self.render_person)
+ f.set_renderer('users', self.render_users)
f.set_renderer('stores', self.render_stores)
f.set_label('stores', "Stores") # TODO: should not be necessary
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index d4850ef4..10bd5c26 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -947,6 +947,18 @@ class MasterView(View):
url = self.request.route_url('users.view', uuid=user.uuid)
return tags.link_to(text, url)
+ def render_users(self, obj, field):
+ users = obj.users
+ if not users:
+ return ""
+
+ items = []
+ for user in users:
+ text = user.username
+ url = self.request.route_url('users.view', uuid=user.uuid)
+ items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
+ return HTML.tag('ul', c=items)
+
def render_customer(self, obj, field):
customer = getattr(obj, field)
if not customer:
From b4ea1489a7075b562767f41fb1faa3ef4ec999cf Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Sun, 9 Aug 2020 15:06:41 -0500
Subject: [PATCH 0011/1524] Update changelog
---
CHANGES.rst | 20 ++++++++++++++++++++
tailbone/_version.py | 2 +-
2 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 6457e598..1516feb7 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,26 @@
CHANGELOG
=========
+0.8.101 (2020-08-09)
+--------------------
+
+* Fix missing scrollbar when version diff table is too wide for screen.
+
+* Add basic web views for "new customer order" batches.
+
+* Tweak the buefy autocomplete component a bit.
+
+* Add basic/unfinished "new customer order" page/feature.
+
+* Add ``protected_usernames()`` config function.
+
+* Add ``model`` to global template context, plus ``h.maxlen()``.
+
+* Coalesce on ``User.active`` when merging.
+
+* Expose user reference(s) for employees.
+
+
0.8.100 (2020-07-30)
--------------------
diff --git a/tailbone/_version.py b/tailbone/_version.py
index 0745e4ce..c5157649 100644
--- a/tailbone/_version.py
+++ b/tailbone/_version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8; -*-
-__version__ = '0.8.100'
+__version__ = '0.8.101'
From d0e7f7dda2039cc2fc2cef0c022663cb03e62b96 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Sun, 9 Aug 2020 15:50:25 -0500
Subject: [PATCH 0012/1524] Improve rendering of `true_margin` column for
pricing batch row grid
---
tailbone/views/batch/pricing.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/tailbone/views/batch/pricing.py b/tailbone/views/batch/pricing.py
index 34586ea1..88063d00 100644
--- a/tailbone/views/batch/pricing.py
+++ b/tailbone/views/batch/pricing.py
@@ -26,6 +26,8 @@ Views for pricing batches
from __future__ import unicode_literals, absolute_import
+import six
+
from rattail.db import model
from rattail.time import localtime
@@ -203,12 +205,26 @@ class PricingBatchView(BatchMasterView):
g.set_renderer('current_price', self.render_current_price)
+ g.set_renderer('true_margin', self.render_true_margin)
+
def render_vendor_id(self, row, field):
vendor_id = row.vendor.id if row.vendor else None
if not vendor_id:
return ""
return vendor_id
+ def render_true_margin(self, row, field):
+ margin = row.true_margin
+ if margin:
+ margin = six.text_type(margin)
+ else:
+ margin = HTML.literal(' ')
+ if row.old_true_margin is not None:
+ title = "WAS: {}".format(row.old_true_margin)
+ else:
+ title = "WAS: NULL"
+ return HTML.tag('span', title=title, c=[margin])
+
def row_grid_extra_class(self, row, i):
extra_class = None
From dca890f16900a183ca561b751d1845b4ee3c6e05 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Mon, 10 Aug 2020 19:37:29 -0500
Subject: [PATCH 0013/1524] Update changelog
---
CHANGES.rst | 6 ++++++
tailbone/_version.py | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 1516feb7..d238ac5a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,12 @@
CHANGELOG
=========
+0.8.102 (2020-08-10)
+--------------------
+
+* Improve rendering of ``true_margin`` column for pricing batch row grid.
+
+
0.8.101 (2020-08-09)
--------------------
diff --git a/tailbone/_version.py b/tailbone/_version.py
index c5157649..9acd832b 100644
--- a/tailbone/_version.py
+++ b/tailbone/_version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8; -*-
-__version__ = '0.8.101'
+__version__ = '0.8.102'
From aac0e7d35c9011b3602d2525756b96a04313f9fb Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Tue, 11 Aug 2020 18:28:03 -0500
Subject: [PATCH 0014/1524] Tweak config methods for customer master view
---
tailbone/views/customers.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/tailbone/views/customers.py b/tailbone/views/customers.py
index cdb44429..a5cf963a 100644
--- a/tailbone/views/customers.py
+++ b/tailbone/views/customers.py
@@ -385,14 +385,17 @@ class CustomersView(MasterView):
@classmethod
def defaults(cls, config):
+ cls._defaults(config)
+ cls._customer_defaults(config)
+
+ @classmethod
+ def _customer_defaults(cls, config):
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
permission_prefix = cls.get_permission_prefix()
model_key = cls.get_model_key()
model_title = cls.get_model_title()
- cls._defaults(config)
-
# detach person
if cls.people_detachable:
config.add_tailbone_permission(permission_prefix, '{}.detach_person'.format(permission_prefix),
From 7924502b657a5de7076e5a23307241e830bb0fc0 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Thu, 13 Aug 2020 12:55:17 -0500
Subject: [PATCH 0015/1524] Update changelog
---
CHANGES.rst | 6 ++++++
tailbone/_version.py | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index d238ac5a..728a3483 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,12 @@
CHANGELOG
=========
+0.8.103 (2020-08-13)
+--------------------
+
+* Tweak config methods for customer master view.
+
+
0.8.102 (2020-08-10)
--------------------
diff --git a/tailbone/_version.py b/tailbone/_version.py
index 9acd832b..ce6cf316 100644
--- a/tailbone/_version.py
+++ b/tailbone/_version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8; -*-
-__version__ = '0.8.102'
+__version__ = '0.8.103'
From a038f2a98dcb7f2f2263eddbe3d6cf0ee2e1d7b5 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Sun, 16 Aug 2020 16:57:06 -0500
Subject: [PATCH 0016/1524] Make "download row results" a bit more generic
to handle non-native table/rows, w/ non-uuid key
---
tailbone/templates/master/view.mako | 4 ++--
tailbone/views/master.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako
index d07e1cc9..94454bd9 100644
--- a/tailbone/templates/master/view.mako
+++ b/tailbone/templates/master/view.mako
@@ -72,8 +72,8 @@
% if master.has_rows and master.rows_downloadable_csv and request.has_perm('{}.row_results_csv'.format(permission_prefix)):
${h.link_to("Download row results as CSV", url('{}.row_results_csv'.format(route_prefix), uuid=instance.uuid))}
% endif
- % if master.has_rows and master.rows_downloadable_xlsx and request.has_perm('{}.row_results_xlsx'.format(permission_prefix)):
- ${h.link_to("Download row results as XLSX", url('{}.row_results_xlsx'.format(route_prefix), uuid=instance.uuid))}
+ % if master.has_rows and master.rows_downloadable_xlsx and master.has_perm('row_results_xlsx'):
+ ${h.link_to("Download row results as XLSX", master.get_action_url('row_results_xlsx', instance))}
% endif
%def>
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index 10bd5c26..827e5500 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -3868,7 +3868,7 @@ class MasterView(View):
if cls.has_rows and cls.rows_downloadable_xlsx:
config.add_tailbone_permission(permission_prefix, '{}.row_results_xlsx'.format(permission_prefix),
"Download {} results as XLSX".format(row_model_title))
- config.add_route('{}.row_results_xlsx'.format(route_prefix), '{}/{{uuid}}/rows-xlsx'.format(url_prefix))
+ config.add_route('{}.row_results_xlsx'.format(route_prefix), '{}/rows-xlsx'.format(instance_url_prefix))
config.add_view(cls, attr='row_results_xlsx', route_name='{}.row_results_xlsx'.format(route_prefix),
permission='{}.row_results_xlsx'.format(permission_prefix))
From b5028ab2d0e8a240657b6b274befe9e8efc64e30 Mon Sep 17 00:00:00 2001
From: Lance Edgar
Date: Mon, 17 Aug 2020 21:38:12 -0500
Subject: [PATCH 0017/1524] Add pagination to price, cost history grids for
product view
---
tailbone/grids/core.py | 3 +++
tailbone/templates/grids/b-table.mako | 4 ++++
tailbone/templates/products/view.mako | 8 ++++----
3 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py
index 60934879..d475370c 100644
--- a/tailbone/grids/core.py
+++ b/tailbone/grids/core.py
@@ -985,6 +985,9 @@ class Grid(object):
context['empty_labels'] = empty_labels
if 'grid_columns' not in context:
context['grid_columns'] = self.get_buefy_columns()
+ context.setdefault('paginated', False)
+ if context['paginated']:
+ context.setdefault('per_page', 20)
# locate the 'view' action
# TODO: this should be easier, and/or moved elsewhere?
diff --git a/tailbone/templates/grids/b-table.mako b/tailbone/templates/grids/b-table.mako
index 42e82273..8608b456 100644
--- a/tailbone/templates/grids/b-table.mako
+++ b/tailbone/templates/grids/b-table.mako
@@ -5,6 +5,10 @@
striped
hoverable
narrowed
+ % if paginated:
+ paginated
+ per-page="${per_page}"
+ % endif
% if vshow is not Undefined and vshow:
v-show="${vshow}"
% endif
diff --git a/tailbone/templates/products/view.mako b/tailbone/templates/products/view.mako
index c8f2a5ec..23b6d2ca 100644
--- a/tailbone/templates/products/view.mako
+++ b/tailbone/templates/products/view.mako
@@ -380,7 +380,7 @@
- ${regular_price_history_grid.render_buefy_table_element(data_prop='regularPriceHistoryData', loading='regularPriceHistoryLoading')|n}
+ ${regular_price_history_grid.render_buefy_table_element(data_prop='regularPriceHistoryData', loading='regularPriceHistoryLoading', paginated=True, per_page=10)|n}