1483 lines
55 KiB
Python
1483 lines
55 KiB
Python
# -*- coding: utf-8; -*-
|
|
################################################################################
|
|
#
|
|
# Rattail -- Retail Software Framework
|
|
# Copyright © 2010-2022 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/>.
|
|
#
|
|
################################################################################
|
|
"""
|
|
Person Views
|
|
"""
|
|
|
|
from __future__ import unicode_literals, absolute_import
|
|
|
|
import datetime
|
|
import logging
|
|
|
|
import six
|
|
import sqlalchemy as sa
|
|
from sqlalchemy import orm
|
|
|
|
from rattail.db import model, api
|
|
from rattail.db.util import maxlen
|
|
from rattail.time import localtime
|
|
from rattail.util import OrderedDict, simple_error
|
|
|
|
import colander
|
|
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
|
|
from webhelpers2.html import HTML, tags
|
|
|
|
from tailbone import forms, grids
|
|
from tailbone.views import MasterView
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class PersonView(MasterView):
|
|
"""
|
|
Master view for the Person class.
|
|
"""
|
|
model_class = model.Person
|
|
model_title_plural = "People"
|
|
route_prefix = 'people'
|
|
touchable = True
|
|
has_versions = True
|
|
bulk_deletable = True
|
|
is_contact = True
|
|
manage_notes_from_profile_view = False
|
|
supports_autocomplete = True
|
|
|
|
labels = {
|
|
'default_phone': "Phone Number",
|
|
'default_email': "Email Address",
|
|
}
|
|
|
|
grid_columns = [
|
|
'display_name',
|
|
'first_name',
|
|
'last_name',
|
|
'phone',
|
|
'email',
|
|
'merge_requested',
|
|
]
|
|
|
|
form_fields = [
|
|
'first_name',
|
|
'middle_name',
|
|
'last_name',
|
|
'display_name',
|
|
'default_phone',
|
|
'default_email',
|
|
'address',
|
|
'employee',
|
|
'customers',
|
|
'members',
|
|
'users',
|
|
]
|
|
|
|
mergeable = True
|
|
|
|
def __init__(self, request):
|
|
super(PersonView, self).__init__(request)
|
|
app = self.get_rattail_app()
|
|
|
|
# always get a reference to the People Handler
|
|
self.people_handler = app.get_people_handler()
|
|
self.merge_handler = self.people_handler
|
|
# TODO: deprecate / remove this
|
|
self.handler = self.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")
|
|
|
|
g.set_link('display_name')
|
|
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.
|
|
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']
|
|
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
|
|
|
|
f.set_label('display_name', "Full Name")
|
|
|
|
# TODO: should remove this?
|
|
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?
|
|
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)
|
|
|
|
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)
|
|
|
|
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:
|
|
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)]))
|
|
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)
|
|
|
|
def render_users(self, person, field):
|
|
use_buefy = self.get_use_buefy()
|
|
users = person.users
|
|
items = []
|
|
for user in users:
|
|
text = user.username
|
|
url = self.request.route_url('users.view', uuid=user.uuid)
|
|
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
|
|
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")
|
|
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 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),
|
|
'phone_type_options': self.get_phone_type_options(),
|
|
'email_type_options': self.get_email_type_options(),
|
|
'max_lengths': self.get_max_lengths(),
|
|
'customers_data': self.get_context_customers(person),
|
|
'customer_xref_buttons': self.get_customer_xref_buttons(person),
|
|
'members_data': self.get_context_members(person),
|
|
'member_xref_buttons': self.get_member_xref_buttons(person),
|
|
'employee': employee,
|
|
'employee_data': self.get_context_employee(employee) if employee else {},
|
|
'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),
|
|
'dynamic_content_title': self.get_context_content_title(person),
|
|
}
|
|
|
|
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_customer_xref_buttons(self, person):
|
|
buttons = []
|
|
for supp in self.iter_view_supplements():
|
|
if hasattr(supp, 'get_customer_xref_buttons'):
|
|
buttons.extend(supp.get_customer_xref_buttons(person) or [])
|
|
buttons = self.normalize_xref_buttons(buttons)
|
|
return buttons
|
|
|
|
def get_member_xref_buttons(self, person):
|
|
buttons = []
|
|
for supp in self.iter_view_supplements():
|
|
if hasattr(supp, 'get_member_xref_buttons'):
|
|
buttons.extend(supp.get_member_xref_buttons(person) or [])
|
|
buttons = self.normalize_xref_buttons(buttons)
|
|
return buttons
|
|
|
|
def template_kwargs_view_profile(self, **kwargs):
|
|
"""
|
|
Stub method so subclass can call `super()` for it.
|
|
"""
|
|
return kwargs
|
|
|
|
def template_kwargs_view_profile_buefy(self, **kwargs):
|
|
"""
|
|
Note that any subclass should not need to define this method.
|
|
It by default invokes :meth:`template_kwargs_view_profile()`
|
|
and returns that result.
|
|
"""
|
|
return self.template_kwargs_view_profile(**kwargs)
|
|
|
|
def get_max_lengths(self):
|
|
model = self.model
|
|
return {
|
|
'person_first_name': maxlen(model.Person.first_name),
|
|
'person_middle_name': maxlen(model.Person.middle_name),
|
|
'person_last_name': maxlen(model.Person.last_name),
|
|
'address_street': maxlen(model.PersonMailingAddress.street),
|
|
'address_street2': maxlen(model.PersonMailingAddress.street2),
|
|
'address_city': maxlen(model.PersonMailingAddress.city),
|
|
'address_state': maxlen(model.PersonMailingAddress.state),
|
|
'address_zipcode': maxlen(model.PersonMailingAddress.zipcode),
|
|
}
|
|
|
|
def get_phone_type_options(self):
|
|
"""
|
|
Returns a list of "phone type" options, for use in dropdown.
|
|
"""
|
|
# TODO: should probably define this list somewhere else
|
|
phone_types = [
|
|
"Home",
|
|
"Mobile",
|
|
"Work",
|
|
"Other",
|
|
"Fax",
|
|
]
|
|
return [{'value': typ, 'label': typ}
|
|
for typ in phone_types]
|
|
|
|
def get_email_type_options(self):
|
|
"""
|
|
Returns a list of "email type" options, for use in dropdown.
|
|
"""
|
|
# TODO: should probably define this list somewhere else
|
|
email_types = [
|
|
"Home",
|
|
"Work",
|
|
"Other",
|
|
]
|
|
return [{'value': typ, 'label': typ}
|
|
for typ in email_types]
|
|
|
|
def get_context_person(self, person):
|
|
|
|
context = {
|
|
'uuid': person.uuid,
|
|
'first_name': person.first_name,
|
|
'middle_name': person.middle_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),
|
|
'phones': self.get_context_phones(person),
|
|
'emails': self.get_context_emails(person),
|
|
'dynamic_content_title': self.get_context_content_title(person),
|
|
}
|
|
|
|
if person.address:
|
|
context['address'] = self.get_context_address(person.address)
|
|
|
|
return context
|
|
|
|
def get_context_content_title(self, person):
|
|
return six.text_type(person)
|
|
|
|
def get_context_address(self, address):
|
|
context = {
|
|
'uuid': address.uuid,
|
|
'street': address.street,
|
|
'street2': address.street2,
|
|
'city': address.city,
|
|
'state': address.state,
|
|
'zipcode': address.zipcode,
|
|
'display': six.text_type(address),
|
|
}
|
|
|
|
model = self.model
|
|
if isinstance(address, model.PersonMailingAddress):
|
|
person = address.person
|
|
context['invalid'] = self.handler.address_is_invalid(person, address)
|
|
|
|
return context
|
|
|
|
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,
|
|
'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)
|
|
context['view_url'] = self.request.route_url('employees.view', uuid=employee.uuid)
|
|
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_edit_name(self):
|
|
"""
|
|
View which allows a person's name to be updated.
|
|
"""
|
|
person = self.get_instance()
|
|
data = dict(self.request.json_body)
|
|
|
|
self.handler.update_names(person,
|
|
first=data['first_name'],
|
|
middle=data['middle_name'],
|
|
last=data['last_name'])
|
|
|
|
self.Session.flush()
|
|
return {
|
|
'success': True,
|
|
'person': self.get_context_person(person),
|
|
'dynamic_content_title': self.get_context_content_title(person),
|
|
}
|
|
|
|
def get_context_phones(self, person):
|
|
data = []
|
|
for phone in person.phones:
|
|
data.append({
|
|
'uuid': phone.uuid,
|
|
'type': phone.type,
|
|
'number': phone.number,
|
|
'preferred': phone.preferred,
|
|
'preference': phone.preference,
|
|
})
|
|
return data
|
|
|
|
def profile_add_phone(self):
|
|
"""
|
|
View which adds a new phone number for the person.
|
|
"""
|
|
person = self.get_instance()
|
|
data = dict(self.request.json_body)
|
|
|
|
try:
|
|
phone = self.handler.add_phone(person, data['phone_number'],
|
|
type=data['phone_type'],
|
|
preferred=data['phone_preferred'])
|
|
except Exception as error:
|
|
log.warning("failed to add phone", exc_info=True)
|
|
return {'error': simple_error(error)}
|
|
|
|
self.Session.flush()
|
|
return {
|
|
'success': True,
|
|
'person': self.get_context_person(person),
|
|
}
|
|
|
|
def profile_update_phone(self):
|
|
"""
|
|
View which updates a phone number for the person.
|
|
"""
|
|
person = self.get_instance()
|
|
data = dict(self.request.json_body)
|
|
|
|
phone = self.Session.query(model.PersonPhoneNumber).get(data['phone_uuid'])
|
|
if not phone:
|
|
return {'error': "Phone not found."}
|
|
|
|
kwargs = {
|
|
'number': data['phone_number'],
|
|
'type': data['phone_type'],
|
|
}
|
|
if 'phone_preferred' in data:
|
|
kwargs['preferred'] = data['phone_preferred']
|
|
|
|
try:
|
|
phone = self.handler.update_phone(person, phone, **kwargs)
|
|
except Exception as error:
|
|
log.warning("failed to update phone", exc_info=True)
|
|
return {'error': simple_error(error)}
|
|
|
|
self.Session.flush()
|
|
return {
|
|
'success': True,
|
|
'person': self.get_context_person(person),
|
|
}
|
|
|
|
def profile_delete_phone(self):
|
|
"""
|
|
View which allows a person's phone number to be deleted.
|
|
"""
|
|
person = self.get_instance()
|
|
data = dict(self.request.json_body)
|
|
|
|
# validate phone
|
|
phone = self.Session.query(model.PersonPhoneNumber).get(data['phone_uuid'])
|
|
if not phone:
|
|
return {'error': "Phone not found."}
|
|
if phone not in person.phones:
|
|
return {'error': "Phone does not belong to this person."}
|
|
|
|
# remove phone
|
|
person.remove_phone(phone)
|
|
|
|
self.Session.flush()
|
|
return {
|
|
'success': True,
|
|
'person': self.get_context_person(person),
|
|
}
|
|
|
|
def profile_set_preferred_phone(self):
|
|
"""
|
|
View which allows a person's "preferred" phone to be set.
|
|
"""
|
|
person = self.get_instance()
|
|
data = dict(self.request.json_body)
|
|
|
|
# validate phone
|
|
phone = self.Session.query(model.PersonPhoneNumber).get(data['phone_uuid'])
|
|
if not phone:
|
|
return {'error': "Phone not found."}
|
|
if phone not in person.phones:
|
|
return {'error': "Phone does not belong to this person."}
|
|
|
|
# update phone preference
|
|
person.set_primary_phone(phone)
|
|
|
|
self.Session.flush()
|
|
return {
|
|
'success': True,
|
|
'person': self.get_context_person(person),
|
|
}
|
|
|
|
def get_context_emails(self, person):
|
|
data = []
|
|
for email in person.emails:
|
|
data.append({
|
|
'uuid': email.uuid,
|
|
'type': email.type,
|
|
'address': email.address,
|
|
'invalid': email.invalid,
|
|
'preferred': email.preferred,
|
|
'preference': email.preference,
|
|
})
|
|
return data
|
|
|
|
def profile_add_email(self):
|
|
"""
|
|
View which adds a new email address for the person.
|
|
"""
|
|
person = self.get_instance()
|
|
data = dict(self.request.json_body)
|
|
|
|
kwargs = {
|
|
'type': data['email_type'],
|
|
'invalid': False,
|
|
}
|
|
if 'email_preferred' in data:
|
|
kwargs['preferred'] = data['email_preferred']
|
|
|
|
try:
|
|
email = self.handler.add_email(person, data['email_address'], **kwargs)
|
|
except Exception as error:
|
|
log.warning("failed to add email", exc_info=True)
|
|
return {'error': simple_error(error)}
|
|
|
|
self.Session.flush()
|
|
return {
|
|
'success': True,
|
|
'person': self.get_context_person(person),
|
|
}
|
|
|
|
def profile_update_email(self):
|
|
"""
|
|
View which updates an email address for the person.
|
|
"""
|
|
person = self.get_instance()
|
|
data = dict(self.request.json_body)
|
|
|
|
email = self.Session.query(model.PersonEmailAddress).get(data['email_uuid'])
|
|
if not email:
|
|
return {'error': "Email not found."}
|
|
|
|
try:
|
|
email = self.handler.update_email(person, email,
|
|
address=data['email_address'],
|
|
type=data['email_type'],
|
|
invalid=data['email_invalid'])
|
|
except Exception as error:
|
|
log.warning("failed to add email", exc_info=True)
|
|
return {'error': simple_error(error)}
|
|
|
|
self.Session.flush()
|
|
return {
|
|
'success': True,
|
|
'person': self.get_context_person(person),
|
|
}
|
|
|
|
def profile_delete_email(self):
|
|
"""
|
|
View which allows a person's email address to be deleted.
|
|
"""
|
|
person = self.get_instance()
|
|
data = dict(self.request.json_body)
|
|
|
|
# validate email
|
|
email = self.Session.query(model.PersonEmailAddress).get(data['email_uuid'])
|
|
if not email:
|
|
return {'error': "Email not found."}
|
|
if email not in person.emails:
|
|
return {'error': "Email does not belong to this person."}
|
|
|
|
# remove email
|
|
person.remove_email(email)
|
|
|
|
self.Session.flush()
|
|
|
|
return {
|
|
'success': True,
|
|
'person': self.get_context_person(person),
|
|
}
|
|
|
|
def profile_set_preferred_email(self):
|
|
"""
|
|
View which allows a person's "preferred" email to be set.
|
|
"""
|
|
person = self.get_instance()
|
|
data = dict(self.request.json_body)
|
|
|
|
# validate email
|
|
email = self.Session.query(model.PersonEmailAddress).get(data['email_uuid'])
|
|
if not email:
|
|
return {'error': "Email not found."}
|
|
if email not in person.emails:
|
|
return {'error': "Email does not belong to this person."}
|
|
|
|
# update email preference
|
|
person.set_primary_email(email)
|
|
|
|
self.Session.flush()
|
|
return {
|
|
'success': True,
|
|
'person': self.get_context_person(person),
|
|
}
|
|
|
|
def profile_edit_address(self):
|
|
"""
|
|
View which allows a person's mailing address to be updated.
|
|
"""
|
|
person = self.get_instance()
|
|
data = dict(self.request.json_body)
|
|
|
|
# update person address
|
|
address = self.people_handler.ensure_address(person)
|
|
self.people_handler.update_address(person, address, **data)
|
|
|
|
self.Session.flush()
|
|
return {
|
|
'success': True,
|
|
'person': self.get_context_person(person),
|
|
}
|
|
|
|
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'])
|
|
self.Session.flush()
|
|
return self.profile_start_employee_result(employee, start_date)
|
|
|
|
def profile_start_employee_result(self, employee, start_date):
|
|
person = employee.person
|
|
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),
|
|
'dynamic_content_title': self.get_context_content_title(person),
|
|
}
|
|
|
|
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'))
|
|
self.Session.flush()
|
|
return self.profile_end_employee_result(employee, end_date)
|
|
|
|
def profile_end_employee_result(self, employee, end_date):
|
|
person = employee.person
|
|
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),
|
|
'dynamic_content_title': self.get_context_content_title(person),
|
|
}
|
|
|
|
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,
|
|
'employee': self.get_context_employee(employee),
|
|
'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 - edit personal details
|
|
config.add_tailbone_permission('people_profile',
|
|
'people_profile.edit_person',
|
|
"Edit the Personal details")
|
|
|
|
# profile - edit name
|
|
config.add_route('{}.profile_edit_name'.format(route_prefix),
|
|
'{}/profile/edit-name'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='profile_edit_name',
|
|
route_name='{}.profile_edit_name'.format(route_prefix),
|
|
renderer='json',
|
|
permission='people_profile.edit_person')
|
|
|
|
# profile - add phone
|
|
config.add_route('{}.profile_add_phone'.format(route_prefix),
|
|
'{}/profile/add-phone'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='profile_add_phone',
|
|
route_name='{}.profile_add_phone'.format(route_prefix),
|
|
renderer='json',
|
|
permission='people_profile.edit_person')
|
|
|
|
# profile - update phone
|
|
config.add_route('{}.profile_update_phone'.format(route_prefix),
|
|
'{}/profile/update-phone'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='profile_update_phone',
|
|
route_name='{}.profile_update_phone'.format(route_prefix),
|
|
renderer='json',
|
|
permission='people_profile.edit_person')
|
|
|
|
# profile - delete phone
|
|
config.add_route('{}.profile_delete_phone'.format(route_prefix),
|
|
'{}/profile/delete-phone'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='profile_delete_phone',
|
|
route_name='{}.profile_delete_phone'.format(route_prefix),
|
|
renderer='json',
|
|
permission='people_profile.edit_person')
|
|
|
|
# profile - set preferred phone
|
|
config.add_route('{}.profile_set_preferred_phone'.format(route_prefix),
|
|
'{}/profile/set-preferred-phone'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='profile_set_preferred_phone',
|
|
route_name='{}.profile_set_preferred_phone'.format(route_prefix),
|
|
renderer='json',
|
|
permission='people_profile.edit_person')
|
|
|
|
# profile - add email
|
|
config.add_route('{}.profile_add_email'.format(route_prefix),
|
|
'{}/profile/add-email'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='profile_add_email',
|
|
route_name='{}.profile_add_email'.format(route_prefix),
|
|
renderer='json',
|
|
permission='people_profile.edit_person')
|
|
|
|
# profile - update email
|
|
config.add_route('{}.profile_update_email'.format(route_prefix),
|
|
'{}/profile/update-email'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='profile_update_email',
|
|
route_name='{}.profile_update_email'.format(route_prefix),
|
|
renderer='json',
|
|
permission='people_profile.edit_person')
|
|
|
|
# profile - delete email
|
|
config.add_route('{}.profile_delete_email'.format(route_prefix),
|
|
'{}/profile/delete-email'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='profile_delete_email',
|
|
route_name='{}.profile_delete_email'.format(route_prefix),
|
|
renderer='json',
|
|
permission='people_profile.edit_person')
|
|
|
|
# profile - set preferred email
|
|
config.add_route('{}.profile_set_preferred_email'.format(route_prefix),
|
|
'{}/profile/set-preferred-email'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='profile_set_preferred_email',
|
|
route_name='{}.profile_set_preferred_email'.format(route_prefix),
|
|
renderer='json',
|
|
permission='people_profile.edit_person')
|
|
|
|
# profile - edit address
|
|
config.add_route('{}.profile_edit_address'.format(route_prefix),
|
|
'{}/profile/edit-address'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='profile_edit_address',
|
|
route_name='{}.profile_edit_address'.format(route_prefix),
|
|
renderer='json',
|
|
permission='people_profile.edit_person')
|
|
|
|
# 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))
|
|
|
|
|
|
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)
|
|
|
|
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 defaults(config, **kwargs):
|
|
base = globals()
|
|
|
|
PersonView = kwargs.get('PersonView', base['PersonView'])
|
|
PersonView.defaults(config)
|
|
|
|
PersonNoteView = kwargs.get('PersonNoteView', base['PersonNoteView'])
|
|
PersonNoteView.defaults(config)
|
|
|
|
MergePeopleRequestView = kwargs.get('MergePeopleRequestView', base['MergePeopleRequestView'])
|
|
MergePeopleRequestView.defaults(config)
|
|
|
|
|
|
def includeme(config):
|
|
defaults(config)
|