From 30852fda1c119e6031914b983920898cd57d5aa9 Mon Sep 17 00:00:00 2001 From: Jessica Tallon Date: Thu, 1 Oct 2015 13:23:33 +0200 Subject: [PATCH] Add the __model_args__ deletion code This adds the "deleted" fields to the models as well as a new __model_args__ section whcih supports the option for changing the deletion type. Deletion is now handled by choosing a deletion method based on the __model_args__["deletion"] setting, for example if it's soft deletion it will call Model.soft_delete() --- mediagoblin/db/base.py | 54 ++++++++++++++++++++++++++++- mediagoblin/db/migrations.py | 67 ++++++++++++++++++++++++++++++++++++ mediagoblin/db/models.py | 29 +++++++++++++++- 3 files changed, 148 insertions(+), 2 deletions(-) diff --git a/mediagoblin/db/base.py b/mediagoblin/db/base.py index 6acb0b79..a425343b 100644 --- a/mediagoblin/db/base.py +++ b/mediagoblin/db/base.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - +import datetime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import inspect @@ -26,6 +26,16 @@ if not DISABLE_GLOBALS: class GMGTableBase(object): + # Deletion types + HARD_DELETE = "hard-deletion" + SOFT_DELETE = "soft-deletion" + + __default_model_args__ = { + "deletion": HARD_DELETE, + "soft_deletion_field": "deleted", + "soft_deletion_retain": ("id",) + } + @property def _session(self): return inspect(self).session @@ -37,6 +47,11 @@ class GMGTableBase(object): if not DISABLE_GLOBALS: query = Session.query_property() + def get_model_arg(self, argument): + model_args = self.__default_model_args__.copy() + model_args.update(getattr(self, "__model_args__", {})) + return model_args.get(argument) + def get(self, key): return getattr(self, key) @@ -56,6 +71,43 @@ class GMGTableBase(object): sess.flush() def delete(self, commit=True): + """ Delete the object either using soft or hard deletion """ + if self.get_model_arg("deletion") == self.HARD_DELETE: + return self.hard_delete(commit) + elif self.get_model_arg("deletion") == self.SOFT_DELETE: + return self.soft_delete(commit) + else: + raise ValueError( + "__model_args__['deletion'] is an invalid value %s" % ( + self.get_model_arg("deletion") + )) + + def soft_delete(self, commit): + # Find the deletion field + field_name = self.get_model_arg("soft_deletion_field") + + # We can't use self.__table__.columns as it only shows it of the + # current model and no parent if polymorphism is being used. This + # will cause problems for example for the User model. + if field_name not in dir(type(self)): + raise ValueError("Cannot find soft_deletion_field") + + # Store a value in the deletion field + setattr(self, field_name, datetime.datetime.utcnow()) + + # Iterate through the fields and remove data + retain_fields = self.get_model_arg("soft_deletion_retain") + for field_name in self.__table__.columns.keys(): + # should we skip this field? + if field_name in retain_fields: + continue + + setattr(self, field_name, None) + + # Save the changes + self.save(commit) + + def hard_delete(self, commit): """Delete the object and commit the change immediately by default""" sess = self._session assert sess is not None, "Not going to delete detached %r" % self diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 4e9d3a2a..019eb338 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -1820,3 +1820,70 @@ def federation_actor(db): # commit changes to db. db.commit() + +@RegisterMigration(39, MIGRATIONS) +def federation_soft_deletion(db): + """ Introduces soft deletion to models + + This adds a deleted DateTime column which represents if the model is + deleted and if so, when. With this change comes changes on the models + that soft delete the models rather than the previous hard deletion. + """ + metadata = MetaData(bind=db.bind) + + # User Model + user_table = inspect_table(metadata, "core__users") + user_deleted_column = Column( + "deleted", + DateTime, + nullable=True + ) + user_deleted_column.create(user_table) + + # MediaEntry + media_entry_table = inspect_table(metadata, "core__media_entries") + me_deleted_column = Column( + "deleted", + DateTime, + nullable=True + ) + me_deleted_column.create(media_entry_table) + + # MediaComment + media_comment_table = inspect_table(metadata, "core__media_comments") + mc_deleted_column = Column( + "deleted", + DateTime, + nullable=True + ) + mc_deleted_column.create(media_comment_table) + + # Collection + collection_table = inspect_table(metadata, "core__collections") + collection_deleted_column = Column( + "deleted", + DateTime, + nullable=True + ) + collection_deleted_column.create(collection_table) + + # Generator + generator_table = inspect_table(metadata, "core__generators") + generator_deleted_column = Column( + "deleted", + DateTime, + nullable=True + ) + generator_deleted_column.create(generator_table) + + # Activity + activity_table = inspect_table(metadata, "core__activities") + activity_deleted_column = Column( + "deleted", + DateTime, + nullable=True + ) + activity_deleted_column.create(activity_table) + + # Commit changes to the db + db.commit() diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 9f4a144c..d7ddc55d 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -243,6 +243,7 @@ class User(Base, UserMixin): created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + deleted = Column(DateTime, nullable=True) location = Column(Integer, ForeignKey("core__locations.id")) @@ -254,6 +255,10 @@ class User(Base, UserMixin): 'polymorphic_on': type, } + __model_args__ = { + 'deletion': Base.SOFT_DELETE, + } + def delete(self, **kwargs): """Deletes a User and all related entries/comments/files/...""" # Collections get deleted by relationships. @@ -516,6 +521,7 @@ class MediaEntry(Base, MediaEntryMixin): created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow, index=True) updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + deleted = Column(DateTime, nullable=True) fail_error = Column(Unicode) fail_metadata = Column(JSONEncoded) @@ -909,6 +915,7 @@ class MediaComment(Base, MediaCommentMixin): Integer, ForeignKey(MediaEntry.id), nullable=False, index=True) actor = Column(Integer, ForeignKey(User.id), nullable=False) created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + deleted = Column(DateTime, nullable=True) content = Column(UnicodeText, nullable=False) location = Column(Integer, ForeignKey("core__locations.id")) get_location = relationship("Location", lazy="joined") @@ -934,6 +941,10 @@ class MediaComment(Base, MediaCommentMixin): lazy="dynamic", cascade="all, delete-orphan")) + __model_args__ = { + "deletion": Base.SOFT_DELETE, + } + def serialize(self, request): """ Unserialize to python dictionary for API """ href = request.urlgen( @@ -1010,6 +1021,7 @@ class Collection(Base, CollectionMixin): created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow, index=True) updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + deleted = Column(DateTime, nullable=True) description = Column(UnicodeText) actor = Column(Integer, ForeignKey(User.id), nullable=False) num_items = Column(Integer, default=0) @@ -1028,9 +1040,13 @@ class Collection(Base, CollectionMixin): backref=backref("collections", cascade="all, delete-orphan")) __table_args__ = ( - UniqueConstraint('actor', 'slug'), + UniqueConstraint("actor", "slug"), {}) + __model_args__ = { + "delete": Base.SOFT_DELETE, + } + # These are the types, It's strongly suggested if new ones are invented they # are prefixed to ensure they're unique from other types. Any types used in # the main mediagoblin should be prefixed "core-" @@ -1422,8 +1438,13 @@ class Generator(Base): name = Column(Unicode, nullable=False) published = Column(DateTime, default=datetime.datetime.utcnow) updated = Column(DateTime, default=datetime.datetime.utcnow) + deleted = Column(DateTime, nullable=True) object_type = Column(Unicode, nullable=False) + __model_args__ = { + "deletion": Base.SOFT_DELETE, + } + def __repr__(self): return "<{klass} {name}>".format( klass=self.__class__.__name__, @@ -1464,6 +1485,8 @@ class Activity(Base, ActivityMixin): nullable=False) published = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + deleted = Column(DateTime, nullable=True) + verb = Column(Unicode, nullable=False) content = Column(Unicode, nullable=True) title = Column(Unicode, nullable=True) @@ -1488,6 +1511,10 @@ class Activity(Base, ActivityMixin): cascade="all, delete-orphan")) get_generator = relationship(Generator) + __model_args__ = { + "deletion": Base.SOFT_DELETE, + } + def __repr__(self): if self.content is None: return "<{klass} verb:{verb}>".format( -- 2.25.1