tailbone/tailbone/views/people.py

1059 lines
39 KiB
Python
Raw Normal View History

# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2021 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
2013-09-01 09:27:47 -05:00
Person Views
"""
from __future__ import unicode_literals, absolute_import
import datetime
2017-08-04 11:55:53 -05:00
import six
import sqlalchemy as sa
from sqlalchemy import orm
from rattail.db import model, api
from rattail.time import localtime
from rattail.util import OrderedDict
import colander
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from webhelpers2.html import HTML, tags
from tailbone import forms, grids
from tailbone.views import MasterView, AutocompleteView
class PersonView(MasterView):
"""
Master view for the Person class.
"""
model_class = model.Person
model_title_plural = "People"
route_prefix = 'people'
2019-08-02 11:30:46 -05:00
touchable = True
has_versions = True
bulk_deletable = True
is_contact = True
manage_notes_from_profile_view = False
labels = {
'default_phone': "Phone Number",
'default_email': "Email Address",
}
grid_columns = [
'display_name',
'first_name',
'last_name',
'phone',
'email',
'merge_requested',
]
2017-12-04 18:49:52 -06:00
form_fields = [
'first_name',
'middle_name',
'last_name',
'display_name',
'default_phone',
'default_email',
2017-12-04 18:49:52 -06:00
'address',
'employee',
'customers',
'members',
2017-12-04 18:49:52 -06:00
'users',
]
mergeable = True
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 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)
g.joiners['email'] = lambda q: q.outerjoin(model.PersonEmailAddress, sa.and_(
model.PersonEmailAddress.parent_uuid == model.Person.uuid,
model.PersonEmailAddress.preference == 1))
g.joiners['phone'] = lambda q: q.outerjoin(model.PersonPhoneNumber, sa.and_(
model.PersonPhoneNumber.parent_uuid == model.Person.uuid,
model.PersonPhoneNumber.preference == 1))
g.filters['email'] = g.make_filter('email', model.PersonEmailAddress.address)
g.set_filter('phone', model.PersonPhoneNumber.number,
factory=grids.filters.AlchemyPhoneNumberFilter)
g.joiners['customer_id'] = lambda q: q.outerjoin(model.CustomerPerson).outerjoin(model.Customer)
g.filters['customer_id'] = g.make_filter('customer_id', model.Customer.id)
g.filters['first_name'].default_active = True
g.filters['first_name'].default_verb = 'contains'
g.filters['last_name'].default_active = True
g.filters['last_name'].default_verb = 'contains'
g.set_joiner('employee_status', lambda q: q.outerjoin(model.Employee))
g.set_filter('employee_status', model.Employee.status,
value_enum=self.enum.EMPLOYEE_STATUS)
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")
g.set_label('phone', "Phone Number")
g.set_label('email', "Email Address")
g.set_label('customer_id', "Customer ID")
2017-08-02 20:40:02 -05:00
g.set_link('display_name')
g.set_link('first_name')
g.set_link('last_name')
2017-08-02 20:40:02 -05:00
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.
key = self.request.matchdict['uuid']
instance = self.Session.query(model.Person).get(key)
if instance:
return instance
instance = self.Session.query(model.VendorContact).get(key)
if instance:
return instance.person
raise HTTPNotFound
def is_person_protected(self, person):
for user in person.users:
if self.user_is_protected(user):
return True
return False
def editable_instance(self, person):
if self.request.is_root:
return True
return not self.is_person_protected(person)
def deletable_instance(self, person):
if self.request.is_root:
return True
return not self.is_person_protected(person)
def objectify(self, form, data=None):
if data is None:
data = form.validated
# do normal create/update
person = super(PersonView, self).objectify(form, data)
# collect data from all name fields
names = {}
if 'first_name' in form:
names['first'] = data['first_name']
if 'middle_name' in form:
names['middle'] = data['middle_name']
if 'last_name' in form:
names['last'] = data['last_name']
2021-02-04 16:44:40 -06:00
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?
# ugh, for now we must convert them
for key in names:
if names[key] is colander.null:
names[key] = None
# do explicit name update w/ common handler logic
self.handler.update_names(person, **names)
return person
def delete_instance(self, person):
"""
Supplements the default logic as follows:
Any customer associations are first deleted for the person. Once that
is complete, deletion continues as per usual.
"""
session = orm.object_session(person)
# must explicitly remove all CustomerPerson records
for cp in list(person._customers):
customer = cp.customer
session.delete(cp)
# session.flush()
customer._people.reorder()
# continue with normal logic
super(PersonView, self).delete_instance(person)
def touch_instance(self, person):
"""
Supplements the default logic as follows:
In addition to "touching" the person proper, we also "touch" each
contact info record associated with them.
"""
# touch person, as per usual
super(PersonView, self).touch_instance(person)
def touch(obj):
change = model.Change()
change.class_name = obj.__class__.__name__
change.instance_uuid = obj.uuid
change.deleted = False
self.Session.add(change)
# phone numbers
for phone in person.phones:
touch(phone)
# email addresses
for email in person.emails:
touch(email)
# mailing addresses
for address in person.addresses:
touch(address)
def configure_common_form(self, f):
super(PersonView, self).configure_common_form(f)
person = f.model_instance
2017-12-04 18:49:52 -06:00
f.set_label('display_name', "Full Name")
# TODO: should remove this?
2017-12-04 18:49:52 -06:00
f.set_readonly('phone')
f.set_label('phone', "Phone Number")
f.set_renderer('default_phone', self.render_default_phone)
if not self.creating and person.phones:
f.set_default('default_phone', person.phones[0].number)
# TODO: should remove this?
2017-12-04 18:49:52 -06:00
f.set_readonly('email')
f.set_label('email', "Email Address")
f.set_renderer('default_email', self.render_default_email)
if not self.creating and person.emails:
f.set_default('default_email', person.emails[0].address)
2017-12-04 18:49:52 -06:00
f.set_readonly('address')
f.set_label('address', "Mailing Address")
# employee
if self.creating:
f.remove_field('employee')
else:
f.set_readonly('employee')
f.set_renderer('employee', self.render_employee)
# customers
if self.creating:
f.remove_field('customers')
else:
f.set_readonly('customers')
f.set_renderer('customers', self.render_customers)
# members
if self.creating:
f.remove_field('members')
else:
f.set_readonly('members')
f.set_renderer('members', self.render_members)
# users
if self.creating:
f.remove_field('users')
else:
f.set_readonly('users')
f.set_renderer('users', self.render_users)
2017-12-04 18:49:52 -06:00
def render_employee(self, person, field):
employee = person.employee
if not employee:
return ""
text = six.text_type(employee)
url = self.request.route_url('employees.view', uuid=employee.uuid)
return tags.link_to(text, url)
def render_customers(self, person, field):
customers = person._customers
if not customers:
return ""
items = []
for customer in customers:
customer = customer.customer
text = six.text_type(customer)
if customer.number:
text = "(#{}) {}".format(customer.number, text)
elif customer.id:
2017-12-04 18:49:52 -06:00
text = "({}) {}".format(customer.id, text)
url = self.request.route_url('customers.view', uuid=customer.uuid)
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
2017-12-04 18:49:52 -06:00
return HTML.tag('ul', c=items)
def render_members(self, person, field):
members = person.members
if not members:
return ""
items = []
for member in members:
text = six.text_type(member)
if member.number:
text = "(#{}) {}".format(member.number, text)
elif member.id:
text = "({}) {}".format(member.id, text)
url = self.request.route_url('members.view', uuid=member.uuid)
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
return HTML.tag('ul', c=items)
2017-12-04 18:49:52 -06:00
def render_users(self, person, field):
use_buefy = self.get_use_buefy()
2017-12-04 18:49:52 -06:00
users = person.users
items = []
for user in users:
text = user.username
url = self.request.route_url('users.view', uuid=user.uuid)
2018-02-12 19:22:05 -06:00
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
2017-12-04 18:49:52 -06:00
if items:
return HTML.tag('ul', c=items)
elif self.viewing and self.request.has_perm('users.create'):
if use_buefy:
return HTML.tag('b-button', type='is-primary', c="Make User",
**{'@click': 'clickMakeUser()'})
else:
return HTML.tag('button', type='button', id='make-user', c="Make User")
2017-12-04 18:49:52 -06:00
else:
return ""
def get_version_child_classes(self):
return [
(model.PersonPhoneNumber, 'parent_uuid'),
(model.PersonEmailAddress, 'parent_uuid'),
(model.PersonMailingAddress, 'parent_uuid'),
(model.Employee, 'person_uuid'),
(model.CustomerPerson, 'person_uuid'),
(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 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):
self.handler.perform_merge(removing, keeping, user=self.request.user)
def view_profile(self):
"""
View which exposes the "full profile" for a given person, i.e. all
related customer, employee, user info etc.
"""
self.viewing = True
person = self.get_instance()
employee = person.employee
context = {
'person': person,
'instance': person,
'instance_title': self.get_instance_title(person),
'today': localtime(self.rattail_config).date(),
'person_data': self.get_context_person(person),
'customers_data': self.get_context_customers(person),
'members_data': self.get_context_members(person),
'employee': employee,
'employee_view_url': self.request.route_url('employees.view', uuid=employee.uuid) if employee else None,
'employee_history': employee.get_current_history() if employee else None,
'employee_history_data': self.get_context_employee_history(employee),
}
use_buefy = self.get_use_buefy()
template = 'view_profile_buefy' if use_buefy else 'view_profile'
return self.render_to_response(template, context)
def get_context_person(self, person):
return {
'uuid': person.uuid,
'first_name': person.first_name,
'last_name': person.last_name,
'display_name': person.display_name,
'view_url': self.get_action_url('view', person),
'view_profile_url': self.get_action_url('view_profile', person),
}
def get_context_address(self, address):
return {
'uuid': address.uuid,
'street': address.street,
'street2': address.street2,
'city': address.city,
'state': address.state,
'zipcode': address.zipcode,
'display': six.text_type(address),
}
def get_context_customers(self, person):
data = []
for cp in person._customers:
customer = cp.customer
data.append({
'uuid': customer.uuid,
'ordinal': cp.ordinal,
'id': customer.id,
'number': customer.number,
'name': customer.name,
'view_url': self.request.route_url('customers.view',
uuid=customer.uuid),
'people': [self.get_context_person(p)
for p in customer.people],
'addresses': [self.get_context_address(a)
for a in customer.addresses],
})
return data
def get_context_members(self, person):
data = OrderedDict()
for member in person.members:
data[member.uuid] = self.get_context_member(member)
for customer in person.customers:
for member in customer.members:
if member.uuid not in data:
data[member.uuid] = self.get_context_member(member)
return list(data.values())
def get_context_member(self, member):
profile_url = None
if member.person:
profile_url = self.request.route_url('people.view_profile',
uuid=member.person_uuid)
return {
'uuid': member.uuid,
2020-03-18 12:29:18 -05:00
'number': member.number,
'id': member.id,
'active': member.active,
'joined': six.text_type(member.joined) if member.joined else None,
'withdrew': six.text_type(member.withdrew) if member.withdrew else None,
'customer_uuid': member.customer_uuid,
'customer_name': member.customer.name if member.customer else None,
'person_uuid': member.person_uuid,
'display': six.text_type(member),
'person_display_name': member.person.display_name if member.person else None,
'view_url': self.request.route_url('members.view', uuid=member.uuid),
'view_profile_url': profile_url,
}
def get_context_employee(self, employee):
"""
Return a dict of context data for the given employee.
"""
app = self.get_rattail_app()
handler = app.get_employment_handler()
context = handler.get_context_employee(employee)
return context
def get_context_employee_history(self, employee):
data = []
if employee:
for history in employee.sorted_history(reverse=True):
data.append({
'uuid': history.uuid,
'start_date': six.text_type(history.start_date),
'end_date': six.text_type(history.end_date or ''),
})
return data
def ensure_customer(self, person):
"""
Return the `Customer` record for the given person, establishing it
first if necessary.
"""
app = self.get_rattail_app()
handler = app.get_clientele_handler()
customer = handler.ensure_customer(person)
return customer
def profile_start_employee(self):
"""
View which will cause the person to start being an employee.
"""
person = self.get_instance()
app = self.get_rattail_app()
handler = app.get_employment_handler()
reason = handler.why_not_begin_employment(person)
if reason:
return {'error': reason}
data = self.request.json_body
start_date = datetime.datetime.strptime(data['start_date'], '%Y-%m-%d').date()
employee = handler.begin_employment(person, start_date,
employee_id=data['id'])
return self.profile_start_employee_result(employee, start_date)
def profile_start_employee_result(self, employee, start_date):
return {
'success': True,
'employee': self.get_context_employee(employee),
'employee_view_url': self.request.route_url('employees.view', uuid=employee.uuid),
'start_date': six.text_type(start_date),
'employee_history_data': self.get_context_employee_history(employee),
}
def profile_end_employee(self):
"""
View which will cause the person to stop being an employee.
"""
person = self.get_instance()
app = self.get_rattail_app()
handler = app.get_employment_handler()
reason = handler.why_not_end_employment(person)
if reason:
return {'error': reason}
data = dict(self.request.json_body)
end_date = datetime.datetime.strptime(data['end_date'], '%Y-%m-%d').date()
employee = handler.get_employee(person)
handler.end_employment(employee, end_date,
revoke_access=data.get('revoke_access'))
return self.profile_end_employee_result(employee, end_date)
def profile_end_employee_result(self, employee, end_date):
return {
'success': True,
'employee': self.get_context_employee(employee),
'employee_view_url': self.request.route_url('employees.view', uuid=employee.uuid),
'end_date': six.text_type(end_date),
'employee_history_data': self.get_context_employee_history(employee),
}
def profile_edit_employee_history(self):
"""
AJAX view for updating an employee history record.
"""
person = self.get_instance()
employee = person.employee
uuid = self.request.json_body['uuid']
history = self.Session.query(model.EmployeeHistory).get(uuid)
if not history or history not in employee.history:
return {'error': "Must specify a valid Employee History record for this Person."}
# all history records have a start date, so always update that
start_date = self.request.json_body['start_date']
start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d').date()
history.start_date = start_date
# only update end_date if history already had one
if history.end_date:
end_date = self.request.json_body['end_date']
end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d').date()
history.end_date = end_date
self.Session.flush()
current_history = employee.get_current_history()
return {
'success': True,
'start_date': six.text_type(current_history.start_date),
'end_date': six.text_type(current_history.end_date or ''),
'employee_history_data': self.get_context_employee_history(employee),
}
def profile_update_employee_id(self):
"""
View to update an employee's ID value.
"""
app = self.get_rattail_app()
employment = app.get_employment_handler()
person = self.get_instance()
employee = employment.get_employee(person)
data = self.request.json_body
employee.id = data['employee_id']
self.Session.flush()
return {
'success': True,
'employee': self.get_context_employee(employee),
}
def make_note_form(self, mode, person):
schema = NoteSchema().bind(session=self.Session(),
person_uuid=person.uuid)
if mode == 'create':
del schema['uuid']
form = forms.Form(schema=schema, request=self.request)
return form
def profile_add_note(self):
person = self.get_instance()
form = self.make_note_form('create', person)
if form.validate(newstyle=True):
note = self.create_note(person, form)
self.Session.flush()
return self.profile_add_note_success(note)
else:
return self.profile_add_note_failure(person, form)
def create_note(self, person, form):
note = model.PersonNote()
note.type = form.validated['note_type']
note.subject = form.validated['note_subject']
note.text = form.validated['note_text']
note.created_by = self.request.user
person.notes.append(note)
return note
def profile_add_note_success(self, note):
return self.redirect(self.get_action_url('view_profile', person))
def profile_add_note_failure(self, person, form):
return self.redirect(self.get_action_url('view_profile', person))
def profile_edit_note(self):
person = self.get_instance()
form = self.make_note_form('edit', person)
if form.validate(newstyle=True):
note = self.update_note(person, form)
self.Session.flush()
return self.profile_edit_note_success(note)
else:
return self.profile_edit_note_failure(person, form)
def update_note(self, person, form):
note = self.Session.query(model.PersonNote).get(form.validated['uuid'])
note.subject = form.validated['note_subject']
note.text = form.validated['note_text']
return note
def profile_edit_note_success(self, note):
return self.redirect(self.get_action_url('view_profile', person))
def profile_edit_note_failure(self, person, form):
return self.redirect(self.get_action_url('view_profile', person))
def profile_delete_note(self):
person = self.get_instance()
form = self.make_note_form('delete', person)
if form.validate(newstyle=True):
self.delete_note(person, form)
self.Session.flush()
return self.profile_delete_note_success(person)
else:
return self.profile_delete_note_failure(person, form)
def delete_note(self, person, form):
note = self.Session.query(model.PersonNote).get(form.validated['uuid'])
self.Session.delete(note)
def profile_delete_note_success(self, person):
return self.redirect(self.get_action_url('view_profile', person))
def profile_delete_note_failure(self, person, form):
return self.redirect(self.get_action_url('view_profile', person))
def make_user(self):
uuid = self.request.POST['person_uuid']
person = self.Session.query(model.Person).get(uuid)
if not person:
return self.notfound()
if person.users:
raise RuntimeError("person {} already has {} user accounts: ".format(
person.uuid, len(person.users), person))
user = model.User()
user.username = api.make_username(person)
user.person = person
user.active = False
self.Session.add(user)
self.Session.flush()
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.
"""
self.handler.request_merge(self.request.user,
self.request.POST['removing_uuid'],
self.request.POST['keeping_uuid'])
return self.redirect(self.get_index_url())
@classmethod
def defaults(cls, config):
cls._people_defaults(config)
cls._defaults(config)
@classmethod
def _people_defaults(cls, config):
permission_prefix = cls.get_permission_prefix()
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
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
config.add_tailbone_permission_group('people_profile', "People Profile View")
config.add_tailbone_permission('people_profile', 'people_profile.toggle_employee',
"Toggle the person's Employee status")
config.add_tailbone_permission('people_profile', 'people_profile.edit_employee_history',
"Edit the person's Employee History records")
# view profile
config.add_tailbone_permission(permission_prefix, '{}.view_profile'.format(permission_prefix),
"View full \"profile\" for {}".format(model_title))
config.add_route('{}.view_profile'.format(route_prefix), '{}/{{{}}}/profile'.format(url_prefix, model_key),
request_method='GET')
config.add_view(cls, attr='view_profile', route_name='{}.view_profile'.format(route_prefix),
permission='{}.view_profile'.format(permission_prefix))
# profile - start employee
config.add_route('{}.profile_start_employee'.format(route_prefix), '{}/profile/start-employee'.format(instance_url_prefix),
request_method='POST')
config.add_view(cls, attr='profile_start_employee', route_name='{}.profile_start_employee'.format(route_prefix),
permission='people_profile.toggle_employee', renderer='json')
# profile - end employee
config.add_route('{}.profile_end_employee'.format(route_prefix), '{}/profile/end-employee'.format(instance_url_prefix),
request_method='POST')
config.add_view(cls, attr='profile_end_employee', route_name='{}.profile_end_employee'.format(route_prefix),
permission='people_profile.toggle_employee', renderer='json')
# profile - edit employee history
config.add_route('{}.profile_edit_employee_history'.format(route_prefix), '{}/profile/edit-employee-history'.format(instance_url_prefix),
request_method='POST')
config.add_view(cls, attr='profile_edit_employee_history', route_name='{}.profile_edit_employee_history'.format(route_prefix),
permission='people_profile.edit_employee_history', renderer='json')
# profile - update employee ID
config.add_route('{}.profile_update_employee_id'.format(route_prefix),
'{}/profile/update-employee-id'.format(instance_url_prefix),
request_method='POST')
config.add_view(cls, attr='profile_update_employee_id',
route_name='{}.profile_update_employee_id'.format(route_prefix),
renderer='json',
permission='employees.edit')
# manage notes from profile view
if cls.manage_notes_from_profile_view:
# add note
config.add_tailbone_permission('people_profile', 'people_profile.add_note',
"Add new {} Note records".format(model_title))
config.add_route('{}.profile_add_note'.format(route_prefix), '{}/{{{}}}/profile/new-note'.format(url_prefix, model_key),
request_method='POST')
config.add_view(cls, attr='profile_add_note', route_name='{}.profile_add_note'.format(route_prefix),
permission='people_profile.add_note')
# edit note
config.add_tailbone_permission('people_profile', 'people_profile.edit_note',
"Edit {} Note records".format(model_title))
config.add_route('{}.profile_edit_note'.format(route_prefix), '{}/{{{}}}/profile/edit-note'.format(url_prefix, model_key),
request_method='POST')
config.add_view(cls, attr='profile_edit_note', route_name='{}.profile_edit_note'.format(route_prefix),
permission='people_profile.edit_note')
# delete note
config.add_tailbone_permission('people_profile', 'people_profile.delete_note',
"Delete {} Note records".format(model_title))
config.add_route('{}.profile_delete_note'.format(route_prefix), '{}/{{{}}}/profile/delete-note'.format(url_prefix, model_key),
request_method='POST')
config.add_view(cls, attr='profile_delete_note', route_name='{}.profile_delete_note'.format(route_prefix),
permission='people_profile.delete_note')
# make user for person
config.add_route('{}.make_user'.format(route_prefix), '{}/make-user'.format(url_prefix),
request_method='POST')
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
class PeopleAutocomplete(AutocompleteView):
mapped_class = model.Person
fieldname = 'display_name'
class PeopleEmployeesAutocomplete(PeopleAutocomplete):
"""
Autocomplete view for the Person model, but restricted to return only
results for people who are employees.
"""
def filter_query(self, q):
return q.join(model.Employee)
class PersonNoteView(MasterView):
"""
Master view for the PersonNote class.
"""
model_class = model.PersonNote
route_prefix = 'person_notes'
url_prefix = '/people/notes'
has_versions = True
grid_columns = [
'person',
'type',
'subject',
'created',
'created_by',
]
form_fields = [
'person',
'type',
'subject',
'text',
'created',
'created_by',
]
def get_instance_title(self, note):
return note.subject or "(no subject)"
def configure_grid(self, g):
super(PersonNoteView, self).configure_grid(g)
# person
g.set_joiner('person', lambda q: q.join(model.Person,
model.Person.uuid == model.PersonNote.parent_uuid))
g.set_sorter('person', model.Person.display_name)
g.set_filter('person', model.Person.display_name, label="Person Name")
# created_by
CreatorPerson = orm.aliased(model.Person)
g.set_joiner('created_by', lambda q: q.join(model.User).outerjoin(CreatorPerson,
CreatorPerson.uuid == model.User.person_uuid))
g.set_sorter('created_by', CreatorPerson.display_name)
2019-05-15 15:34:32 -05:00
g.set_sort_defaults('created', 'desc')
g.set_link('person')
g.set_link('subject')
g.set_link('created')
def configure_form(self, f):
super(PersonNoteView, self).configure_form(f)
# person
f.set_readonly('person')
f.set_renderer('person', self.render_person)
# created
f.set_readonly('created')
# created_by
f.set_readonly('created_by')
f.set_renderer('created_by', self.render_user)
@colander.deferred
def valid_note_uuid(node, kw):
session = kw['session']
person_uuid = kw['person_uuid']
def validate(node, value):
note = session.query(model.PersonNote).get(value)
if not note:
raise colander.Invalid(node, "Note not found")
if note.person.uuid != person_uuid:
raise colander.Invalid(node, "Note is for the wrong person")
return note.uuid
return validate
class NoteSchema(colander.Schema):
uuid = colander.SchemaNode(colander.String(),
validator=valid_note_uuid)
note_type = colander.SchemaNode(colander.String())
note_subject = 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):
# autocomplete
config.add_route('people.autocomplete', '/people/autocomplete')
config.add_view(PeopleAutocomplete, route_name='people.autocomplete',
renderer='json', permission='people.list')
config.add_route('people.autocomplete.employees', '/people/autocomplete/employees')
config.add_view(PeopleEmployeesAutocomplete, route_name='people.autocomplete.employees',
renderer='json', permission='people.list')
PersonView.defaults(config)
PersonNoteView.defaults(config)
MergePeopleRequestView.defaults(config)