Migrate Activity to using the new GenericForeignKey
authorJessica Tallon <jessica@megworld.co.uk>
Mon, 2 Mar 2015 13:27:52 +0000 (14:27 +0100)
committerJessica Tallon <jessica@megworld.co.uk>
Tue, 26 May 2015 14:48:58 +0000 (16:48 +0200)
mediagoblin/db/migrations.py
mediagoblin/db/models.py

index 446f30df6ab586dea6753da6e8b103d915ae11c6..8661c95a081a1c516743d1e1b4f7a4cdd8191267 100644 (file)
@@ -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()
+
+
+
+
+
 
index 97f8b398dd6aedc8afd1e0b76130ed695a1ae0ef..4b59279223ae07ec284ff6cd5096951db841cb92 100644 (file)
@@ -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()