#
# 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
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
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)
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
# 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()
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"))
'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.
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)
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")
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(
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)
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-"
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__,
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)
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(