python - SQLAlchemy audit logging; how to handle deletes? -


i'm using modified version of versioning code example comes sqlalchemy record user id , date on changes. however, want modify deletes done marking is_deleted type flag instead of running actual sql delete. problem i'm not sure how capture delete , replace update.

here's have far:

''' http://docs.sqlalchemy.org/en/rel_0_8/orm/examples.html?highlight=versioning#versioned-objects ''' sqlalchemy.ext.declarative import declared_attr sqlalchemy.orm import mapper, class_mapper, attributes, object_mapper, scoping sqlalchemy.orm.session import session sqlalchemy.orm.exc import unmappedclasserror, unmappedcolumnerror sqlalchemy import table, column, foreignkeyconstraint, datetime, string, boolean sqlalchemy import event sqlalchemy.orm.properties import relationshipproperty datetime import datetime sqlalchemy.schema import foreignkey sqlalchemy.sql.expression import false  def col_references_table(col, table):     fk in col.foreign_keys:         if fk.references(table):             return true     return false  def _history_mapper(local_mapper):     cls = local_mapper.class_      # set "active_history" flag     # on on column-mapped attributes old version     # of info loaded (currently sets on attributes)     prop in local_mapper.iterate_properties:         getattr(local_mapper.class_, prop.key).impl.active_history = true      super_mapper = local_mapper.inherits     super_history_mapper = getattr(cls, '__history_mapper__', none)      polymorphic_on = none     super_fks = []     if not super_mapper or local_mapper.local_table not super_mapper.local_table:         cols = []         column in local_mapper.local_table.c:             if column.name.startswith('version_'):                 continue              col = column.copy()             col.unique = false              if super_mapper , col_references_table(column, super_mapper.local_table):                 super_fks.append((col.key, list(super_history_mapper.local_table.primary_key)[0]))              cols.append(col)              if column local_mapper.polymorphic_on:                 polymorphic_on = col          if super_mapper:             super_fks.append(('version_datetime', super_history_mapper.base_mapper.local_table.c.version_datetime))             super_fks.append(('version_userid', super_history_mapper.base_mapper.local_table.c.version_userid))             super_fks.append(('version_deleted', super_history_mapper.base_mapper.local_table.c.version_deleted))             cols.append(column('version_datetime', datetime, default=datetime.now, nullable=false, primary_key=true, info={'colanderalchemy': {'exclude': true}}))             cols.append(column('version_userid', string(60), foreignkey("user.login"), nullable=true, info={'colanderalchemy': {'exclude': true}}))             cols.append(column('version_deleted', boolean, server_default=false(), nullable=false, info={'colanderalchemy': {'exclude': true}}))         else:             cols.append(column('version_datetime', datetime, default=datetime.now, nullable=false, primary_key=true, info={'colanderalchemy': {'exclude': true}}))             cols.append(column('version_userid', string(60), foreignkey("user.login"), nullable=true, info={'colanderalchemy': {'exclude': true}}))             cols.append(column('version_deleted', boolean, server_default=false(), nullable=false, info={'colanderalchemy': {'exclude': true}}))          if super_fks:             cols.append(foreignkeyconstraint(*zip(*super_fks)))          table = table(local_mapper.local_table.name + '_history', local_mapper.local_table.metadata,            *cols         )     else:         # single table inheritance.  take additional columns may have         # been added , add them history table.         column in local_mapper.local_table.c:             if column.key not in super_history_mapper.local_table.c:                 col = column.copy()                 col.unique = false                 super_history_mapper.local_table.append_column(col)         table = none      if super_history_mapper:         bases = (super_history_mapper.class_,)     else:         bases = local_mapper.base_mapper.class_.__bases__     versioned_cls = type.__new__(type, "%shistory" % cls.__name__, bases, {})      m = mapper(             versioned_cls,             table,             inherits=super_history_mapper,             polymorphic_on=polymorphic_on,             polymorphic_identity=local_mapper.polymorphic_identity             )     cls.__history_mapper__ = m      if not super_history_mapper:         local_mapper.local_table.append_column(             column('version_datetime', datetime, default=datetime.now, nullable=false, primary_key=false, info={'colanderalchemy': {'exclude': true}})         )         local_mapper.add_property("version_datetime", local_mapper.local_table.c.version_datetime)         local_mapper.local_table.append_column(             column('version_userid', string(60), foreignkey("user.login"), nullable=true, info={'colanderalchemy': {'exclude': true}})         )         local_mapper.add_property("version_userid", local_mapper.local_table.c.version_userid)         local_mapper.local_table.append_column(             column('version_deleted', boolean, server_default=false(), nullable=false, info={'colanderalchemy': {'exclude': true}})         )         local_mapper.add_property("version_deleted", local_mapper.local_table.c.version_deleted)   class versioned(object):     @declared_attr     def __mapper_cls__(cls):         def map(cls, *arg, **kw):             mp = mapper(cls, *arg, **kw)             _history_mapper(mp)             return mp         return map   def versioned_objects(iter):     obj in iter:         if hasattr(obj, '__history_mapper__'):             yield obj  def create_version(obj, session, deleted = false):     obj_mapper = object_mapper(obj)     history_mapper = obj.__history_mapper__     history_cls = history_mapper.class_      obj_state = attributes.instance_state(obj)      attr = {}      obj_changed = false      om, hm in zip(obj_mapper.iterate_to_root(), history_mapper.iterate_to_root()):         if hm.single:             continue          hist_col in hm.local_table.c:             if hist_col.key.startswith('version_'):                 continue              obj_col = om.local_table.c[hist_col.key]              # value of             # attribute based on mapperproperty related             # mapped column.  allow usage of mapperproperties             # have different keyname of mapped column.             try:                 prop = obj_mapper.get_property_by_column(obj_col)             except unmappedcolumnerror:                 # in case of single table inheritance, there may                 # columns on mapped table intended subclass only.                 # "unmapped" status of subclass column on                 # base class feature of declarative module of sqla 0.5.2.                 continue              # expired object attributes , deferred cols might not in             # dict.  force load no matter using getattr().             if prop.key not in obj_state.dict:                 getattr(obj, prop.key)              a, u, d = attributes.get_history(obj, prop.key)              if d:                 attr[hist_col.key] = d[0]                 obj_changed = true             elif u:                 attr[hist_col.key] = u[0]             else:                 # if attribute had no value.                 attr[hist_col.key] = a[0]                 obj_changed = true      if not obj_changed:         # not changed, have relationships.  ok         # check         prop in obj_mapper.iterate_properties:             if isinstance(prop, relationshipproperty) , \                 attributes.get_history(obj, prop.key).has_changes():                 obj_changed = true                 break      if not obj_changed , not deleted:         return      attr['version_datetime'] = obj.version_datetime     attr['version_userid'] = obj.version_userid     attr['version_deleted'] = obj.version_deleted     hist = history_cls()     key, value in attr.items():         setattr(hist, key, value)     session.add(hist)     obj.version_datetime = datetime.now()     obj.version_userid = getattr(session, 'userid', none)     obj.version_deleted = deleted  def versioned_session(session):     @event.listens_for(session, 'before_flush')     def before_flush(session, flush_context, instances):         obj in versioned_objects(session.deleted):             create_version(obj, session, deleted = true)         obj in versioned_objects(session.dirty):             create_version(obj, session)  def add_userid_to_session(userid, session):     if isinstance(session, scoping.scoped_session):         thread_local_session = session.registry()         thread_local_session.userid = userid     elif isinstance(session, session):         session.userid = userid     else:         raise typeerror("not sure how add userid session of type {}".format(type(session)))  

