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()
This commit is contained in:
Jessica Tallon 2015-10-01 13:23:33 +02:00
parent 0f3bf8d4b1
commit 30852fda1c
3 changed files with 148 additions and 2 deletions

View 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

View 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()

View 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(