Migrate Activity to using the new GenericForeignKey

This commit is contained in:
Jessica Tallon 2015-03-02 14:27:52 +01:00
parent 641ae2f1e1
commit bfe1e8ce88
2 changed files with 166 additions and 47 deletions

View File

@ -36,7 +36,7 @@ from mediagoblin.db.extratypes import JSONEncoded, MutationDict
from mediagoblin.db.migration_tools import ( from mediagoblin.db.migration_tools import (
RegisterMigration, inspect_table, replace_table_hack) RegisterMigration, inspect_table, replace_table_hack)
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User, from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
Privilege, Generator) Privilege, Generator, GenericForeignKey)
from mediagoblin.db.extratypes import JSONEncoded, MutationDict from mediagoblin.db.extratypes import JSONEncoded, MutationDict
@ -910,6 +910,14 @@ class ActivityIntermediator_R0(declarative_base()):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
type = Column(Unicode, nullable=False) 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()): class Activity_R0(declarative_base()):
__tablename__ = "core__activities" __tablename__ = "core__activities"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
@ -927,6 +935,7 @@ class Activity_R0(declarative_base()):
ForeignKey(ActivityIntermediator_R0.id), ForeignKey(ActivityIntermediator_R0.id),
nullable=True) nullable=True)
@RegisterMigration(24, MIGRATIONS) @RegisterMigration(24, MIGRATIONS)
def activity_migration(db): def activity_migration(db):
""" """
@ -1250,6 +1259,12 @@ def datetime_to_utc(db):
# Commit this to the database # Commit this to the database
db.commit() 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()): class GenericModelReference_V0(declarative_base()):
__tablename__ = "core__generic_model_reference" __tablename__ = "core__generic_model_reference"
@ -1263,4 +1278,128 @@ def create_generic_model_reference(db):
GenericModelReference_V0.__table__.create(db.bind) GenericModelReference_V0.__table__.create(db.bind)
db.commit() 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()

View File

@ -63,8 +63,7 @@ class GenericModelReference(Base):
# This will be the tablename of the model # This will be the tablename of the model
model_type = Column(Unicode, nullable=False) model_type = Column(Unicode, nullable=False)
@property def get_object(self):
def get(self):
# This can happen if it's yet to be saved # This can happen if it's yet to be saved
if self.model_type is None or self.obj_pk is None: if self.model_type is None or self.obj_pk is None:
return None return None
@ -72,8 +71,7 @@ class GenericModelReference(Base):
model = self._get_model_from_type(self.model_type) model = self._get_model_from_type(self.model_type)
return model.query.filter_by(id=self.obj_pk) return model.query.filter_by(id=self.obj_pk)
@property def set_object(self, obj):
def set(self, obj):
model = obj.__class__ model = obj.__class__
# Check we've been given a object # Check we've been given a object
@ -118,11 +116,31 @@ class GenericForeignKey(ForeignKey):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(GenericForeignKey, self).__init__( super(GenericForeignKey, self).__init__(
"core__generic_model_reference.id", GenericModelReference.id,
*args, *args,
**kwargs **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): class Location(Base):
""" Represents a physical location """ """ Represents a physical location """
__tablename__ = "core__locations" __tablename__ = "core__locations"
@ -1414,10 +1432,10 @@ class Activity(Base, ActivityMixin):
ForeignKey("core__generators.id"), ForeignKey("core__generators.id"),
nullable=True) nullable=True)
object = Column(Integer, object = Column(Integer,
ForeignKey("core__activity_intermediators.id"), GenericForeignKey(),
nullable=False) nullable=False)
target = Column(Integer, target = Column(Integer,
ForeignKey("core__activity_intermediators.id"), GenericForeignKey(),
nullable=True) nullable=True)
get_actor = relationship(User, get_actor = relationship(User,
@ -1437,44 +1455,6 @@ class Activity(Base, ActivityMixin):
content=self.content 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): def save(self, set_updated=True, *args, **kwargs):
if set_updated: if set_updated:
self.updated = datetime.datetime.now() self.updated = datetime.datetime.now()