From 0209957defdf877f98d648494bc37c1a15a3640d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 1 Feb 2021 17:15:39 -0600 Subject: [PATCH 0001/1385] Fix config defaults for PurchaseView so can customize that more easily --- tailbone/views/purchases/core.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tailbone/views/purchases/core.py b/tailbone/views/purchases/core.py index 2c31e904..2cd28be8 100644 --- a/tailbone/views/purchases/core.py +++ b/tailbone/views/purchases/core.py @@ -355,21 +355,23 @@ class PurchaseView(MasterView): @classmethod def defaults(cls, config): + cls._purchase_defaults(config) + cls._defaults(config) + + @classmethod + def _purchase_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) - # receiving worksheet config.add_tailbone_permission(permission_prefix, '{}.receiving_worksheet'.format(permission_prefix), "Print receiving worksheet for {}".format(model_title)) config.add_route('{}.receiving_worksheet'.format(route_prefix), '{}/{{{}}}/receiving-worksheet'.format(url_prefix, model_key)) config.add_view(cls, attr='receiving_worksheet', route_name='{}.receiving_worksheet'.format(route_prefix), permission='{}.receiving_worksheet'.format(permission_prefix)) - def includeme(config): From e3bf7f2bb267ad07aba8597ca49b39fbff467a20 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 2 Feb 2021 10:57:58 -0600 Subject: [PATCH 0002/1385] Add stub methods for `MasterView.template_kwargs_view()` etc. otherwise subclass has to consider, can i call super() or not? it still does for some other views, but at least create/view/edit are common enough that it should always be able to call super() without concern for those --- tailbone/views/master.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 68130a2f..a0e3a3b0 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -2077,6 +2077,24 @@ class MasterView(View): return kwargs + def template_kwargs_create(self, **kwargs): + """ + Method stub, so subclass can always invoke super() for it. + """ + return kwargs + + def template_kwargs_view(self, **kwargs): + """ + Method stub, so subclass can always invoke super() for it. + """ + return kwargs + + def template_kwargs_edit(self, **kwargs): + """ + Method stub, so subclass can always invoke super() for it. + """ + return kwargs + def get_db_engines(self): """ Must return a dict (or even better, OrderedDict) which contains all From 9b76e233544ddeaaa557a285ca4d71f5b573e4e5 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 2 Feb 2021 13:28:56 -0600 Subject: [PATCH 0003/1385] Update references to vendor catalog batches per table/model rename --- tailbone/views/batch/vendorcatalog.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tailbone/views/batch/vendorcatalog.py b/tailbone/views/batch/vendorcatalog.py index cc9374f1..25eddd68 100644 --- a/tailbone/views/batch/vendorcatalog.py +++ b/tailbone/views/batch/vendorcatalog.py @@ -49,9 +49,10 @@ class VendorCatalogView(FileBatchMasterView): """ Master view for vendor catalog batches. """ - model_class = model.VendorCatalog - model_row_class = model.VendorCatalogRow + model_class = model.VendorCatalogBatch + model_row_class = model.VendorCatalogBatchRow default_handler_spec = 'rattail.batch.vendorcatalog:VendorCatalogHandler' + route_prefix = 'vendorcatalogs' url_prefix = '/vendors/catalogs' template_prefix = '/batch/vendorcatalog' editable = False From 0128690da88ee7ee1b7e9fb5be70540c47f7cd28 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 2 Feb 2021 13:45:53 -0600 Subject: [PATCH 0004/1385] Update references to vendor invoice batches per table/model rename --- tailbone/views/batch/vendorinvoice.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tailbone/views/batch/vendorinvoice.py b/tailbone/views/batch/vendorinvoice.py index c16a7a6a..bd030666 100644 --- a/tailbone/views/batch/vendorinvoice.py +++ b/tailbone/views/batch/vendorinvoice.py @@ -42,9 +42,10 @@ class VendorInvoiceView(FileBatchMasterView): """ Master view for vendor invoice batches. """ - model_class = model.VendorInvoice - model_row_class = model.VendorInvoiceRow + model_class = model.VendorInvoiceBatch + model_row_class = model.VendorInvoiceBatchRow default_handler_spec = 'rattail.batch.vendorinvoice:VendorInvoiceHandler' + route_prefix = 'vendorinvoices' url_prefix = '/vendors/invoices' grid_columns = [ From f93fd7aefa90f45d665e71138d6fc9241b0cc438 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 2 Feb 2021 14:48:34 -0600 Subject: [PATCH 0005/1385] Fix display of handheld batch links, when viewing label batch --- tailbone/views/batch/labels.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tailbone/views/batch/labels.py b/tailbone/views/batch/labels.py index 8aeab62b..5015ffdc 100644 --- a/tailbone/views/batch/labels.py +++ b/tailbone/views/batch/labels.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2021 Lance Edgar # # This file is part of Rattail. # @@ -138,11 +138,11 @@ class LabelBatchView(BatchMasterView): f.set_label('label_profile_uuid', "Label Profile") def render_handheld_batches(self, label_batch, field): - items = '' + items = [] for handheld in label_batch._handhelds: text = handheld.handheld.id_str url = self.request.route_url('batch.handheld.view', uuid=handheld.handheld_uuid) - items += HTML.tag('li', c=tags.link_to(text, url)) + items.append(HTML.tag('li', c=[tags.link_to(text, url)])) return HTML.tag('ul', c=items) def configure_row_grid(self, g): From 63350469d0eb8b3d12d10c78ab9208a19c033d9b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 2 Feb 2021 18:58:46 -0600 Subject: [PATCH 0006/1385] Prevent updates to batch rows, if batch is immutable probably need a lot more support for this elsewhere; this is all i needed for the moment though.. --- tailbone/api/batch/core.py | 7 ++++++- tailbone/api/batch/ordering.py | 5 ++++- tailbone/api/master.py | 8 ++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tailbone/api/batch/core.py b/tailbone/api/batch/core.py index 1200f703..a2f44596 100644 --- a/tailbone/api/batch/core.py +++ b/tailbone/api/batch/core.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2021 Lance Edgar # # This file is part of Rattail. # @@ -117,6 +117,7 @@ class APIBatchView(APIBatchMixin, APIMasterView): 'executed_display': self.pretty_datetime(executed) if executed else None, 'executed_by_uuid': batch.executed_by_uuid, 'executed_by_display': six.text_type(batch.executed_by or ''), + 'mutable': self.handler.is_mutable(batch), } def create_object(self, data): @@ -268,6 +269,7 @@ class APIBatchRowView(APIBatchMixin, APIMasterView): 'batch_description': batch.description, 'batch_complete': batch.complete, 'batch_executed': bool(batch.executed), + 'batch_mutable': self.handler.is_mutable(batch), 'sequence': row.sequence, 'status_code': row.status_code, 'status_display': row.STATUS.get(row.status_code, six.text_type(row.status_code)), @@ -280,6 +282,9 @@ class APIBatchRowView(APIBatchMixin, APIMasterView): Invokes the batch handler's ``refresh_row()`` method after updating the row's field data per usual. """ + if not self.handler.is_mutable(row.batch): + return {'error': "Batch is not mutable"} + # update row per usual row = super(APIBatchRowView, self).update_object(row, data) diff --git a/tailbone/api/batch/ordering.py b/tailbone/api/batch/ordering.py index 031bccdf..21de8da0 100644 --- a/tailbone/api/batch/ordering.py +++ b/tailbone/api/batch/ordering.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2021 Lance Edgar # # This file is part of Rattail. # @@ -267,6 +267,9 @@ class OrderingBatchRowViews(APIBatchRowView): Note that the "normal" logic for this method is not invoked at all. """ + if not self.handler.is_mutable(row.batch): + return {'error': "Batch is not mutable"} + self.handler.update_row_quantity(row, **data) return row diff --git a/tailbone/api/master.py b/tailbone/api/master.py index f215bee1..775292bc 100644 --- a/tailbone/api/master.py +++ b/tailbone/api/master.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2021 Lance Edgar # # This file is part of Rattail. # @@ -345,8 +345,12 @@ class APIMasterView(APIView): # assume our data comes only from request JSON body data = self.request.json_body - # update and return data for object + # try to update data for object, returning error as necessary obj = self.update_object(obj, data) + if isinstance(obj, dict) and 'error' in obj: + return {'error': obj['error']} + + # return data for object self.Session.flush() return self._get(obj) From 562d7b48bc0420b399bab5c0822bb88b3c2647b7 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 4 Feb 2021 11:04:00 -0600 Subject: [PATCH 0007/1385] Update changelog --- CHANGES.rst | 14 ++++++++++++++ tailbone/_version.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 60989a3a..eb919210 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,20 @@ CHANGELOG ========= +0.8.123 (2021-02-04) +-------------------- + +* Fix config defaults for PurchaseView. + +* Add stub methods for ``MasterView.template_kwargs_view()`` etc. + +* Update references to vendor catalog batches etc. + +* Fix display of handheld batch links, when viewing label batch. + +* Prevent updates to batch rows, if batch is immutable. + + 0.8.122 (2021-02-01) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index ab0a1242..5e5bd121 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.122' +__version__ = '0.8.123' From 8f69b07ee2085287697d61e87bfe4a9fb2aabae2 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 4 Feb 2021 16:44:40 -0600 Subject: [PATCH 0008/1385] Fix bug when editing a Person --- tailbone/views/people.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tailbone/views/people.py b/tailbone/views/people.py index de970119..6e72fe1f 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -180,7 +180,7 @@ class PersonView(MasterView): names['middle'] = data['middle_name'] if 'last_name' in form: names['last'] = data['last_name'] - if 'display_name' in form: + if 'display_name' in form and 'display_name' not in form.readonly_fields: names['full'] = data['display_name'] # TODO: why do we find colander.null values in data at this point? From 85403dfa5ea8a7afd42f1bc3d4f6d60baf2d1caa Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 4 Feb 2021 16:45:24 -0600 Subject: [PATCH 0009/1385] 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 eb919210..77fc38cf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +0.8.124 (2021-02-04) +-------------------- + +* Fix bug when editing a Person. + + 0.8.123 (2021-02-04) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 5e5bd121..c5d2b24f 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.123' +__version__ = '0.8.124' From cc2308c3992e064441fdcf6040ba20f532c0403d Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 9 Feb 2021 12:19:26 -0600 Subject: [PATCH 0010/1385] Fix some permission bugs when showing batch tools etc. --- tailbone/templates/batch/index.mako | 7 ++++++- tailbone/templates/master/index.mako | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tailbone/templates/batch/index.mako b/tailbone/templates/batch/index.mako index 8d54facc..89358567 100644 --- a/tailbone/templates/batch/index.mako +++ b/tailbone/templates/batch/index.mako @@ -150,7 +150,7 @@ <%def name="modify_this_page_vars()"> ${parent.modify_this_page_vars()} - % if master.results_executable and master.has_perm('execute_multiple'): + % if master.results_refreshable and master.has_perm('refresh'): + % endif + % if master.results_executable and master.has_perm('execute_multiple'): + + + +${parent.body()} diff --git a/tailbone/templates/people/merge-requests/view.mako b/tailbone/templates/people/merge-requests/view.mako new file mode 100644 index 00000000..5dcbea03 --- /dev/null +++ b/tailbone/templates/people/merge-requests/view.mako @@ -0,0 +1,40 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/view.mako" /> + +<%def name="page_content()"> + ${parent.page_content()} + % if not instance.merged and request.has_perm('people.merge'): + % if use_buefy: + ${h.form(url('people.merge'), **{'@submit': 'submitMergeForm'})} + ${h.csrf_token(request)} + ${h.hidden('uuids', value=','.join([instance.removing_uuid, instance.keeping_uuid]))} + + {{ mergeFormButtonText }} + + ${h.end_form()} + % endif + % endif + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + % if not instance.merged and request.has_perm('people.merge'): + + % endif + + +${parent.body()} diff --git a/tailbone/views/master.py b/tailbone/views/master.py index b21051cb..c6f0253d 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -378,7 +378,7 @@ class MasterView(View): Return a dictionary of kwargs to be passed to the factory when creating new grid instances. """ - checkboxes = self.checkboxes + checkboxes = kwargs.get('checkboxes', self.checkboxes) if not checkboxes and self.mergeable and self.has_perm('merge'): checkboxes = True if not checkboxes and self.supports_set_enabled_toggle and self.has_perm('enable_disable_set'): diff --git a/tailbone/views/people.py b/tailbone/views/people.py index b8e06ced..5b4064b3 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -33,7 +33,7 @@ import sqlalchemy as sa from sqlalchemy import orm from rattail.db import model, api -from rattail.time import localtime +from rattail.time import localtime, make_utc from rattail.util import OrderedDict import colander @@ -68,6 +68,7 @@ class PersonView(MasterView): 'last_name', 'phone', 'email', + 'merge_requested', ] form_fields = [ @@ -93,6 +94,15 @@ class PersonView(MasterView): app = self.get_rattail_app() self.handler = app.get_people_handler() + def make_grid_kwargs(self, **kwargs): + kwargs = super(PersonView, self).make_grid_kwargs(**kwargs) + + # turn on checkboxes if user can create a merge reqeust + if self.mergeable and self.has_perm('request_merge'): + kwargs['checkboxes'] = True + + return kwargs + def configure_grid(self, g): super(PersonView, self).configure_grid(g) @@ -123,6 +133,9 @@ class PersonView(MasterView): g.sorters['email'] = lambda q, d: q.order_by(getattr(model.PersonEmailAddress.address, d)()) g.sorters['phone'] = lambda q, d: q.order_by(getattr(model.PersonPhoneNumber.number, d)()) + g.set_label('merge_requested', "MR") + g.set_renderer('merge_requested', self.render_merge_requested) + g.set_sort_defaults('display_name') g.set_label('display_name', "Full Name") @@ -134,6 +147,23 @@ class PersonView(MasterView): g.set_link('first_name') g.set_link('last_name') + def render_merge_requested(self, person, field): + model = self.model + merge_request = self.Session.query(model.MergePeopleRequest)\ + .filter(sa.or_( + model.MergePeopleRequest.removing_uuid == person.uuid, + model.MergePeopleRequest.keeping_uuid == person.uuid))\ + .filter(model.MergePeopleRequest.merged == None)\ + .first() + if merge_request: + use_buefy = self.get_use_buefy() + if use_buefy: + return HTML.tag('span', + class_='has-text-danger has-text-weight-bold', + title="A merge has been requested for this person.", + c="MR") + return "MR" + def get_instance(self): # TODO: I don't recall why this fallback check for a vendor contact # exists here, but leaving it intact for now. @@ -383,7 +413,7 @@ class PersonView(MasterView): raise Exception(reason) def merge_objects(self, removing, keeping): - self.handler.perform_merge(removing, keeping) + self.handler.perform_merge(removing, keeping, user=self.request.user) def view_profile(self): """ @@ -696,6 +726,18 @@ class PersonView(MasterView): self.request.session.flash("User has been created: {}".format(user.username)) return self.redirect(self.request.route_url('users.view', uuid=user.uuid)) + def request_merge(self): + """ + Create a new merge request for the given 2 people. + """ + merge = self.model.MergePeopleRequest() + merge.removing_uuid = self.request.POST['removing_uuid'] + merge.keeping_uuid = self.request.POST['keeping_uuid'] + merge.requested_by = self.request.user + merge.requested = make_utc() + self.Session.add(merge) + return self.redirect(self.get_index_url()) + @classmethod def defaults(cls, config): cls._people_defaults(config) @@ -709,6 +751,7 @@ class PersonView(MasterView): instance_url_prefix = cls.get_instance_url_prefix() model_key = cls.get_model_key() model_title = cls.get_model_title() + model_title_plural = cls.get_model_title_plural() # "profile" perms # TODO: should let view class (or config) determine which of these are available @@ -777,6 +820,15 @@ class PersonView(MasterView): config.add_view(cls, attr='make_user', route_name='{}.make_user'.format(route_prefix), permission='users.create') + # merge requests + if cls.mergeable: + config.add_tailbone_permission(permission_prefix, '{}.request_merge'.format(permission_prefix), + "Request merge for 2 {}".format(model_title_plural)) + config.add_route('{}.request_merge'.format(route_prefix), '{}/request-merge'.format(url_prefix), + request_method='POST') + config.add_view(cls, attr='request_merge', route_name='{}.request_merge'.format(route_prefix), + permission='{}.request_merge'.format(permission_prefix)) + # TODO: deprecate / remove this PeopleView = PersonView @@ -888,6 +940,84 @@ class NoteSchema(colander.Schema): note_text = colander.SchemaNode(colander.String(), missing='') +class MergePeopleRequestView(MasterView): + """ + Master view for the MergePeopleRequest class. + """ + model_class = model.MergePeopleRequest + route_prefix = 'people_merge_requests' + url_prefix = '/people/merge-requests' + creatable = False + editable = False + + labels = { + 'removing_uuid': "Removing", + 'keeping_uuid': "Keeping", + } + + grid_columns = [ + 'removing_uuid', + 'keeping_uuid', + 'requested', + 'requested_by', + 'merged', + 'merged_by', + ] + + form_fields = [ + 'removing_uuid', + 'keeping_uuid', + 'requested', + 'requested_by', + 'merged', + 'merged_by', + ] + + def configure_grid(self, g): + super(MergePeopleRequestView, self).configure_grid(g) + + g.set_renderer('removing_uuid', self.render_referenced_person_name) + g.set_renderer('keeping_uuid', self.render_referenced_person_name) + + g.filters['merged'].default_active = True + g.filters['merged'].default_verb = 'is_null' + + g.set_sort_defaults('requested', 'desc') + + g.set_link('removing_uuid') + g.set_link('keeping_uuid') + + def render_referenced_person_name(self, merge_request, field): + uuid = getattr(merge_request, field) + person = self.Session.query(self.model.Person).get(uuid) + if person: + return six.text_type(person) + return "(person not found)" + + def get_instance_title(self, merge_request): + model = self.model + removing = self.Session.query(model.Person).get(merge_request.removing_uuid) + keeping = self.Session.query(model.Person).get(merge_request.keeping_uuid) + return "{} -> {}".format( + removing or "(not found)", + keeping or "(not found)") + + def configure_form(self, f): + super(MergePeopleRequestView, self).configure_form(f) + + f.set_renderer('removing_uuid', self.render_referenced_person) + f.set_renderer('keeping_uuid', self.render_referenced_person) + + def render_referenced_person(self, merge_request, field): + uuid = getattr(merge_request, field) + person = self.Session.query(self.model.Person).get(uuid) + if person: + text = six.text_type(person) + url = self.request.route_url('people.view', uuid=person.uuid) + return tags.link_to(text, url) + return "(person not found)" + + def includeme(config): # autocomplete @@ -900,3 +1030,4 @@ def includeme(config): PersonView.defaults(config) PersonNoteView.defaults(config) + MergePeopleRequestView.defaults(config) From a881b310bc96610598a91e5ea8e0e187ca4e4ada Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 23 Aug 2021 14:25:08 -0500 Subject: [PATCH 0067/1385] Allow customization of row 'view' action url --- tailbone/views/master.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index c6f0253d..6d5a3d96 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -490,9 +490,8 @@ class MasterView(View): # view action if self.rows_viewable: - view = lambda r, i: self.get_row_action_url('view', r) icon = 'eye' if use_buefy else 'zoomin' - actions.append(self.make_action('view', icon=icon, url=view)) + actions.append(self.make_action('view', icon=icon, url=self.row_view_action_url)) # edit action if self.rows_editable and self.has_perm('edit_row'): @@ -1344,6 +1343,9 @@ class MasterView(View): """ return True + def row_view_action_url(self, row, i): + return self.get_row_action_url('view', row) + def row_edit_action_url(self, row, i): if self.row_editable(row): return self.get_row_action_url('edit', row) From 3cf4c0f8e40d89cae2ec2a998cf029630006bd63 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 23 Aug 2021 19:26:50 -0500 Subject: [PATCH 0068/1385] Require explicit opt-in for "clicking grid row checks box" feature sometimes it makes sense *not* to enable that, in which case disabled probably should be the default --- tailbone/grids/core.py | 2 ++ tailbone/templates/grids/buefy.mako | 2 ++ tailbone/views/master.py | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index 5c8e1c87..f6df3375 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -75,6 +75,7 @@ class Grid(object): sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc', pageable=False, default_pagesize=20, default_page=1, checkboxes=False, checked=None, check_handler=None, check_all_handler=None, + clicking_row_checks_box=False, main_actions=[], more_actions=[], delete_speedbump=False, ajax_data_url=None, component='tailbone-grid', **kwargs): @@ -128,6 +129,7 @@ class Grid(object): self.checked = lambda item: False self.check_handler = check_handler self.check_all_handler = check_all_handler + self.clicking_row_checks_box = clicking_row_checks_box self.main_actions = main_actions or [] self.more_actions = more_actions or [] diff --git a/tailbone/templates/grids/buefy.mako b/tailbone/templates/grids/buefy.mako index 00b9ce9e..6eaff11a 100644 --- a/tailbone/templates/grids/buefy.mako +++ b/tailbone/templates/grids/buefy.mako @@ -143,8 +143,10 @@ :checkable="checkable" % if grid.checkboxes: :checked-rows.sync="checkedRows" + % if grid.clicking_row_checks_box: @click="rowClick" % endif + % endif % if grid.check_handler: @check="${grid.check_handler}" % endif diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 6d5a3d96..a1d6da79 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -73,6 +73,10 @@ class MasterView(View): pageable = True checkboxes = False + # set to True to allow user to click "anywhere" in a row in order + # to toggle its checkbox + clicking_row_checks_box = False + # set to True in order to encode search values as utf-8 use_byte_string_filters = False @@ -399,6 +403,7 @@ class MasterView(View): 'url': lambda obj: self.get_action_url('view', obj), 'checkboxes': checkboxes, 'checked': self.checked, + 'clicking_row_checks_box': self.clicking_row_checks_box, 'assume_local_times': self.has_local_times, } if 'main_actions' not in kwargs and 'more_actions' not in kwargs: From c3079fe899a5c54b77042fe7a0b7742204dd0ab8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 24 Aug 2021 09:39:45 -0500 Subject: [PATCH 0069/1385] Add `before_render_index()` customization hook for MasterView --- tailbone/views/master.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index a1d6da79..f2634328 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -335,8 +335,17 @@ class MasterView(View): context['download_results_rows_fields_available'] = available context['download_results_rows_fields_default'] = self.download_results_rows_fields_default(available) + self.before_render_index() return self.render_to_response('index', context) + def before_render_index(self): + """ + Perform any needed logic just prior to rendering the index + response. Note that this logic is invoked only when rendering + the main index page, but *not* invoked when refreshing partial + grid contents etc. + """ + def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs): """ Creates a new grid instance From 445862d48d5c07306c314faa27159c1e59f5761c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 26 Aug 2021 11:55:09 -0500 Subject: [PATCH 0070/1385] Update changelog --- CHANGES.rst | 16 ++++++++++++++++ tailbone/_version.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7976b45d..069c6c94 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,22 @@ CHANGELOG ========= +0.8.139 (2021-08-26) +-------------------- + +* Tweak how email preview is sent, and attempt "to" is displayed. + +* Move "merge 2 people" logic into People Handler. + +* Expose "merge request tracking" feature for People data. + +* Allow customization of row 'view' action url. + +* Require explicit opt-in for "clicking grid row checks box" feature. + +* Add ``before_render_index()`` customization hook for MasterView. + + 0.8.138 (2021-08-04) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 10c12322..565c21e5 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.138' +__version__ = '0.8.139' From 897bb177bc649966283fda0c79dad1cd244cb32f Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 28 Aug 2021 14:24:56 -0500 Subject: [PATCH 0071/1385] Make it easier to override rendering grid component in master/index was needed so i could pass extra event handlers to it --- tailbone/templates/master/index.mako | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako index d1389a47..8e855422 100644 --- a/tailbone/templates/master/index.mako +++ b/tailbone/templates/master/index.mako @@ -466,17 +466,22 @@ % endif + ${self.render_grid_component()} + + % if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple': + ${h.form('#', ref='deleteObjectForm')} + ${h.csrf_token(request)} + ${h.end_form()} + % endif + + +<%def name="render_grid_component()"> <${grid.component} :csrftoken="csrftoken" % if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple': @deleteActionClicked="deleteObject" % endif > - % if master.deletable and request.has_perm('{}.delete'.format(permission_prefix)) and master.delete_confirm == 'simple': - ${h.form('#', ref='deleteObjectForm')} - ${h.csrf_token(request)} - ${h.end_form()} - % endif <%def name="make_this_page_component()"> From fe584f193fca3fdcfaeef35623e034ec998ac5ea Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 28 Aug 2021 18:45:31 -0500 Subject: [PATCH 0072/1385] Always show all grid actions...for now we don't have a great way to accommodate too many actions; ideally could hide some in a drawer, but for now we just show them all for simplicity... --- tailbone/templates/grids/buefy.mako | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tailbone/templates/grids/buefy.mako b/tailbone/templates/grids/buefy.mako index 6eaff11a..90e8121b 100644 --- a/tailbone/templates/grids/buefy.mako +++ b/tailbone/templates/grids/buefy.mako @@ -187,7 +187,9 @@ % if grid.main_actions or grid.more_actions: - % for action in grid.main_actions: + ## TODO: we do not currently differentiate for "main vs. more" + ## here, but ideally we would tuck "more" away in a drawer etc. + % for action in grid.main_actions + grid.more_actions: Date: Sun, 29 Aug 2021 10:28:36 -0500 Subject: [PATCH 0073/1385] Allow grid columns to be *invisible* (but still present in grid) this can be useful when you need contextual data for a given row, for sake of front-end UI features, but do not want to actually show the extra data column(s) --- tailbone/grids/core.py | 29 ++++++++++++++++++++++++++++- tailbone/templates/grids/buefy.mako | 5 ++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/tailbone/grids/core.py b/tailbone/grids/core.py index f6df3375..c476c426 100644 --- a/tailbone/grids/core.py +++ b/tailbone/grids/core.py @@ -69,7 +69,7 @@ class Grid(object): def __init__(self, key, data, columns=None, width='auto', request=None, model_class=None, model_title=None, model_title_plural=None, - enums={}, labels={}, assume_local_times=False, renderers={}, + enums={}, labels={}, assume_local_times=False, renderers={}, invisible=[], extra_row_class=None, linked_columns=[], url='#', joiners={}, filterable=False, filters={}, use_byte_string_filters=False, sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc', @@ -105,6 +105,7 @@ class Grid(object): self.labels = labels or {} self.assume_local_times = assume_local_times self.renderers = self.make_default_renderers(renderers or {}) + self.invisible = invisible or [] self.extra_row_class = extra_row_class self.linked_columns = linked_columns or [] self.url = url @@ -161,13 +162,38 @@ class Grid(object): return [prop.key for prop in mapper.iterate_properties] def hide_column(self, key): + """ + This *removes* a column from the grid, altogether. + + This method should really be renamed to ``remove_column()`` + instead. + """ if key in self.columns: self.columns.remove(key) def hide_columns(self, *keys): + """ + This *removes* columns from the grid, altogether. + + This method should really be renamed to ``remove_columns()`` + instead. + """ for key in keys: self.hide_column(key) + def set_invisible(self, key, invisible=True): + """ + Mark the given column as "invisible" (but do not remove it). + + Use :meth:`hide_column()` if you actually want to remove it. + """ + if invisible: + if key not in self.invisible: + self.invisible.append(key) + else: + if key in self.invisible: + self.invisible.remove(key) + def append(self, field): self.columns.append(field) @@ -1185,6 +1211,7 @@ class Grid(object): 'field': name, 'label': self.get_label(name), 'sortable': self.sortable and name in self.sorters, + 'visible': name not in self.invisible, }) return columns diff --git a/tailbone/templates/grids/buefy.mako b/tailbone/templates/grids/buefy.mako index 90e8121b..b55ff30e 100644 --- a/tailbone/templates/grids/buefy.mako +++ b/tailbone/templates/grids/buefy.mako @@ -176,7 +176,10 @@