Expose "merge request tracking" feature for People data

more to come i'm sure, but this covers the basics
This commit is contained in:
Lance Edgar 2021-08-19 18:00:51 -05:00
parent cf32d4235e
commit ac133ce830
4 changed files with 279 additions and 3 deletions

View file

@ -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:
<b-button @click="showMergeRequest()"
icon-pack="fas"
icon-left="object-ungroup"
:disabled="checkedRows.length != 2">
Request Merge
</b-button>
<b-modal has-modal-card
:active.sync="mergeRequestShowDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Request Merge of 2 People</p>
</header>
<section class="modal-card-body">
<b-table :data="mergeRequestRows"
striped hoverable>
<template slot-scope="props">
<b-table-column field="customer_number"
label="Customer #">
<span v-html="props.row.customer_number"></span>
</b-table-column>
<b-table-column field="first_name"
label="First Name">
<span v-html="props.row.first_name"></span>
</b-table-column>
<b-table-column field="last_name"
label="Last Name">
<span v-html="props.row.last_name"></span>
</b-table-column>
</template>
</b-table>
</section>
<footer class="modal-card-foot">
<b-button @click="mergeRequestShowDialog = false">
Cancel
</b-button>
${h.form(url('{}.request_merge'.format(route_prefix)), **{'@submit': 'submitMergeRequest'})}
${h.csrf_token(request)}
${h.hidden('removing_uuid', **{':value': 'mergeRequestRemovingUUID'})}
${h.hidden('keeping_uuid', **{':value': 'mergeRequestKeepingUUID'})}
<b-button type="is-primary"
native-type="submit"
:disabled="mergeRequestSubmitting">
{{ mergeRequestSubmitText }}
</b-button>
${h.end_form()}
</footer>
</div>
</b-modal>
% endif
% endif
${parent.grid_tools()}
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
% if master.mergeable and master.has_perm('request_merge'):
${grid.component_studly}Data.mergeRequestShowDialog = false
${grid.component_studly}Data.mergeRequestRows = []
${grid.component_studly}Data.mergeRequestSubmitText = "Submit Merge Request"
${grid.component_studly}Data.mergeRequestSubmitting = false
${grid.component_studly}.computed.mergeRequestRemovingUUID = function() {
if (this.mergeRequestRows.length) {
return this.mergeRequestRows[0].uuid
}
return null
}
${grid.component_studly}.computed.mergeRequestKeepingUUID = function() {
if (this.mergeRequestRows.length) {
return this.mergeRequestRows[1].uuid
}
return null
}
${grid.component_studly}.methods.showMergeRequest = function() {
this.mergeRequestRows = this.checkedRows
this.mergeRequestShowDialog = true
}
${grid.component_studly}.methods.submitMergeRequest = function() {
this.mergeRequestSubmitting = true
this.mergeRequestSubmitText = "Working, please wait..."
}
% endif
</script>
</%def>
${parent.body()}

View file

@ -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]))}
<b-button type="is-primary"
native-type="submit"
:disabled="mergeFormSubmitting"
icon-pack="fas"
icon-left="object-ungroup">
{{ mergeFormButtonText }}
</b-button>
${h.end_form()}
% endif
% endif
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
% if not instance.merged and request.has_perm('people.merge'):
<script type="text/javascript">
ThisPageData.mergeFormButtonText = "Perform Merge"
ThisPageData.mergeFormSubmitting = false
ThisPage.methods.submitMergeForm = function() {
this.mergeFormButtonText = "Working, please wait..."
this.mergeFormSubmitting = true
}
</script>
% endif
</%def>
${parent.body()}

View file

@ -378,7 +378,7 @@ class MasterView(View):
Return a dictionary of kwargs to be passed to the factory when creating Return a dictionary of kwargs to be passed to the factory when creating
new grid instances. new grid instances.
""" """
checkboxes = self.checkboxes checkboxes = kwargs.get('checkboxes', self.checkboxes)
if not checkboxes and self.mergeable and self.has_perm('merge'): if not checkboxes and self.mergeable and self.has_perm('merge'):
checkboxes = True checkboxes = True
if not checkboxes and self.supports_set_enabled_toggle and self.has_perm('enable_disable_set'): if not checkboxes and self.supports_set_enabled_toggle and self.has_perm('enable_disable_set'):

View file

@ -33,7 +33,7 @@ import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from rattail.db import model, api from rattail.db import model, api
from rattail.time import localtime from rattail.time import localtime, make_utc
from rattail.util import OrderedDict from rattail.util import OrderedDict
import colander import colander
@ -68,6 +68,7 @@ class PersonView(MasterView):
'last_name', 'last_name',
'phone', 'phone',
'email', 'email',
'merge_requested',
] ]
form_fields = [ form_fields = [
@ -93,6 +94,15 @@ class PersonView(MasterView):
app = self.get_rattail_app() app = self.get_rattail_app()
self.handler = app.get_people_handler() 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): def configure_grid(self, g):
super(PersonView, self).configure_grid(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['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.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_sort_defaults('display_name')
g.set_label('display_name', "Full Name") g.set_label('display_name', "Full Name")
@ -134,6 +147,23 @@ class PersonView(MasterView):
g.set_link('first_name') g.set_link('first_name')
g.set_link('last_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): def get_instance(self):
# TODO: I don't recall why this fallback check for a vendor contact # TODO: I don't recall why this fallback check for a vendor contact
# exists here, but leaving it intact for now. # exists here, but leaving it intact for now.
@ -383,7 +413,7 @@ class PersonView(MasterView):
raise Exception(reason) raise Exception(reason)
def merge_objects(self, removing, keeping): 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): def view_profile(self):
""" """
@ -696,6 +726,18 @@ class PersonView(MasterView):
self.request.session.flash("User has been created: {}".format(user.username)) self.request.session.flash("User has been created: {}".format(user.username))
return self.redirect(self.request.route_url('users.view', uuid=user.uuid)) 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 @classmethod
def defaults(cls, config): def defaults(cls, config):
cls._people_defaults(config) cls._people_defaults(config)
@ -709,6 +751,7 @@ class PersonView(MasterView):
instance_url_prefix = cls.get_instance_url_prefix() instance_url_prefix = cls.get_instance_url_prefix()
model_key = cls.get_model_key() model_key = cls.get_model_key()
model_title = cls.get_model_title() model_title = cls.get_model_title()
model_title_plural = cls.get_model_title_plural()
# "profile" perms # "profile" perms
# TODO: should let view class (or config) determine which of these are available # 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), config.add_view(cls, attr='make_user', route_name='{}.make_user'.format(route_prefix),
permission='users.create') 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 # TODO: deprecate / remove this
PeopleView = PersonView PeopleView = PersonView
@ -888,6 +940,84 @@ class NoteSchema(colander.Schema):
note_text = colander.SchemaNode(colander.String(), missing='') 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): def includeme(config):
# autocomplete # autocomplete
@ -900,3 +1030,4 @@ def includeme(config):
PersonView.defaults(config) PersonView.defaults(config)
PersonNoteView.defaults(config) PersonNoteView.defaults(config)
MergePeopleRequestView.defaults(config)