def make_audit_table(table, metadata): """Create an audit log table for an archetype. """ audit_tbl_name = naming.audit_table_name(table.name) audit_columns = get_audit_table_columns(table) audit_tbl = sa.Table(audit_tbl_name, metadata, *audit_columns, useexisting=False ) return audit_tbl
def downgrade(): def drop_audit_table(audit_tbl_name): print "Dropping audit table:", audit_tbl_name op.drop_table(audit_tbl_name) drop_audit_table(naming.audit_table_name("group")) drop_audit_table(naming.audit_table_name("member")) drop_audit_table(naming.audit_table_name("doc_principal")) item_vote_tbl_columns = [ sa.Column("vote_id", sa.Integer, primary_key=True), sa.Column( "item_id", sa.Integer, # !+RENAME doc_id sa.ForeignKey("doc.doc_id"), nullable=False), sa.Column("date", sa.Date), sa.Column("affirmative_vote", sa.Integer), sa.Column("negative_vote", sa.Integer), sa.Column("remarks", sa.UnicodeText), sa.Column("language", sa.String(5), nullable=False), ] item_member_vote_tbl_columns = [ sa.Column("vote_id", sa.Integer, sa.ForeignKey("item_vote.vote_id"), primary_key=True, nullable=False), sa.Column("member_id", sa.Integer, sa.ForeignKey("user.user_id"), primary_key=True, nullable=False), sa.Column("vote", sa.Boolean), ] print "Re-create table: item_vote" op.create_table("item_vote", *item_vote_tbl_columns) print "Re-create table: item_member_vote" op.create_table("item_member_vote", *item_member_vote_tbl_columns) print "Re-adding unique constraint on table column: debate_record_audit.sitting_id" op.create_unique_constraint("debate_record_audit_sitting_id_key", "debate_record_audit", ["sitting_id"])
def downgrade(): def drop_audit_table(audit_tbl_name): print "Dropping audit table:", audit_tbl_name op.drop_table(audit_tbl_name) drop_audit_table(naming.audit_table_name("group")) drop_audit_table(naming.audit_table_name("member")) drop_audit_table(naming.audit_table_name("doc_principal")) item_vote_tbl_columns = [ sa.Column("vote_id", sa.Integer, primary_key=True), sa.Column("item_id", sa.Integer, # !+RENAME doc_id sa.ForeignKey("doc.doc_id"), nullable=False ), sa.Column("date", sa.Date), sa.Column("affirmative_vote", sa.Integer), sa.Column("negative_vote", sa.Integer), sa.Column("remarks", sa.UnicodeText), sa.Column("language", sa.String(5), nullable=False), ] item_member_vote_tbl_columns = [ sa.Column("vote_id", sa.Integer, sa.ForeignKey("item_vote.vote_id"), primary_key=True, nullable=False ), sa.Column("member_id", sa.Integer, sa.ForeignKey("user.user_id"), primary_key=True, nullable=False ), sa.Column("vote", sa.Boolean), ] print "Re-create table: item_vote" op.create_table("item_vote", *item_vote_tbl_columns) print "Re-create table: item_member_vote" op.create_table("item_member_vote", *item_member_vote_tbl_columns) print "Re-adding unique constraint on table column: debate_record_audit.sitting_id" op.create_unique_constraint("debate_record_audit_sitting_id_key", "debate_record_audit", ["sitting_id"])
def get_audit_table_columns(table): """Derive the columns of the audit table from the table being audited. """ entity_name = table.name audit_tbl_name = naming.audit_table_name(entity_name) # audit-specific columns -- prefix with "audit_" to avoid potential # clashing of column names from table being audited. columns = [ sa.Column("audit_id", sa.Integer, sa.ForeignKey("audit.audit_id"), primary_key=True), ] def extend_cols(cols, ext_cols): names = [ c.name for c in cols ] for c in ext_cols: assert c.name not in names, "Duplicate column [%s]." % (c.name) names.append(c.name) if c.primary_key: # PK columns on auditable table become FK columns on audit table if len(table.primary_key) == 1: # single-column PK - the id column of the "owning" object for # which the change is being logged; we always retain the same # original column name i.e. doc_id for case of "doc", and have # the audit_head_id property always read and write to this. assert c.name == "%s_id" % (entity_name), \ "Inconsistent PK column naming [%s != %s]" % ( "%s_id" % (entity_name), c.name) else: # composite PK log.debug("Table %r -> skipping pk column %r name " "constraint check for multi-column PK: %s", audit_tbl_name, c.name, table.primary_key.columns) # add the column, corresponding ForeignKeyConstraint added at end cols.append(sa.Column(c.name, c.type, nullable=False, index=True)) # !+FK columns may specify type as None (not c.type), to let # auto detection of the type from that of the FK col else: # !+ should special ext col constraints NOT be carried over # e.g. default value on ext, not/nullable on ext...? cols.append(c.copy()) # auditable "unique" columns may NOT be unique in the audit table! if cols[-1].unique: cols[-1].unique = False extend_cols(columns, table.columns) # add ForeignKeyConstraint corresponding to original PK pk_col_names = [ c.name for c in table.primary_key.columns ] columns.append( sa.ForeignKeyConstraint(pk_col_names, [ "%s.%s" % (entity_name, name) for name in pk_col_names ])) # !+additional tables... return columns
def make_audit_table(tbl_name): tbl = getattr(schema, tbl_name) audit_tbl_name = naming.audit_table_name(tbl_name) audit_tbl_columns = schema.get_audit_table_columns(tbl) print "Creating audit table:", audit_tbl_name op.create_table(audit_tbl_name, *audit_tbl_columns)
def decorate_model(self, model): # Assumption: if a domain class is explicitly pre-defined, then it is # assumed that all necessary setup is also taken care of. # Typically, only the sub-classes of an archetype (mapped to a same # table) need dynamic creation/setup. def get_audit_class_for(auditable_class): audit_cls_name = "%sAudit" % (auditable_class.__name__) return getattr(MODEL_MODULE, audit_cls_name, None) def get_base_audit_class(model): """Identify what should be the BASE audit class for a {model}Audit class to inherit from, and return it. """ assert interfaces.IFeatureAudit.implementedBy(model), model ti = capi.get_type_info(model) if ti.archetype is None: # !+ should this be allowed to ever happen? # i.e. require each type to declare an archetype? base_audit_class = domain.Audit else: base_audit_class = get_audit_class_for(ti.archetype) if base_audit_class is None: # fallback to get the audit class for the sys archetype base_audit_class = get_audit_class_for(ti.sys_archetype) assert base_audit_class is not None, (model, ti.archetype, base_audit_class) return base_audit_class def new_audit_class(model): """Create, set on MODEL_MODULE, and map {model}Audit class. """ base_audit_cls = get_base_audit_class(model) audit_cls = base_audit_cls.auditFactory(model) # set on MODEL_MODULE setattr(MODEL_MODULE, audit_cls.__name__, audit_cls) # mapper for newly created audit_cls mapper(audit_cls, inherits=base_audit_cls, polymorphic_identity=naming.polymorphic_identity(model)) log.info("GENERATED new_audit_class %s(%s) for type %s", audit_cls, base_audit_cls, model) return audit_cls # domain - audit class audit_cls = get_audit_class_for(model) if audit_cls is None: audit_cls = new_audit_class(model) # auditor - head cls import bungeni.core.audit bungeni.core.audit.set_auditor(model) # mapper - audit class # assumption: audit_cls uses single inheritance only (and not only for # those created dynamically in feature_audit()) base_audit_cls = audit_cls.__bases__[0] assert issubclass(base_audit_cls, domain.Audit), \ "Audit class %s is not a proper subclass of %s" % ( audit_cls, domain.Audit) # extended attributes - propagate any on head cls also to its audit_cls for vp_name, vp_type in model.extended_properties: mapper_add_relation_vertical_property(audit_cls, vp_name, vp_type) # !+NOTE: capi.get_type_info(model).descriptor_model is still None # model.changes <-> change.audit.audit_head=doc: # doc[@TYPE] <-- TYPE_audit <-> audit <-> change # get head table for kls, and its audit table. tbl = utils.get_local_table(model) # NOT mapped_table, as when cls_mapper.single=False (e.g. for # the case of the group type) it gves an sa.sql.expression.Join, # and not a table object: # principal JOIN "group" ON principal.principal_id = "group".group_id audit_tbl = getattr(schema, naming.audit_table_name(tbl.name)) cls_mapper = class_mapper(model) cls_mapper.add_property( "changes", relation( domain.Change, # join condition, may be determined by a composite primary key primaryjoin=sa.and_(*[ pk_col == audit_tbl.c.get(pk_col.name) for pk_col in tbl.primary_key ]), secondary=audit_tbl, secondaryjoin=sa.and_( audit_tbl.c.audit_id == schema.change.c.audit_id, ), lazy=True, order_by=schema.change.c.audit_id.desc(), cascade="all", passive_deletes=False, # SA default ))
def decorate_model(self, model): # Assumption: if a domain class is explicitly pre-defined, then it is # assumed that all necessary setup is also taken care of. # Typically, only the sub-classes of an archetype (mapped to a same # table) need dynamic creation/setup. def get_audit_class_for(auditable_class): audit_cls_name = "%sAudit" % (auditable_class.__name__) return getattr(MODEL_MODULE, audit_cls_name, None) def get_base_audit_class(model): """Identify what should be the BASE audit class for a {model}Audit class to inherit from, and return it. """ assert interfaces.IFeatureAudit.implementedBy(model), model ti = capi.get_type_info(model) if ti.archetype is None: # !+ should this be allowed to ever happen? # i.e. require each type to declare an archetype? base_audit_class = domain.Audit else: base_audit_class = get_audit_class_for(ti.archetype) if base_audit_class is None: # fallback to get the audit class for the sys archetype base_audit_class = get_audit_class_for(ti.sys_archetype) assert base_audit_class is not None, (model, ti.archetype, base_audit_class) return base_audit_class def new_audit_class(model): """Create, set on MODEL_MODULE, and map {model}Audit class. """ base_audit_cls = get_base_audit_class(model) audit_cls = base_audit_cls.auditFactory(model) # set on MODEL_MODULE setattr(MODEL_MODULE, audit_cls.__name__, audit_cls) # mapper for newly created audit_cls mapper(audit_cls, inherits=base_audit_cls, polymorphic_identity=naming.polymorphic_identity(model) ) log.info("GENERATED new_audit_class %s(%s) for type %s", audit_cls, base_audit_cls, model) return audit_cls # domain - audit class audit_cls = get_audit_class_for(model) if audit_cls is None: audit_cls = new_audit_class(model) # auditor - head cls import bungeni.core.audit bungeni.core.audit.set_auditor(model) # mapper - audit class # assumption: audit_cls uses single inheritance only (and not only for # those created dynamically in feature_audit()) base_audit_cls = audit_cls.__bases__[0] assert issubclass(base_audit_cls, domain.Audit), \ "Audit class %s is not a proper subclass of %s" % ( audit_cls, domain.Audit) # extended attributes - propagate any on head cls also to its audit_cls for vp_name, vp_type in model.extended_properties: mapper_add_relation_vertical_property( audit_cls, vp_name, vp_type) # !+NOTE: capi.get_type_info(model).descriptor_model is still None # model.changes <-> change.audit.audit_head=doc: # doc[@TYPE] <-- TYPE_audit <-> audit <-> change # get head table for kls, and its audit table. tbl = utils.get_local_table(model) # NOT mapped_table, as when cls_mapper.single=False (e.g. for # the case of the group type) it gves an sa.sql.expression.Join, # and not a table object: # principal JOIN "group" ON principal.principal_id = "group".group_id audit_tbl = getattr(schema, naming.audit_table_name(tbl.name)) cls_mapper = class_mapper(model) cls_mapper.add_property("changes", relation(domain.Change, # join condition, may be determined by a composite primary key primaryjoin=sa.and_( *[ pk_col == audit_tbl.c.get(pk_col.name) for pk_col in tbl.primary_key ]), secondary=audit_tbl, secondaryjoin=sa.and_( audit_tbl.c.audit_id == schema.change.c.audit_id, ), lazy=True, order_by=schema.change.c.audit_id.desc(), cascade="all", passive_deletes=False, # SA default ))