From bfe1e8ce880d3ea30c24bb1c6840126f5a50638d Mon Sep 17 00:00:00 2001 From: Jessica Tallon Date: Mon, 2 Mar 2015 14:27:52 +0100 Subject: [PATCH] Migrate Activity to using the new GenericForeignKey --- mediagoblin/db/migrations.py | 141 ++++++++++++++++++++++++++++++++++- mediagoblin/db/models.py | 72 +++++++----------- 2 files changed, 166 insertions(+), 47 deletions(-) diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 446f30df..8661c95a 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -36,7 +36,7 @@ from mediagoblin.db.extratypes import JSONEncoded, MutationDict from mediagoblin.db.migration_tools import ( RegisterMigration, inspect_table, replace_table_hack) from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User, - Privilege, Generator) + Privilege, Generator, GenericForeignKey) from mediagoblin.db.extratypes import JSONEncoded, MutationDict @@ -910,6 +910,14 @@ class ActivityIntermediator_R0(declarative_base()): id = Column(Integer, primary_key=True) type = Column(Unicode, nullable=False) + # These are needed for migration 29 + TYPES = { + "user": User, + "media": MediaEntry, + "comment": MediaComment, + "collection": Collection, + } + class Activity_R0(declarative_base()): __tablename__ = "core__activities" id = Column(Integer, primary_key=True) @@ -927,6 +935,7 @@ class Activity_R0(declarative_base()): ForeignKey(ActivityIntermediator_R0.id), nullable=True) + @RegisterMigration(24, MIGRATIONS) def activity_migration(db): """ @@ -1250,6 +1259,12 @@ def datetime_to_utc(db): # Commit this to the database db.commit() +## +# Migrations to handle migrating from activity specific foreign key to the +# new GenericForeignKey implementations. They have been split up to improve +# readability and minimise errors +## + class GenericModelReference_V0(declarative_base()): __tablename__ = "core__generic_model_reference" @@ -1263,4 +1278,128 @@ def create_generic_model_reference(db): GenericModelReference_V0.__table__.create(db.bind) db.commit() +@RegisterMigration(28, MIGRATIONS) +def add_foreign_key_fields(db): + """ + Add the fields for GenericForeignKey to the model under temporary name, + this is so that later a data migration can occur. They will be renamed to + the origional names. + """ + metadata = MetaData(bind=db.bind) + activity_table = inspect_table(metadata, "core__activities") + + # Create column and add to model. + object_column = Column("temp_object", Integer, GenericForeignKey()) + object_column.create(activity_table) + + target_column = Column("temp_target", Integer, GenericForeignKey()) + target_column.create(activity_table) + + # Commit this to the database + db.commit() + +@RegisterMigration(29, MIGRATIONS) +def migrate_data_foreign_keys(db): + """ + This will migrate the data from the old object and target attributes which + use the old ActivityIntermediator to the new temparay fields which use the + new GenericForeignKey. + """ + metadata = MetaData(bind=db.bind) + activity_table = inspect_table(metadata, "core__activities") + ai_table = inspect_table(metadata, "core__activity_intermediators") + gmr_table = inspect_table(metadata, "core__generic_model_reference") + + + # Iterate through all activities doing the migration per activity. + for activity in db.execute(activity_table.select()): + # First do the "Activity.object" migration to "Activity.temp_object" + # I need to get the object from the Activity, I can't use the old + # Activity.get_object as we're in a migration. + object_ai = db.execute(ai_table.select( + ai_table.c.id==activity.object + )).first() + + object_ai_type = ActivityIntermediator_R0.TYPES[object_ai.type] + object_ai_table = inspect_table(metadata, object_ai_type.__tablename__) + + activity_object = db.execute(object_ai_table.select( + object_ai_table.c.activity==object_ai.id + )).first() + + # now we need to create the GenericModelReference + object_gmr = db.execute(gmr_table.insert().values( + obj_pk=activity_object.id, + model_type=object_ai_type.__tablename__ + )) + + # Now set the ID of the GenericModelReference in the GenericForignKey + db.execute(activity_table.update().values( + temp_object=object_gmr.inserted_primary_key[0] + )) + + # Now do same process for "Activity.target" to "Activity.temp_target" + # not all Activities have a target so if it doesn't just skip the rest + # of this. + if activity.target is None: + continue + + # Now get the target for the activity. + target_ai = db.execute(ai_table.select( + ai_table.c.id==activity.target + )).first() + + target_ai_type = ActivityIntermediator_R0.TYPES[target_ai.type] + target_ai_table = inspect_table(metadata, target_ai_type.__tablename__) + + activity_target = db.execute(target_ai_table.select( + target_ai_table.c.activity==target_ai.id + )).first() + + # We now want to create the new target GenericModelReference + target_gmr = db.execute(gmr_table.insert().values( + obj_pk=activity_target.id, + model_type=target_ai_type.__tablename__ + )) + + # Now set the ID of the GenericModelReference in the GenericForignKey + db.execute(activity_table.update().values( + temp_object=target_gmr.inserted_primary_key[0] + )) + + # Commit to the database. + db.commit() + +@RegisterMigration(30, MIGRATIONS) +def rename_and_remove_object_and_target(db): + """ + Renames the new Activity.object and Activity.target fields and removes the + old ones. + """ + metadata = MetaData(bind=db.bind) + activity_table = inspect_table(metadata, "core__activities") + + # Firstly lets remove the old fields. + old_object_column = activity_table.columns["object"] + old_target_column = activity_table.columns["target"] + + # Drop the tables. + old_object_column.drop() + old_target_column.drop() + + # Now get the new columns. + new_object_column = activity_table.columns["temp_object"] + new_target_column = activity_table.columns["temp_target"] + + # rename them to the old names. + new_object_column.alter(name="object") + new_target_column.alter(name="target") + + # Commit the changes to the database. + db.commit() + + + + + diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 97f8b398..4b592792 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -53,7 +53,7 @@ class GenericModelReference(Base): Represents a relationship to any model that is defined with a integer pk NB: This model should not be used directly but through the GenericForeignKey - field provided. + field provided. """ __tablename__ = "core__generic_model_reference" @@ -63,8 +63,7 @@ class GenericModelReference(Base): # This will be the tablename of the model model_type = Column(Unicode, nullable=False) - @property - def get(self): + def get_object(self): # This can happen if it's yet to be saved if self.model_type is None or self.obj_pk is None: return None @@ -72,8 +71,7 @@ class GenericModelReference(Base): model = self._get_model_from_type(self.model_type) return model.query.filter_by(id=self.obj_pk) - @property - def set(self, obj): + def set_object(self, obj): model = obj.__class__ # Check we've been given a object @@ -118,11 +116,31 @@ class GenericForeignKey(ForeignKey): def __init__(self, *args, **kwargs): super(GenericForeignKey, self).__init__( - "core__generic_model_reference.id", + GenericModelReference.id, *args, **kwargs ) + def __get__(self, *args, **kwargs): + """ Looks up GenericModelReference and model for field """ + # Find the value of the foreign key. + ref = super(self, GenericForeignKey).__get__(*args, **kwargs) + + # If this hasn't been set yet return None + if ref is None: + return None + + # Look up the GenericModelReference for this. + gmr = GenericModelReference.query.filter_by(id=ref).first() + + # If it's set to something invalid (i.e. no GMR exists return None) + if gmr is None: + return None + + # Ask the GMR for the corresponding model + return gmr.get_object() + + class Location(Base): """ Represents a physical location """ __tablename__ = "core__locations" @@ -1414,10 +1432,10 @@ class Activity(Base, ActivityMixin): ForeignKey("core__generators.id"), nullable=True) object = Column(Integer, - ForeignKey("core__activity_intermediators.id"), + GenericForeignKey(), nullable=False) target = Column(Integer, - ForeignKey("core__activity_intermediators.id"), + GenericForeignKey(), nullable=True) get_actor = relationship(User, @@ -1437,44 +1455,6 @@ class Activity(Base, ActivityMixin): content=self.content ) - @property - def get_object(self): - if self.object is None: - return None - - ai = ActivityIntermediator.query.filter_by(id=self.object).first() - return ai.get() - - def set_object(self, obj): - self.object = self._set_model(obj) - - @property - def get_target(self): - if self.target is None: - return None - - ai = ActivityIntermediator.query.filter_by(id=self.target).first() - return ai.get() - - def set_target(self, obj): - self.target = self._set_model(obj) - - def _set_model(self, obj): - # Firstly can we set obj - if not hasattr(obj, "activity"): - raise ValueError( - "{0!r} is unable to be set on activity".format(obj)) - - if obj.activity is None: - # We need to create a new AI - ai = ActivityIntermediator() - ai.set(obj) - ai.save() - return ai.id - - # Okay we should have an existing AI - return ActivityIntermediator.query.filter_by(id=obj.activity).first().id - def save(self, set_updated=True, *args, **kwargs): if set_updated: self.updated = datetime.datetime.now() -- 2.25.1