Add the __model_args__ deletion code
authorJessica Tallon <tsyesika@tsyesika.se>
Thu, 1 Oct 2015 11:23:33 +0000 (13:23 +0200)
committerJessica Tallon <tsyesika@tsyesika.se>
Wed, 7 Oct 2015 12:40:44 +0000 (14:40 +0200)
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
mediagoblin/db/migrations.py
mediagoblin/db/models.py

index 6acb0b790372f25a73975a86550e78b64f26a7b2..a425343bb4c3404f518de23f48f258265f2f363e 100644 (file)
@@ -13,7 +13,7 @@
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
+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
index 4e9d3a2a99045587a4f122992de909aa035d6bf9..019eb338166f6200092981d5bb591d2249ea585e 100644 (file)
@@ -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()
index 9f4a144cfa887fd77d2b7677e33afc6314f36b85..d7ddc55d11b6c3c968ccbde88f57e89d61e86f08 100644 (file)
@@ -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(