Extract changelog into global sqlalchemy event handler

master
informatic 2020-04-16 23:26:47 +02:00
parent df88c56678
commit 0cf4531bbc
3 changed files with 68 additions and 59 deletions

View File

@ -5,41 +5,8 @@ from wtforms import TextAreaField
from formity.models import FaceshieldRequest, RequestChange, Status, PostalCode, ExternalUser
from spaceauth import current_user
import enum
import decimal
from flask_weasyprint import HTML, render_pdf
from sqlalchemy import inspect, func
from sqlalchemy.orm import class_mapper
from sqlalchemy.orm.attributes import get_history
diff_sanitization_rules = [
(enum.Enum, lambda v: v.name),
(decimal.Decimal, float),
]
def get_diff(target, blacklist=['created', 'updated']):
state_before = {}
state_after = {}
inspr = inspect(target)
attrs = class_mapper(target.__class__).column_attrs
for attr in attrs:
if attr.key in blacklist:
continue
hist = getattr(inspr.attrs, attr.key).history
if hist.has_changes():
state_before[attr.key] = get_history(target, attr.key)[2].pop()
state_after[attr.key] = getattr(target, attr.key)
for data in [state_before, state_after]:
for t, c in diff_sanitization_rules:
if isinstance(data[attr.key], t):
data[attr.key] = c(data[attr.key])
if state_after[attr.key] == state_before[attr.key] or (state_after[attr.key] in ['', None] and state_before[attr.key] in ['', None]):
state_after.pop(attr.key)
state_before.pop(attr.key)
return state_before, state_after
from sqlalchemy import func
class IndexView(AdminSecurityMixin, flask_admin.AdminIndexView):
@ -106,8 +73,13 @@ class FaceshieldRequestAdmin(ModelView):
'id',
'entity_info', 'full_name', 'phone_number', 'email', 'extra',
'faceshield_front_required', 'faceshield_model',
'faceshield_full_required',
'faceshield_full_delivered', 'faceshield_front_delivered', 'handling_orga',
'faceshield_front_delivered',
'faceshield_full_required', 'faceshield_full_delivered',
'adapter_3m_dar_required', 'adapter_3m_dar_delivered',
'adapter_easybreath_dar_required', 'adapter_easybreath_dar_delivered',
'adapter_rd40_dar_required', 'adapter_rd40_dar_delivered',
'adapter_secura_dar_required', 'adapter_secura_dar_delivered',
'handling_orga',
'created', 'ua', 'ip', 'status', 'remarks',
'shipping_name', 'shipping_street', 'shipping_postalcode', 'shipping_city',
'shipping_latitude', 'shipping_longitude',
@ -144,24 +116,6 @@ class FaceshieldRequestAdmin(ModelView):
def can_export(self):
return not current_user.external
def on_model_change(self, form, model, is_created):
if is_created:
return
try:
before, after = get_diff(model)
except Exception:
# xD
before, after = {'diff': 'failed'}, {'diff': 'failed'}
if before or after:
db.session.add(RequestChange(
user_id=current_user.get_id() or 'nobody',
request_id=model.id,
state_before=before,
state_after=after,
))
@flask_admin.expose('/label/')
def label(self):
return render_pdf(HTML(string=self.label_html()))
@ -180,7 +134,6 @@ class FaceshieldRequestAdmin(ModelView):
if req.status != status:
req.status = status
count += 1
self.on_model_change(None, req, False)
db.session.commit()

View File

@ -2,11 +2,11 @@ from datetime import datetime
import enum
import hashlib
import os
from jinja2 import Markup
from formity.extensions import db
from sqlalchemy import event
from sqlalchemy.ext.hybrid import hybrid_property
from formity.utils import get_diff
class Status(enum.Enum):
new = 1
@ -124,3 +124,23 @@ class ExternalUser(db.Model):
def check_password(self, password):
salt, h = self.password.split(':')
return self._hash(salt, password) == h
@event.listens_for(FaceshieldRequest, 'after_update')
def on_request_change(mapper, connection, target):
try:
before, after = get_diff(target)
except Exception as exc:
# xD
before, after = {'diff': 'failed'}, {'diff': 'failed'}
if before or after:
from flask_login import current_user
user_id = current_user.get_id() if current_user and current_user.get_id() else 'nobody'
@event.listens_for(db.session, "after_flush", once=True)
def receive_after_flush(session, context):
db.session.add(RequestChange(
user_id=user_id,
request_id=target.id,
state_before=before,
state_after=after,
))

View File

@ -1,6 +1,10 @@
import re
import enum
import decimal
import unicodedata
from sqlalchemy.orm import exc
from sqlalchemy import inspect
from sqlalchemy.orm import exc, class_mapper
from sqlalchemy.orm.attributes import get_history
from werkzeug.exceptions import abort
@ -47,3 +51,35 @@ def text_to_id(text):
text = re.sub('[ ]+', '_', text)
text = re.sub('[^0-9a-zA-Z_-]', '', text)
return text
diff_sanitization_rules = [
(enum.Enum, lambda v: v.name),
(decimal.Decimal, float),
]
def get_diff(target, blacklist=['created', 'updated']):
state_before = {}
state_after = {}
inspr = inspect(target)
attrs = class_mapper(target.__class__).column_attrs
for attr in attrs:
if attr.key in blacklist:
continue
hist = getattr(inspr.attrs, attr.key).history
if hist.has_changes():
state_before[attr.key] = get_history(target, attr.key)[2].pop()
state_after[attr.key] = getattr(target, attr.key)
for data in [state_before, state_after]:
for t, c in diff_sanitization_rules:
if isinstance(data[attr.key], t):
data[attr.key] = c(data[attr.key])
if state_after[attr.key] == state_before[attr.key] or (state_after[attr.key] in ['', None] and state_before[attr.key] in ['', None]):
state_after.pop(attr.key)
state_before.pop(attr.key)
return state_before, state_after