diff --git a/tailbone/templates/people/view_profile_buefy.mako b/tailbone/templates/people/view_profile_buefy.mako
index f21c021e..c1799c16 100644
--- a/tailbone/templates/people/view_profile_buefy.mako
+++ b/tailbone/templates/people/view_profile_buefy.mako
@@ -18,11 +18,59 @@
${dynamic_content_title}
%def>
+<%def name="render_instance_header_buttons()">
+ % if request.has_perm('people_profile.view_versions'):
+
+ View History
+
+
+
%def>
+<%def name="render_this_page_component()">
+ ## TODO: should override this in a cleaner way! too much duplicate code w/ parent template
+
+
+%def>
+
<%def name="render_this_page()">
${self.page_content()}
%def>
@@ -551,6 +599,16 @@
{{ member._key }}
+
+
+ {{ member.person_display_name }}
+
+
+ {{ member.person_display_name }}
+
+
+
@@ -562,7 +620,7 @@
- {{ member.active }}
+ {{ member.active ? "Yes" : "No" }}
@@ -574,16 +632,6 @@
{{ member.withdrew }}
-
-
- {{ member.person_display_name }}
-
-
- {{ member.person_display_name }}
-
-
-
${self.render_member_panel_buttons(member)}
@@ -1019,14 +1067,112 @@
${self.render_user_tab()}
%def>
+<%def name="render_profile_info_extra_buttons()">%def>
+
<%def name="render_profile_info_template()">
%def>
@@ -1611,11 +1757,28 @@
phoneTypeOptions: ${json.dumps(phone_type_options)|n},
emailTypeOptions: ${json.dumps(email_type_options)|n},
maxLengths: ${json.dumps(max_lengths)|n},
+
+ % if request.has_perm('people_profile.view_versions'):
+ loadingRevisions: false,
+ showingRevisionDialog: false,
+ revision: {},
+ revisionShowAllFields: false,
+ % endif
}
let ProfileInfo = {
template: '#profile-info-template',
mixins: [FormPosterMixin],
+
+ % if request.has_perm('people_profile.view_versions'):
+ props: {
+ viewingHistory: Boolean,
+ gettingRevisions: Boolean,
+ revisions: Array,
+ revisionVersionMap: null,
+ },
+ % endif
+
computed: {},
methods: {
@@ -1641,6 +1804,29 @@
},
activeTabChangedExtra(value) {},
+
+ % if request.has_perm('people_profile.view_versions'):
+
+ viewRevision(row) {
+ this.revision = this.revisionVersionMap[row.txnid]
+ this.showingRevisionDialog = true
+ },
+
+ viewPrevRevision() {
+ let txnid = this.revision.prev_txnid
+ this.revision = this.revisionVersionMap[txnid]
+ },
+
+ viewNextRevision() {
+ let txnid = this.revision.next_txnid
+ this.revision = this.revisionVersionMap[txnid]
+ },
+
+ toggleVersionFields() {
+ this.revisionShowAllFields = !this.revisionShowAllFields
+ },
+
+ % endif
},
}
@@ -1662,6 +1848,13 @@
${parent.modify_this_page_vars()}
+ % endif
+%def>
+
${parent.body()}
diff --git a/tailbone/views/people.py b/tailbone/views/people.py
index 89b857f1..ce15e48a 100644
--- a/tailbone/views/people.py
+++ b/tailbone/views/people.py
@@ -30,6 +30,7 @@ from collections import OrderedDict
import sqlalchemy as sa
from sqlalchemy import orm
+import sqlalchemy_continuum as continuum
from rattail.db import model, api
from rattail.db.util import maxlen
@@ -42,6 +43,7 @@ from webhelpers2.html import HTML, tags
from tailbone import forms, grids
from tailbone.views import MasterView
+from tailbone.util import raw_datetime
log = logging.getLogger(__name__)
@@ -429,6 +431,9 @@ class PersonView(MasterView):
'dynamic_content_title': self.get_context_content_title(person),
}
+ if self.request.has_perm('people_profile.view_versions'):
+ context['revisions_grid'] = self.profile_revisions_grid(person)
+
template = 'view_profile_buefy'
return self.render_to_response(template, context)
@@ -1015,6 +1020,188 @@ class PersonView(MasterView):
'employee': self.get_context_employee(employee),
}
+ def profile_revisions_grid(self, person):
+ route_prefix = self.get_route_prefix()
+ factory = self.get_grid_factory()
+ g = factory(
+ '{}.profile.revisions'.format(route_prefix),
+ [], # start with empty data!
+ request=self.request,
+ columns=[
+ 'changed',
+ 'changed_by',
+ 'remote_addr',
+ 'comment',
+ ],
+ labels={
+ 'remote_addr': "IP Address",
+ },
+ linked_columns=[
+ 'changed',
+ 'changed_by',
+ 'comment',
+ ],
+ main_actions=[
+ self.make_action('view', icon='eye', url='#',
+ click_handler='viewRevision(props.row)'),
+ ],
+ )
+ return g
+
+ def profile_revisions_collect(self, person, versions=None):
+ model = self.model
+ versions = versions or []
+
+ # Person
+ cls = continuum.version_class(model.Person)
+ query = self.Session.query(cls)\
+ .filter(cls.uuid == person.uuid)
+ versions.extend(query.all())
+
+ # User
+ cls = continuum.version_class(model.User)
+ query = self.Session.query(cls)\
+ .filter(cls.person_uuid == person.uuid)
+ versions.extend(query.all())
+
+ # Member
+ cls = continuum.version_class(model.Member)
+ query = self.Session.query(cls)\
+ .filter(cls.person_uuid == person.uuid)
+ versions.extend(query.all())
+
+ # Employee
+ cls = continuum.version_class(model.Employee)
+ query = self.Session.query(cls)\
+ .filter(cls.person_uuid == person.uuid)
+ versions.extend(query.all())
+
+ # EmployeeHistory
+ cls = continuum.version_class(model.EmployeeHistory)
+ query = self.Session.query(cls)\
+ .join(model.Employee,
+ model.Employee.uuid == cls.employee_uuid)\
+ .filter(model.Employee.person_uuid == person.uuid)
+ versions.extend(query.all())
+
+ # PersonPhoneNumber
+ cls = continuum.version_class(model.PersonPhoneNumber)
+ query = self.Session.query(cls)\
+ .filter(cls.parent_uuid == person.uuid)
+ versions.extend(query.all())
+
+ # PersonEmailAddress
+ cls = continuum.version_class(model.PersonEmailAddress)
+ query = self.Session.query(cls)\
+ .filter(cls.parent_uuid == person.uuid)
+ versions.extend(query.all())
+
+ # PersonMailingAddress
+ cls = continuum.version_class(model.PersonMailingAddress)
+ query = self.Session.query(cls)\
+ .filter(cls.parent_uuid == person.uuid)
+ versions.extend(query.all())
+
+ # CustomerPerson
+ cls = continuum.version_class(model.CustomerPerson)
+ query = self.Session.query(cls)\
+ .filter(cls.person_uuid == person.uuid)
+ versions.extend(query.all())
+
+ # Customer
+ cls = continuum.version_class(model.Customer)
+ query = self.Session.query(cls)\
+ .join(model.CustomerPerson, model.CustomerPerson.customer_uuid == cls.uuid)\
+ .filter(model.CustomerPerson.person_uuid == person.uuid)
+ versions.extend(query.all())
+
+ # PersonNote
+ cls = continuum.version_class(model.PersonNote)
+ query = self.Session.query(cls)\
+ .filter(cls.parent_uuid == person.uuid)
+ versions.extend(query.all())
+
+ return versions
+
+ def profile_revisions_data(self):
+ """
+ View which locates and organizes all relevant "transaction"
+ (version) history data for a given Person. Returns JSON, for
+ use with the Buefy table element on the full profile view.
+ """
+ person = self.get_instance()
+ versions = self.profile_revisions_collect(person)
+
+ # organize final table data
+ data = []
+ all_txns = set([v.transaction for v in versions])
+ for i, txn in enumerate(
+ sorted(all_txns, key=lambda txn: txn.issued_at, reverse=True),
+ 1):
+ data.append({
+ 'txnid': txn.id,
+ 'changed': raw_datetime(self.rattail_config, txn.issued_at),
+ 'changed_by': str(txn.user or '') or None,
+ 'remote_addr': txn.remote_addr,
+ 'comment': txn.meta.get('comment'),
+ })
+ # also stash the sequential index for this transaction, for use later
+ txn._sequential_index = i
+
+ # also organize final transaction/versions (diff) map
+ vmap = {}
+ for version in versions:
+
+ if version.previous and version.operation_type == continuum.Operation.DELETE:
+ diff_class = 'deleted'
+ elif version.previous:
+ diff_class = 'dirty'
+ else:
+ diff_class = 'new'
+
+ # collect before/after field values for version
+ fields = self.fields_for_version(version)
+ values = {}
+ for field in fields:
+ before = ''
+ after = ''
+ if diff_class != 'new':
+ before = repr(getattr(version.previous, field))
+ if diff_class != 'deleted':
+ after = repr(getattr(version, field))
+ values[field] = {'before': before, 'after': after}
+
+ if version.transaction_id not in vmap:
+ txn = version.transaction
+ prev_txnid = None
+ next_txnid = None
+ if txn._sequential_index < len(data):
+ prev_txnid = data[txn._sequential_index]['txnid']
+ if txn._sequential_index > 1:
+ next_txnid = data[txn._sequential_index - 2]['txnid']
+ vmap[txn.id] = {
+ 'index': txn._sequential_index,
+ 'txnid': txn.id,
+ 'prev_txnid': prev_txnid,
+ 'next_txnid': next_txnid,
+ 'changed': raw_datetime(self.rattail_config, txn.issued_at,
+ verbose=True),
+ 'changed_by': str(txn.user or '') or None,
+ 'remote_addr': txn.remote_addr,
+ 'comment': txn.meta.get('comment'),
+ 'versions': [],
+ }
+
+ vmap[version.transaction_id]['versions'].append({
+ 'key': id(version),
+ 'model_title': self.title_for_version(version),
+ 'diff_class': diff_class,
+ 'fields': fields,
+ 'values': values,
+ })
+
+ return {'data': data, 'vmap': vmap}
+
def make_note_form(self, mode, person):
schema = NoteSchema().bind(session=self.Session(),
person_uuid=person.uuid)
@@ -1269,6 +1456,18 @@ class PersonView(MasterView):
renderer='json',
permission='employees.edit')
+ # profile - revisions data
+ config.add_tailbone_permission('people_profile',
+ 'people_profile.view_versions',
+ "View full version history for a profile")
+ config.add_route(f'{route_prefix}.view_profile_revisions',
+ f'{instance_url_prefix}/profile/revisions',
+ request_method='GET')
+ config.add_view(cls, attr='profile_revisions_data',
+ route_name=f'{route_prefix}.view_profile_revisions',
+ permission='people_profile.view_versions',
+ renderer='json')
+
# manage notes from profile view
if cls.manage_notes_from_profile_view: