Add support for version history in person profile view
yay, finally
This commit is contained in:
parent
816e652357
commit
cfdb492349
|
@ -18,11 +18,59 @@
|
|||
${dynamic_content_title}
|
||||
</%def>
|
||||
|
||||
<%def name="render_instance_header_buttons()">
|
||||
% if request.has_perm('people_profile.view_versions'):
|
||||
<b-button v-if="!viewingHistory"
|
||||
icon-pack="fas"
|
||||
icon-left="history"
|
||||
@click="viewHistory()">
|
||||
View History
|
||||
</b-button>
|
||||
<div v-if="viewingHistory"
|
||||
class="buttons">
|
||||
<b-button icon-pack="fas"
|
||||
icon-left="redo"
|
||||
@click="refreshHistory()"
|
||||
:disabled="gettingRevisions">
|
||||
{{ gettingRevisions ? "Working, please wait..." : "Refresh History" }}
|
||||
</b-button>
|
||||
<b-button icon-pack="fas"
|
||||
icon-left="user"
|
||||
@click="viewingHistory = false">
|
||||
View Profile
|
||||
</b-button>
|
||||
</div>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="page_content()">
|
||||
<profile-info @change-content-title="changeContentTitle">
|
||||
<profile-info @change-content-title="changeContentTitle"
|
||||
% if request.has_perm('people_profile.view_versions'):
|
||||
:viewing-history="viewingHistory"
|
||||
:getting-revisions="gettingRevisions"
|
||||
:revisions="revisions"
|
||||
:revision-version-map="revisionVersionMap"
|
||||
% endif
|
||||
>
|
||||
</profile-info>
|
||||
</%def>
|
||||
|
||||
<%def name="render_this_page_component()">
|
||||
## TODO: should override this in a cleaner way! too much duplicate code w/ parent template
|
||||
<this-page @change-content-title="changeContentTitle"
|
||||
% if can_edit_help:
|
||||
:configure-fields-help="configureFieldsHelp"
|
||||
% endif
|
||||
% if request.has_perm('people_profile.view_versions'):
|
||||
:viewing-history="viewingHistory"
|
||||
:getting-revisions="gettingRevisions"
|
||||
:revisions="revisions"
|
||||
:revision-version-map="revisionVersionMap"
|
||||
% endif
|
||||
>
|
||||
</this-page>
|
||||
</%def>
|
||||
|
||||
<%def name="render_this_page()">
|
||||
${self.page_content()}
|
||||
</%def>
|
||||
|
@ -551,6 +599,16 @@
|
|||
{{ member._key }}
|
||||
</b-field>
|
||||
|
||||
<b-field horizontal label="Person">
|
||||
<a v-if="member.person_uuid != person.uuid"
|
||||
:href="member.view_profile_url">
|
||||
{{ member.person_display_name }}
|
||||
</a>
|
||||
<span v-if="member.person_uuid == person.uuid">
|
||||
{{ member.person_display_name }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
<b-field horizontal label="Membership Type">
|
||||
<a v-if="member.view_membership_type_url"
|
||||
:href="member.view_membership_type_url">
|
||||
|
@ -562,7 +620,7 @@
|
|||
</b-field>
|
||||
|
||||
<b-field horizontal label="Active">
|
||||
{{ member.active }}
|
||||
{{ member.active ? "Yes" : "No" }}
|
||||
</b-field>
|
||||
|
||||
<b-field horizontal label="Joined">
|
||||
|
@ -574,16 +632,6 @@
|
|||
{{ member.withdrew }}
|
||||
</b-field>
|
||||
|
||||
<b-field horizontal label="Person">
|
||||
<a v-if="member.person_uuid != person.uuid"
|
||||
:href="member.view_profile_url">
|
||||
{{ member.person_display_name }}
|
||||
</a>
|
||||
<span v-if="member.person_uuid == person.uuid">
|
||||
{{ member.person_display_name }}
|
||||
</span>
|
||||
</b-field>
|
||||
|
||||
</div>
|
||||
<div class="buttons" style="align-items: start;">
|
||||
${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()">
|
||||
<script type="text/x-template" id="profile-info-template">
|
||||
<div>
|
||||
|
||||
${self.render_profile_info_extra_buttons()}
|
||||
|
||||
<b-tabs v-model="activeTab"
|
||||
% if request.has_perm('people_profile.view_versions'):
|
||||
v-show="!viewingHistory"
|
||||
% endif
|
||||
type="is-boxed"
|
||||
@input="activeTabChanged">
|
||||
${self.render_profile_tabs()}
|
||||
</b-tabs>
|
||||
|
||||
% if request.has_perm('people_profile.view_versions'):
|
||||
|
||||
${revisions_grid.render_buefy_table_element(data_prop='revisions',
|
||||
show_footer=True,
|
||||
vshow='viewingHistory',
|
||||
loading='gettingRevisions')|n}
|
||||
|
||||
<b-modal :active.sync="showingRevisionDialog">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
|
||||
<div>
|
||||
<b-field horizontal label="Changed">
|
||||
<div v-html="revision.changed"></div>
|
||||
</b-field>
|
||||
<b-field horizontal label="Changed by">
|
||||
<div v-html="revision.changed_by"></div>
|
||||
</b-field>
|
||||
<b-field horizontal label="IP Address">
|
||||
<div v-html="revision.remote_addr"></div>
|
||||
</b-field>
|
||||
<b-field horizontal label="Comment">
|
||||
<div v-html="revision.comment"></div>
|
||||
</b-field>
|
||||
<b-field horizontal label="TXN ID">
|
||||
<div v-html="revision.txnid"></div>
|
||||
</b-field>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<b-button @click="viewPrevRevision()"
|
||||
:disabled="!revision.prev_txnid">
|
||||
« Prev
|
||||
</b-button>
|
||||
<b-button @click="viewNextRevision()"
|
||||
:disabled="!revision.next_txnid">
|
||||
» Next
|
||||
</b-button>
|
||||
</div>
|
||||
<br />
|
||||
<b-button @click="toggleVersionFields()">
|
||||
{{ revisionShowAllFields ? "Show Diffs Only" : "Show All Fields" }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div v-for="version in revision.versions"
|
||||
:key="version.key">
|
||||
|
||||
<p class="block has-text-weight-bold">
|
||||
{{ version.model_title }}
|
||||
</p>
|
||||
|
||||
<table class="diff monospace is-size-7"
|
||||
:class="version.diff_class">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>field name</th>
|
||||
<th>old value</th>
|
||||
<th>new value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="field in version.fields"
|
||||
:key="field"
|
||||
:class="{diff: version.values[field].after != version.values[field].before}"
|
||||
v-show="revisionShowAllFields || version.values[field].after != version.values[field].before">
|
||||
<td class="field">{{ field }}</td>
|
||||
<td class="old-value">{{ version.values[field].before }}</td>
|
||||
<td class="new-value">{{ version.values[field].after }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
% endif
|
||||
|
||||
</div>
|
||||
</script>
|
||||
</%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()}
|
||||
<script type="text/javascript">
|
||||
|
||||
% if request.has_perm('people_profile.view_versions'):
|
||||
ThisPage.props.viewingHistory = Boolean
|
||||
ThisPage.props.gettingRevisions = Boolean
|
||||
ThisPage.props.revisions = Array
|
||||
ThisPage.props.revisionVersionMap = null
|
||||
% endif
|
||||
|
||||
ThisPage.methods.changeContentTitle = function(newTitle) {
|
||||
this.$emit('change-content-title', newTitle)
|
||||
}
|
||||
|
@ -1717,5 +1910,49 @@
|
|||
${self.make_profile_info_component()}
|
||||
</%def>
|
||||
|
||||
<%def name="modify_whole_page_vars()">
|
||||
${parent.modify_whole_page_vars()}
|
||||
|
||||
% if request.has_perm('people_profile.view_versions'):
|
||||
<script type="text/javascript">
|
||||
|
||||
WholePageData.viewingHistory = false
|
||||
WholePageData.gettingRevisions = false
|
||||
WholePageData.gotRevisions = false
|
||||
WholePageData.revisions = []
|
||||
WholePageData.revisionVersionMap = null
|
||||
|
||||
WholePage.methods.viewHistory = function() {
|
||||
this.viewingHistory = true
|
||||
|
||||
if (!this.gotRevisions && !this.gettingRevisions) {
|
||||
this.getRevisions()
|
||||
}
|
||||
}
|
||||
|
||||
WholePage.methods.refreshHistory = function() {
|
||||
if (!this.gettingRevisions) {
|
||||
this.getRevisions()
|
||||
}
|
||||
}
|
||||
|
||||
WholePage.methods.getRevisions = function() {
|
||||
this.gettingRevisions = true
|
||||
|
||||
let url = '${url('people.view_profile_revisions', uuid=person.uuid)}'
|
||||
this.simpleGET(url, {}, response => {
|
||||
this.revisions = response.data.data
|
||||
this.revisionVersionMap = response.data.vmap
|
||||
this.gotRevisions = true
|
||||
this.gettingRevisions = false
|
||||
}, response => {
|
||||
this.gettingRevisions = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
Loading…
Reference in a new issue