From 64a456a4e50b03e4fa2b33ceb208e88d2e02fce7 Mon Sep 17 00:00:00 2001 From: Jessica Tallon Date: Tue, 20 Oct 2015 12:24:54 +0000 Subject: Comment changes for federation This adds a new Comment link table that is used to link between some object and then the comment object, which can be more or less any object in Mediagoblin. The MediaComment has been renamed to TextComment as that more aptly describes what it is. There is migrations for these changes. There is also the conslidation of the Report tables into a single Report table, the same with the Notification objects. This is because both of them split out MediaEntry and Comment versions into their own polymorphic versions from a base, this is no longer a meaningful distinction as comments can be anything. --- mediagoblin/db/base.py | 29 ++++- mediagoblin/db/migrations.py | 273 +++++++++++++++++++++++++++++++++++++++- mediagoblin/db/mixin.py | 69 ++++++++++- mediagoblin/db/models.py | 287 +++++++++++++++++++++---------------------- 4 files changed, 501 insertions(+), 157 deletions(-) (limited to 'mediagoblin/db') diff --git a/mediagoblin/db/base.py b/mediagoblin/db/base.py index a62cbebc..11afbcec 100644 --- a/mediagoblin/db/base.py +++ b/mediagoblin/db/base.py @@ -13,6 +13,9 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import six +import copy + from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import inspect @@ -22,6 +25,30 @@ if not DISABLE_GLOBALS: from sqlalchemy.orm import scoped_session, sessionmaker Session = scoped_session(sessionmaker()) +class FakeCursor(object): + + def __init__ (self, cursor, mapper, filter=None): + self.cursor = cursor + self.mapper = mapper + self.filter = filter + + def count(self): + return self.cursor.count() + + def __copy__(self): + # Or whatever the function is named to make + # copy.copy happy? + return FakeCursor(copy.copy(self.cursor), self.mapper, self.filter) + + def __iter__(self): + return six.moves.filter(self.filter, six.moves.map(self.mapper, self.cursor)) + + def __getitem__(self, key): + return self.mapper(self.cursor[key]) + + def slice(self, *args, **kwargs): + r = self.cursor.slice(*args, **kwargs) + return list(six.moves.filter(self.filter, six.moves.map(self.mapper, r))) class GMGTableBase(object): # Deletion types @@ -93,7 +120,7 @@ class GMGTableBase(object): id=self.actor ).first() tombstone.object_type = self.object_type - tombstone.save() + tombstone.save(commit=False) # There will be a lot of places where the GenericForeignKey will point # to the model, we want to remap those to our tombstone. diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 2df06fc0..461b9c0a 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -37,7 +37,7 @@ from mediagoblin.tools import crypto from mediagoblin.db.extratypes import JSONEncoded, MutationDict from mediagoblin.db.migration_tools import ( RegisterMigration, inspect_table, replace_table_hack) -from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User, +from mediagoblin.db.models import (MediaEntry, Collection, Comment, User, Privilege, Generator, LocalUser, Location, Client, RequestToken, AccessToken) from mediagoblin.db.extratypes import JSONEncoded, MutationDict @@ -353,7 +353,7 @@ class CommentNotification_v0(Notification_v0): __tablename__ = 'core__comment_notifications' id = Column(Integer, ForeignKey(Notification_v0.id), primary_key=True) - subject_id = Column(Integer, ForeignKey(MediaComment.id)) + subject_id = Column(Integer, ForeignKey(Comment.id)) class ProcessingNotification_v0(Notification_v0): @@ -542,7 +542,7 @@ class CommentReport_v0(ReportBase_v0): id = Column('id',Integer, ForeignKey('core__reports.id'), primary_key=True) - comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=True) + comment_id = Column(Integer, ForeignKey(Comment.id), nullable=True) class MediaReport_v0(ReportBase_v0): @@ -917,7 +917,7 @@ class ActivityIntermediator_R0(declarative_base()): TYPES = { "user": User, "media": MediaEntry, - "comment": MediaComment, + "comment": Comment, "collection": Collection, } @@ -1875,3 +1875,268 @@ def add_public_id(db): # Commit this. db.commit() + +class Comment_V0(declarative_base()): + __tablename__ = "core__comment_links" + + id = Column(Integer, primary_key=True) + target_id = Column( + Integer, + ForeignKey(GenericModelReference_V0.id), + nullable=False + ) + comment_id = Column( + Integer, + ForeignKey(GenericModelReference_V0.id), + nullable=False + ) + added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + + +@RegisterMigration(41, MIGRATIONS) +def federation_comments(db): + """ + This reworks the MediaComent to be a more generic Comment model. + """ + metadata = MetaData(bind=db.bind) + textcomment_table = inspect_table(metadata, "core__media_comments") + gmr_table = inspect_table(metadata, "core__generic_model_reference") + + # First of all add the public_id field to the TextComment table + comment_public_id_column = Column( + "public_id", + Unicode, + unique=True + ) + comment_public_id_column.create( + textcomment_table, + unique_name="public_id_unique" + ) + + comment_updated_column = Column( + "updated", + DateTime, + ) + comment_updated_column.create(textcomment_table) + + + # First create the Comment link table. + Comment_V0.__table__.create(db.bind) + db.commit() + + # now look up the comment table + comment_table = inspect_table(metadata, "core__comment_links") + + # Itierate over all the comments and add them to the link table. + for comment in db.execute(textcomment_table.select()): + # Check if there is a GMR to the comment. + comment_gmr = db.execute(gmr_table.select().where(and_( + gmr_table.c.obj_pk == comment.id, + gmr_table.c.model_type == "core__media_comments" + ))).first() + + if comment_gmr: + comment_gmr = comment_gmr[0] + else: + comment_gmr = db.execute(gmr_table.insert().values( + obj_pk=comment.id, + model_type="core__media_comments" + )).inserted_primary_key[0] + + # Get or create the GMR for the media entry + entry_gmr = db.execute(gmr_table.select().where(and_( + gmr_table.c.obj_pk == comment.media_entry, + gmr_table.c.model_type == "core__media_entries" + ))).first() + + if entry_gmr: + entry_gmr = entry_gmr[0] + else: + entry_gmr = db.execute(gmr_table.insert().values( + obj_pk=comment.media_entry, + model_type="core__media_entries" + )).inserted_primary_key[0] + + # Add the comment link. + db.execute(comment_table.insert().values( + target_id=entry_gmr, + comment_id=comment_gmr, + added=datetime.datetime.utcnow() + )) + + # Add the data to the updated field + db.execute(textcomment_table.update().where( + textcomment_table.c.id == comment.id + ).values( + updated=comment.created + )) + db.commit() + + # Add not null constraint + textcomment_update_column = textcomment_table.columns["updated"] + textcomment_update_column.alter(nullable=False) + + # Remove the unused fields on the TextComment model + comment_media_entry_column = textcomment_table.columns["media_entry"] + comment_media_entry_column.drop() + db.commit() + +@RegisterMigration(42, MIGRATIONS) +def consolidate_reports(db): + """ Consolidates the report tables into just one """ + metadata = MetaData(bind=db.bind) + + report_table = inspect_table(metadata, "core__reports") + comment_report_table = inspect_table(metadata, "core__reports_on_comments") + media_report_table = inspect_table(metadata, "core__reports_on_media") + gmr_table = inspect_table(metadata, "core__generic_model_reference") + + # Add the GMR object field onto the base report table + report_object_id_column = Column( + "object_id", + Integer, + ForeignKey(GenericModelReference_V0.id), + ) + report_object_id_column.create(report_table) + db.commit() + + # Iterate through the reports in the comment table and merge them in. + for comment_report in db.execute(comment_report_table.select()): + # Find a GMR for this if one exists. + crgmr = db.execute(gmr_table.select().where(and_( + gmr_table.c.obj_pk == comment_report.comment_id, + gmr_table.c.model_type == "core__media_comments" + ))).first() + + if crgmr: + crgmr = crgmr[0] + else: + crgmr = db.execute(gmr_table.insert().values( + gmr_table.c.obj_pk == comment_report.comment_id, + gmr_table.c.model_type == "core__media_comments" + )).inserted_primary_key[0] + + # Great now we can save this back onto the (base) report. + db.execute(report_table.update().where( + report_table.c.id == comment_report.id + ).values( + object_id=crgmr + )) + + # Iterate through the Media Reports and do the save as above. + for media_report in db.execute(media_report_table.select()): + # Find Mr. GMR :) + mrgmr = db.execute(gmr_table.select().where(and_( + gmr_table.c.obj_pk == media_report.media_entry_id, + gmr_table.c.model_type == "core__media_entries" + ))).first() + + if mrgmr: + mrgmr = mrgmr[0] + else: + mrgmr = db.execute(gmr_table.insert().values( + obj_pk=media_report.media_entry_id, + model_type="core__media_entries" + )).inserted_primary_key[0] + + # Save back on to the base. + db.execute(report_table.update().where( + report_table.c.id == media_report.id + ).values( + object_id=mrgmr + )) + + db.commit() + + # Add the not null constraint + report_object_id = report_table.columns["object_id"] + report_object_id.alter(nullable=False) + + # Now we can remove the fields we don't need anymore + report_type = report_table.columns["type"] + report_type.drop() + + # Drop both MediaReports and CommentTable. + comment_report_table.drop() + media_report_table.drop() + + # Commit we're done. + db.commit() + +@RegisterMigration(43, MIGRATIONS) +def consolidate_notification(db): + """ Consolidates the notification models into one """ + metadata = MetaData(bind=db.bind) + notification_table = inspect_table(metadata, "core__notifications") + cn_table = inspect_table(metadata, "core__comment_notifications") + cp_table = inspect_table(metadata, "core__processing_notifications") + gmr_table = inspect_table(metadata, "core__generic_model_reference") + + # Add fields needed + notification_object_id_column = Column( + "object_id", + Integer, + ForeignKey(GenericModelReference_V0.id) + ) + notification_object_id_column.create(notification_table) + db.commit() + + # Iterate over comments and move to notification base table. + for comment_notification in db.execute(cn_table.select()): + # Find the GMR. + cngmr = db.execute(gmr_table.select().where(and_( + gmr_table.c.obj_pk == comment_notification.subject_id, + gmr_table.c.model_type == "core__media_comments" + ))).first() + + if cngmr: + cngmr = cngmr[0] + else: + cngmr = db.execute(gmr_table.insert().values( + obj_pk=comment_notification.subject_id, + model_type="core__media_comments" + )).inserted_primary_key[0] + + # Save back on notification + db.execute(notification_table.update().where( + notification_table.c.id == comment_notification.id + ).values( + object_id=cngmr + )) + db.commit() + + # Do the same for processing notifications + for processing_notification in db.execute(cp_table.select()): + cpgmr = db.execute(gmr_table.select().where(and_( + gmr_table.c.obj_pk == processing_notification.subject_id, + gmr_table.c.model_type == "core__processing_notifications" + ))).first() + + if cpgmr: + cpgmr = cpgmr[0] + else: + cpgmr = db.execute(gmr_table.insert().values( + obj_pk=processing_notification.subject_id, + model_type="core__processing_notifications" + )).inserted_primary_key[0] + + db.execute(notification_table.update().where( + notification_table.c.id == processing_notification.id + ).values( + object_id=cpgmr + )) + db.commit() + + # Add the not null constraint + notification_object_id = notification_table.columns["object_id"] + notification_object_id.alter(nullable=False) + + # Now drop the fields we don't need + notification_type_column = notification_table.columns["type"] + notification_type_column.drop() + + # Drop the tables we no longer need + cp_table.drop() + cn_table.drop() + + db.commit() diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index e6a2dc35..ecd04874 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -41,6 +41,47 @@ from mediagoblin.tools.text import cleaned_markdown_conversion from mediagoblin.tools.url import slugify from mediagoblin.tools.translate import pass_to_ugettext as _ +class CommentingMixin(object): + """ + Mixin that gives classes methods to get and add the comments on/to it + + This assumes the model has a "comments" class which is a ForeignKey to the + Collection model. This will hold a Collection of comments which are + associated to this model. It also assumes the model has an "actor" + ForeignKey which points to the creator/publisher/etc. of the model. + + NB: This is NOT the mixin for the Comment Model, this is for + other models which support commenting. + """ + + def get_comment_link(self): + # Import here to avoid cyclic imports + from mediagoblin.db.models import Comment, GenericModelReference + + gmr = GenericModelReference.query.filter_by( + obj_pk=self.id, + model_type=self.__tablename__ + ).first() + + if gmr is None: + return None + + link = Comment.query.filter_by(comment_id=gmr.id).first() + return link + + def get_reply_to(self): + link = self.get_comment_link() + if link is None or link.target_id is None: + return None + + return link.target() + + def soft_delete(self, *args, **kwargs): + link = self.get_comment_link() + if link is not None: + link.delete() + super(CommentingMixin, self).soft_delete(*args, **kwargs) + class GeneratePublicIDMixin(object): """ Mixin that ensures that a the public_id field is populated. @@ -71,9 +112,10 @@ class GeneratePublicIDMixin(object): self.public_id = urlgen( "mediagoblin.api.object", object_type=self.object_type, - id=self.id, + id=str(uuid.uuid4()), qualified=True ) + self.save() return self.public_id class UserMixin(object): @@ -342,7 +384,7 @@ class MediaEntryMixin(GenerateSlugMixin, GeneratePublicIDMixin): return exif_short -class MediaCommentMixin(object): +class TextCommentMixin(GeneratePublicIDMixin): object_type = "comment" @property @@ -367,7 +409,6 @@ class MediaCommentMixin(object): actor=self.get_actor, comment=self.content) - class CollectionMixin(GenerateSlugMixin, GeneratePublicIDMixin): object_type = "collection" @@ -404,6 +445,28 @@ class CollectionMixin(GenerateSlugMixin, GeneratePublicIDMixin): collection=self.slug_or_id, **extra_args) + def add_to_collection(self, obj, content=None, commit=True): + """ Adds an object to the collection """ + # It's here to prevent cyclic imports + from mediagoblin.db.models import CollectionItem + + # Need the ID of this collection for this so check we've got one. + self.save(commit=False) + + # Create the CollectionItem + item = CollectionItem() + item.collection = self.id + item.get_object = obj + + if content is not None: + item.note = content + + self.num_items = self.num_items + 1 + + # Save both! + self.save(commit=commit) + item.save(commit=commit) + return item class CollectionItemMixin(object): @property diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index e52cab82..67659552 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -36,10 +36,10 @@ from sqlalchemy.util import memoized_property from mediagoblin.db.extratypes import (PathTupleWithSlashes, JSONEncoded, MutationDict) -from mediagoblin.db.base import Base, DictReadAttrProxy +from mediagoblin.db.base import Base, DictReadAttrProxy, FakeCursor from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \ - MediaCommentMixin, CollectionMixin, CollectionItemMixin, \ - ActivityMixin + CollectionMixin, CollectionItemMixin, ActivityMixin, TextCommentMixin, \ + CommentingMixin from mediagoblin.tools.files import delete_media_files from mediagoblin.tools.common import import_component from mediagoblin.tools.routing import extract_url_arguments @@ -262,7 +262,7 @@ class User(Base, UserMixin): collection.delete(**kwargs) # Find all the comments and delete those too - for comment in MediaComment.query.filter_by(actor=self.id): + for comment in TextComment.query.filter_by(actor=self.id): comment.delete(**kwargs) # Find all the activities and delete those too @@ -509,7 +509,7 @@ class NonceTimestamp(Base): nonce = Column(Unicode, nullable=False, primary_key=True) timestamp = Column(DateTime, nullable=False, primary_key=True) -class MediaEntry(Base, MediaEntryMixin): +class MediaEntry(Base, MediaEntryMixin, CommentingMixin): """ TODO: Consider fetching the media_files using join """ @@ -595,11 +595,18 @@ class MediaEntry(Base, MediaEntryMixin): )) def get_comments(self, ascending=False): - order_col = MediaComment.created - if not ascending: - order_col = desc(order_col) - return self.all_comments.order_by(order_col) + query = Comment.query.join(Comment.target_helper).filter(and_( + GenericModelReference.obj_pk == self.id, + GenericModelReference.model_type == self.__tablename__ + )) + if ascending: + query = query.order_by(Comment.added.asc()) + else: + qury = query.order_by(Comment.added.desc()) + + return FakeCursor(query, lambda c:c.comment()) + def url_to_prev(self, urlgen): """get the next 'newer' entry by this user""" media = MediaEntry.query.filter( @@ -689,7 +696,7 @@ class MediaEntry(Base, MediaEntryMixin): def soft_delete(self, *args, **kwargs): # Find all of the media comments for this and delete them - for comment in MediaComment.query.filter_by(media_entry=self.id): + for comment in self.get_comments(): comment.delete(*args, **kwargs) super(MediaEntry, self).soft_delete(*args, **kwargs) @@ -927,15 +934,63 @@ class MediaTag(Base): """A dict like view on this object""" return DictReadAttrProxy(self) +class Comment(Base): + """ + Link table between a response and another object that can have replies. + + This acts as a link table between an object and the comments on it, it's + done like this so that you can look up all the comments without knowing + whhich comments are on an object before hand. Any object can be a comment + and more or less any object can accept comments too. + + Important: This is NOT the old MediaComment table. + """ + __tablename__ = "core__comment_links" + + id = Column(Integer, primary_key=True) + + # The GMR to the object the comment is on. + target_id = Column( + Integer, + ForeignKey(GenericModelReference.id), + nullable=False + ) + target_helper = relationship( + GenericModelReference, + foreign_keys=[target_id] + ) + target = association_proxy("target_helper", "get_object", + creator=GenericModelReference.find_or_new) + + # The comment object + comment_id = Column( + Integer, + ForeignKey(GenericModelReference.id), + nullable=False + ) + comment_helper = relationship( + GenericModelReference, + foreign_keys=[comment_id] + ) + comment = association_proxy("comment_helper", "get_object", + creator=GenericModelReference.find_or_new) + + # When it was added + added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + -class MediaComment(Base, MediaCommentMixin): +class TextComment(Base, TextCommentMixin, CommentingMixin): + """ + A basic text comment, this is a usually short amount of text and nothing else + """ + # This is a legacy from when Comments where just on MediaEntry objects. __tablename__ = "core__media_comments" id = Column(Integer, primary_key=True) - media_entry = Column( - Integer, ForeignKey(MediaEntry.id), nullable=False, index=True) + public_id = Column(Unicode, unique=True) actor = Column(Integer, ForeignKey(User.id), nullable=False) created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) content = Column(UnicodeText, nullable=False) location = Column(Integer, ForeignKey("core__locations.id")) get_location = relationship("Location", lazy="joined") @@ -947,38 +1002,25 @@ class MediaComment(Base, MediaCommentMixin): backref=backref("posted_comments", lazy="dynamic", cascade="all, delete-orphan")) - get_entry = relationship(MediaEntry, - backref=backref("comments", - lazy="dynamic", - cascade="all, delete-orphan")) - - # Cascade: Comments are somewhat owned by their MediaEntry. - # So do the full thing. - # lazy=dynamic: MediaEntries might have many comments, - # so make the "all_comments" a query-like thing. - get_media_entry = relationship(MediaEntry, - backref=backref("all_comments", - lazy="dynamic", - cascade="all, delete-orphan")) - deletion_mode = Base.SOFT_DELETE def serialize(self, request): """ Unserialize to python dictionary for API """ - href = request.urlgen( - "mediagoblin.api.object", - object_type=self.object_type, - id=self.id, - qualified=True - ) - media = MediaEntry.query.filter_by(id=self.media_entry).first() + target = self.get_reply_to() + # If this is target just.. give them nothing? + if target is None: + target = {} + else: + target = target.serialize(request, show_comments=False) + + author = self.get_actor published = UTC.localize(self.created) context = { - "id": href, + "id": self.get_public_id(request.urlgen), "objectType": self.object_type, "content": self.content, - "inReplyTo": media.serialize(request, show_comments=False), + "inReplyTo": target, "author": author.serialize(request), "published": published.isoformat(), "updated": published.isoformat(), @@ -991,34 +1033,47 @@ class MediaComment(Base, MediaCommentMixin): def unserialize(self, data, request): """ Takes API objects and unserializes on existing comment """ + if "content" in data: + self.content = data["content"] + + if "location" in data: + Location.create(data["location"], self) + + # Handle changing the reply ID if "inReplyTo" in data: # Validate that the ID is correct try: - media_id = int(extract_url_arguments( + id = extract_url_arguments( url=data["inReplyTo"]["id"], urlmap=request.app.url_map - )["id"]) + )["id"] except ValueError: - return False + raise False + + public_id = request.urlgen( + "mediagoblin.api.object", + id=id, + object_type=data["inReplyTo"]["objectType"], + qualified=True + ) - media = MediaEntry.query.filter_by(id=media_id).first() + media = MediaEntry.query.filter_by(public_id=public_id).first() if media is None: return False - self.media_entry = media.id - - if "content" in data: - self.content = data["content"] - - if "location" in data: - Location.create(data["location"], self) + # We need an ID for this model. + self.save(commit=False) + # Create the link + link = Comment() + link.target = media + link.comment = self + link.save() + return True - - -class Collection(Base, CollectionMixin): +class Collection(Base, CollectionMixin, CommentingMixin): """A representation of a collection of objects. This holds a group/collection of objects that could be a user defined album @@ -1070,6 +1125,7 @@ class Collection(Base, CollectionMixin): OUTBOX_TYPE = "core-outbox" FOLLOWER_TYPE = "core-followers" FOLLOWING_TYPE = "core-following" + COMMENT_TYPE = "core-comments" USER_DEFINED_TYPE = "core-user-defined" def get_collection_items(self, ascending=False): @@ -1201,21 +1257,19 @@ class CommentSubscription(Base): class Notification(Base): __tablename__ = 'core__notifications' id = Column(Integer, primary_key=True) - type = Column(Unicode) - created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + object_id = Column(Integer, ForeignKey(GenericModelReference.id)) + object_helper = relationship(GenericModelReference) + obj = association_proxy("object_helper", "get_object", + creator=GenericModelReference.find_or_new) + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False, index=True) seen = Column(Boolean, default=lambda: False, index=True) user = relationship( User, - backref=backref('notifications', cascade='all, delete-orphan')) - - __mapper_args__ = { - 'polymorphic_identity': 'notification', - 'polymorphic_on': type - } + backref=backref('notifications', cascade='all, delete-orphan')) def __repr__(self): return '<{klass} #{id}: {user}: {subject} ({seen})>'.format( @@ -1233,42 +1287,9 @@ class Notification(Base): subject=getattr(self, 'subject', None), seen='unseen' if not self.seen else 'seen') - -class CommentNotification(Notification): - __tablename__ = 'core__comment_notifications' - id = Column(Integer, ForeignKey(Notification.id), primary_key=True) - - subject_id = Column(Integer, ForeignKey(MediaComment.id)) - subject = relationship( - MediaComment, - backref=backref('comment_notifications', cascade='all, delete-orphan')) - - __mapper_args__ = { - 'polymorphic_identity': 'comment_notification' - } - - -class ProcessingNotification(Notification): - __tablename__ = 'core__processing_notifications' - - id = Column(Integer, ForeignKey(Notification.id), primary_key=True) - - subject_id = Column(Integer, ForeignKey(MediaEntry.id)) - subject = relationship( - MediaEntry, - backref=backref('processing_notifications', - cascade='all, delete-orphan')) - - __mapper_args__ = { - 'polymorphic_identity': 'processing_notification' - } - -# the with_polymorphic call has been moved to the bottom above MODELS -# this is because it causes conflicts with relationship calls. - -class ReportBase(Base): +class Report(Base): """ - This is the basic report object which the other reports are based off of. + Represents a report that someone might file against Media, Comments, etc. :keyword reporter_id Holds the id of the user who created the report, as an Integer column. @@ -1281,8 +1302,6 @@ class ReportBase(Base): an Integer column. :keyword created Holds a datetime column of when the re- -port was filed. - :keyword discriminator This column distinguishes between the - different types of reports. :keyword resolver_id Holds the id of the moderator/admin who resolved the report. :keyword resolved Holds the DateTime object which descri- @@ -1291,8 +1310,11 @@ class ReportBase(Base): resolver's reasons for resolving the report this way. Some of this is auto-generated + :keyword object_id Holds the ID of the GenericModelReference + which points to the reported object. """ __tablename__ = 'core__reports' + id = Column(Integer, primary_key=True) reporter_id = Column(Integer, ForeignKey(User.id), nullable=False) reporter = relationship( @@ -1300,7 +1322,7 @@ class ReportBase(Base): backref=backref("reports_filed_by", lazy="dynamic", cascade="all, delete-orphan"), - primaryjoin="User.id==ReportBase.reporter_id") + primaryjoin="User.id==Report.reporter_id") report_content = Column(UnicodeText) reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False) reported_user = relationship( @@ -1308,69 +1330,42 @@ class ReportBase(Base): backref=backref("reports_filed_on", lazy="dynamic", cascade="all, delete-orphan"), - primaryjoin="User.id==ReportBase.reported_user_id") + primaryjoin="User.id==Report.reported_user_id") created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) - discriminator = Column('type', Unicode(50)) resolver_id = Column(Integer, ForeignKey(User.id)) resolver = relationship( User, backref=backref("reports_resolved_by", lazy="dynamic", cascade="all, delete-orphan"), - primaryjoin="User.id==ReportBase.resolver_id") + primaryjoin="User.id==Report.resolver_id") resolved = Column(DateTime) result = Column(UnicodeText) - __mapper_args__ = {'polymorphic_on': discriminator} + + object_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=False) + object_helper = relationship(GenericModelReference) + obj = association_proxy("object_helper", "get_object", + creator=GenericModelReference.find_or_new) + + def is_archived_report(self): + return self.resolved is not None def is_comment_report(self): - return self.discriminator=='comment_report' + if self.object_id is None: + return False + return isinstance(self.obj(), TextComment) def is_media_entry_report(self): - return self.discriminator=='media_report' - - def is_archived_report(self): - return self.resolved is not None + if self.object_id is None: + return False + return isinstance(self.obj(), MediaEntry) def archive(self,resolver_id, resolved, result): self.resolver_id = resolver_id self.resolved = resolved self.result = result - -class CommentReport(ReportBase): - """ - Reports that have been filed on comments. - :keyword comment_id Holds the integer value of the reported - comment's ID - """ - __tablename__ = 'core__reports_on_comments' - __mapper_args__ = {'polymorphic_identity': 'comment_report'} - - id = Column('id',Integer, ForeignKey('core__reports.id'), - primary_key=True) - comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=True) - comment = relationship( - MediaComment, backref=backref("reports_filed_on", - lazy="dynamic")) - -class MediaReport(ReportBase): - """ - Reports that have been filed on media entries - :keyword media_entry_id Holds the integer value of the reported - media entry's ID - """ - __tablename__ = 'core__reports_on_media' - __mapper_args__ = {'polymorphic_identity': 'media_report'} - - id = Column('id',Integer, ForeignKey('core__reports.id'), - primary_key=True) - media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=True) - media_entry = relationship( - MediaEntry, - backref=backref("reports_filed_on", - lazy="dynamic")) - class UserBan(Base): """ Holds the information on a specific user's ban-state. As long as one of @@ -1576,18 +1571,12 @@ class Graveyard(Base): "deleted": self.deleted } -with_polymorphic( - Notification, - [ProcessingNotification, CommentNotification]) - MODELS = [ - LocalUser, RemoteUser, User, MediaEntry, Tag, MediaTag, MediaComment, + LocalUser, RemoteUser, User, MediaEntry, Tag, MediaTag, Comment, TextComment, Collection, CollectionItem, MediaFile, FileKeynames, MediaAttachmentFile, - ProcessingMetaData, Notification, CommentNotification, - ProcessingNotification, Client, CommentSubscription, ReportBase, - CommentReport, MediaReport, UserBan, Privilege, PrivilegeUserAssociation, - RequestToken, AccessToken, NonceTimestamp, Activity, Generator, Location, - GenericModelReference, Graveyard] + ProcessingMetaData, Notification, Client, CommentSubscription, Report, + UserBan, Privilege, PrivilegeUserAssociation, RequestToken, AccessToken, + NonceTimestamp, Activity, Generator, Location, GenericModelReference, Graveyard] """ Foundations are the default rows that are created immediately after the tables -- cgit v1.2.3