# -*- 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 . # ################################################################################ """ 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), 'members_data': self.get_context_members(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 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 = person.address if not address: address = person.add_address() address.street = data['street'] address.street2 = data['street2'] address.city = data['city'] address.state = data['state'] address.zipcode = data['zipcode'] self.handler.mark_address_invalid(person, address, data['invalid']) 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): 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')) self.Session.flush() 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, '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)