and here's how i'm using (all non-essential parts have been cut out):

base = declarative_base()  class user(versioned, base):     __tablename__ = 'user'     login = column(string(60), primary_key=true, nullable=false)      groups = association_proxy('user_to_groups', 'group', creator=lambda group: usertogroup(group_name=group.name))      def __init__(self, login, groups=none):         self.login = login         if groups:             group in groups:                 self.groups.append(group)  class group(versioned, base):     __tablename__ = 'group'     name = column(string(100), primary_key=true, nullable=false)     description = column(string(100), nullable=true)      users = association_proxy('group_to_user', 'user', creator=lambda user: usertogroup(user_login=user.login))      def __eq__(self, other):         return self.name == other.name  class usertogroup(versioned, base):     __tablename__ = 'user_to_group'     user_login = column(string(60), foreignkey(user.login), primary_key=true)     group_name = column(string(100), foreignkey(group.name), primary_key=true)      user = relationship(user, backref=backref('user_to_groups', cascade='all, delete-orphan'))     group = relationship(group, backref=backref('group_to_user', cascade='all, delete-orphan'))   session.configure(bind=engine) add_userid_to_session("test", session.registry()) versioned_session(session) user = session.query(user).filter(user.login=='test').one() user.groups.remove(group(name ="g:admin")) 

before running code database has 1 user called 'test' , 2 groups user attached called 'g:admin' , 'g:superadmin'.

what is: copy existing user_to_group entry 'test' => 'g:admin' mapping , copy history table. delete entry user_to_group.

what i'd copy value history table , update entry in user_to_group have version_deleted set true.

i'm thinking way snatch entry out of session.deleted (that's why changed order original code) , modify put session.dirty. i'm not sure "safest" way of doing this.

another issue (which require question) how detect relationships covered in table system makes copy of 'user' row history table , updates version information despite no real changes being made row.

edit: i've decided things bit differently, still have problem... instead of having "deleted" flag in live tables delete content , record history item indicating when deletion occurred. if i'm deleting object directly works correctly. if delete object off of relationship i'm not able properly. delete get's issued relationship table remove link, can't seem figure out how detect deletion in "create_version" method.

for example, if do:

group = session.query(group).filter(group.name=='g:admin').one() group.users.remove(group.users[0]) 

no objects placed in session.deleted. can detect sort of deletion via attributes.get_history(obj, prop.key), seems indicate deletion of usertogroup object group (which want detect , record history item on), indicates deletion of group usertogroup object (which don't want because actual group not being deleted).


Comments

Popular posts from this blog

node.js - Bad Request - node js ajax post -

Why does Ruby on Rails generate add a blank line to the end of a file? -

keyboard - Smiles and long press feature in Android -