From 90af8f91b88c6f8e82bfafaf452e923d2ae43224 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 2 Aug 2021 18:26:15 -0500 Subject: [PATCH 0001/1323] Let feedback forms define their own email key so multiple recipient options may be presented to user, e.g. in public frontend --- tailbone/api/common.py | 3 ++- tailbone/forms/common.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tailbone/api/common.py b/tailbone/api/common.py index c2823ff9..81458c01 100644 --- a/tailbone/api/common.py +++ b/tailbone/api/common.py @@ -104,7 +104,8 @@ class CommonView(APIView): data['user_url'] = '#' # TODO: could get from config? data['client_ip'] = self.request.client_addr - send_email(self.rattail_config, self.feedback_email_key, data=data) + email_key = data['email_key'] or self.feedback_email_key + send_email(self.rattail_config, email_key, data=data) return {'ok': True} return {'error': "Form did not validate!"} diff --git a/tailbone/forms/common.py b/tailbone/forms/common.py index 9cc145dd..26934479 100644 --- a/tailbone/forms/common.py +++ b/tailbone/forms/common.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2019 Lance Edgar +# Copyright © 2010-2021 Lance Edgar # # This file is part of Rattail. # @@ -46,6 +46,9 @@ class Feedback(colander.Schema): """ Form schema for user feedback. """ + email_key = colander.SchemaNode(colander.String(), + missing=colander.null) + referrer = colander.SchemaNode(colander.String()) user = colander.SchemaNode(colander.String(), From a10de791a1c04f9abad52db0367f2e51193c8fe8 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 4 Aug 2021 13:01:09 -0500 Subject: [PATCH 0002/1323] 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 27be20df..7976b45d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ CHANGELOG ========= +0.8.138 (2021-08-04) +-------------------- + +* Let feedback forms define their own email key. + + 0.8.137 (2021-07-15) -------------------- diff --git a/tailbone/_version.py b/tailbone/_version.py index 0fe1d51b..10c12322 100644 --- a/tailbone/_version.py +++ b/tailbone/_version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8; -*- -__version__ = '0.8.137' +__version__ = '0.8.138' From 5836099746eec80be94eead28435b36f99e09a9c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 16 Aug 2021 19:29:48 -0500 Subject: [PATCH 0003/1323] Tweak how email preview is sent, and attempt "to" is displayed latter only have been changed for the grid view. preview now is sent "properly" via the configured mail handler, which also means that an attempt may be recorded (whereas previously it would not be) --- tailbone/views/email.py | 52 +++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/tailbone/views/email.py b/tailbone/views/email.py index 2201f8f3..58a0320b 100644 --- a/tailbone/views/email.py +++ b/tailbone/views/email.py @@ -26,6 +26,8 @@ Email Views from __future__ import unicode_literals, absolute_import +import re + import six from rattail import mail @@ -80,8 +82,8 @@ class EmailSettingView(MasterView): self.handler = self.get_handler() def get_handler(self): - # TODO: should let config override which handler we use - return mail.EmailHandler(self.rattail_config) + app = self.get_rattail_app() + return app.get_mail_handler() def get_data(self, session=None): data = [] @@ -277,8 +279,8 @@ class EmailPreview(View): self.handler = self.get_handler() def get_handler(self): - # TODO: should let config override which handler we use - return mail.EmailHandler(self.rattail_config) + app = self.get_rattail_app() + return app.get_mail_handler() def __call__(self): @@ -303,22 +305,15 @@ class EmailPreview(View): if key: email = self.handler.get_email(key) data = email.obtain_sample_data(self.request) - msg = email.make_message(data) - subject = msg['Subject'] - del msg['Subject'] - msg['Subject'] = "[preview] {0}".format(subject) + self.handler.send_message(email, data, + subject_prefix="[PREVIEW] ", + to=[recipient], + cc=None, bcc=None) - del msg['To'] - del msg['Cc'] - del msg['Bcc'] - msg['To'] = recipient - - # TODO: should refactor this to use email handler - sent = mail.deliver_message(self.rattail_config, key, msg) - - self.request.session.flash("Preview for '{}' was {}emailed to {}".format( - key, '' if sent else '(NOT) ', recipient)) + self.request.session.flash( + "Preview for '{}' was emailed to {}".format( + key, recipient)) def preview_template(self, key, type_): email = self.handler.get_email(key) @@ -385,12 +380,33 @@ class EmailAttemptView(MasterView): # status_code g.set_enum('status_code', self.enum.EMAIL_ATTEMPT) + # to + g.set_renderer('to', self.render_to_short) + # links g.set_link('key') g.set_link('sender') g.set_link('subject') g.set_link('to') + to_pattern = re.compile(r'^\{(.*)\}$') + + def render_to_short(self, attempt, column): + value = attempt.to + if not value: + return + + match = self.to_pattern.match(value) + if match: + recips = parse_list(match.group(1)) + if len(recips) > 2: + recips = recips[:2] + recips.append('...') + recips = [HTML.escape(r) for r in recips] + return ', '.join(recips) + + return value + def configure_form(self, f): super(EmailAttemptView, self).configure_form(f) From cf32d4235e7ee233ec79d6f07b79f5fd12f27815 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 18 Aug 2021 19:16:59 -0500 Subject: [PATCH 0004/1323] Move "merge 2 people" logic into People Handler view now delegates to handler, which lives in the rattail package --- tailbone/views/people.py | 66 ++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/tailbone/views/people.py b/tailbone/views/people.py index 6e72fe1f..b8e06ced 100644 --- a/tailbone/views/people.py +++ b/tailbone/views/people.py @@ -85,16 +85,13 @@ class PersonView(MasterView): ] mergeable = True - merge_additive_fields = [ - 'usernames', - 'member_uuids', - ] - merge_fields = merge_additive_fields + [ - 'uuid', - 'first_name', - 'last_name', - 'display_name', - ] + + def __init__(self, request): + super(PersonView, self).__init__(request) + + # always get a reference to the People Handler + app = self.get_rattail_app() + self.handler = app.get_people_handler() def configure_grid(self, g): super(PersonView, self).configure_grid(g) @@ -190,9 +187,7 @@ class PersonView(MasterView): names[key] = None # do explicit name update w/ common handler logic - app = self.get_rattail_app() - handler = app.get_people_handler() - handler.update_names(person, **names) + self.handler.update_names(person, **names) return person @@ -365,33 +360,30 @@ class PersonView(MasterView): (model.VendorContact, 'person_uuid'), ] + def get_merge_fields(self): + fields = self.handler.get_merge_preview_fields() + return [field['name'] for field in fields] + + def get_merge_additive_fields(self): + fields = self.handler.get_merge_preview_fields() + return [field['name'] for field in fields + if field.get('additive')] + + def get_merge_coalesce_fields(self): + fields = self.handler.get_merge_preview_fields() + return [field['name'] for field in fields + if field.get('coalesce')] + def get_merge_data(self, person): - return { - 'uuid': person.uuid, - 'first_name': person.first_name, - 'last_name': person.last_name, - 'display_name': person.display_name, - 'usernames': [u.username for u in person.users], - 'member_uuids': [m.uuid for m in person.members], - } + return self.handler.get_merge_preview_data(person) + + def validate_merge(self, removing, keeping): + reason = self.handler.why_not_merge(removing, keeping) + if reason: + raise Exception(reason) def merge_objects(self, removing, keeping): - """ - Execute a merge operation on the two given person records. - """ - # move Member records to final Person - for member in list(removing.members): - removing.members.remove(member) - keeping.members.append(member) - - # move User records to final Person - for user in list(removing.users): - removing.users.remove(user) - keeping.users.append(user) - - # delete unwanted Person - self.Session.delete(removing) - self.Session.flush() + self.handler.perform_merge(removing, keeping) def view_profile(self): """ From ac133ce8304585c33a2af1876c8fda33fd089626 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 19 Aug 2021 18:00:51 -0500 Subject: [PATCH 0005/1323] Expose "merge request tracking" feature for People data more to come i'm sure, but this covers the basics --- tailbone/templates/people/index.mako | 105 ++++++++++++++ .../templates/people/merge-requests/view.mako | 40 ++++++ tailbone/views/master.py | 2 +- tailbone/views/people.py | 135 +++++++++++++++++- 4 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 tailbone/templates/people/index.mako create mode 100644 tailbone/templates/people/merge-requests/view.mako diff --git a/tailbone/templates/people/index.mako b/tailbone/templates/people/index.mako new file mode 100644 index 00000000..377063b8 --- /dev/null +++ b/tailbone/templates/people/index.mako @@ -0,0 +1,105 @@ +## -*- coding: utf-8; -*- +<%inherit file="/master/index.mako" /> + +<%def name="grid_tools()"> + + % if master.mergeable and master.has_perm('request_merge'): + % if use_buefy: + + Request Merge + + + + + % endif + % endif + + ${parent.grid_tools()} + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + +${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 0006/1323] 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 0007/1323] 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 0008/1323] 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 0009/1323] 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 0010/1323] 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 0011/1323] 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 0012/1323] 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 @@