diff options
26 files changed, 754 insertions, 344 deletions
diff --git a/mediagoblin/api/views.py b/mediagoblin/api/views.py index c515a8fa..671c3b36 100644 --- a/mediagoblin/api/views.py +++ b/mediagoblin/api/views.py @@ -22,7 +22,7 @@ from werkzeug.datastructures import FileStorage from mediagoblin.decorators import oauth_required, require_active_login from mediagoblin.api.decorators import user_has_privilege -from mediagoblin.db.models import User, LocalUser, MediaEntry, MediaComment, Activity +from mediagoblin.db.models import User, LocalUser, MediaEntry, Comment, TextComment, Activity from mediagoblin.tools.federation import create_activity, create_generator from mediagoblin.tools.routing import extract_url_arguments from mediagoblin.tools.response import redirect, json_response, json_error, \ @@ -268,7 +268,7 @@ def feed_endpoint(request, outbox=None): status=403 ) - comment = MediaComment(actor=request.user.id) + comment = TextComment(actor=request.user.id) comment.unserialize(data["object"], request) comment.save() @@ -278,7 +278,7 @@ def feed_endpoint(request, outbox=None): verb="post", actor=request.user, obj=comment, - target=comment.get_entry, + target=comment.get_reply_to(), generator=generator ) @@ -286,12 +286,22 @@ def feed_endpoint(request, outbox=None): elif obj.get("objectType", None) == "image": # Posting an image to the feed - media_id = int(extract_url_arguments( + media_id = extract_url_arguments( url=data["object"]["id"], urlmap=request.app.url_map - )["id"]) + )["id"] - media = MediaEntry.query.filter_by(id=media_id).first() + # Build public_id + public_id = request.urlgen( + "mediagoblin.api.object", + object_type=obj["objectType"], + id=media_id, + qualified=True + ) + + media = MediaEntry.query.filter_by( + public_id=public_id + ).first() if media is None: return json_response( @@ -345,10 +355,17 @@ def feed_endpoint(request, outbox=None): if "id" not in obj: return json_error("Object ID has not been specified.") - obj_id = int(extract_url_arguments( + obj_id = extract_url_arguments( url=obj["id"], urlmap=request.app.url_map - )["id"]) + )["id"] + + public_id = request.urlgen( + "mediagoblin.api.object", + object_type=obj["objectType"], + id=obj_id, + qualified=True + ) # Now try and find object if obj["objectType"] == "comment": @@ -358,7 +375,9 @@ def feed_endpoint(request, outbox=None): status=403 ) - comment = MediaComment.query.filter_by(id=obj_id).first() + comment = TextComment.query.filter_by( + public_id=public_id + ).first() if comment is None: return json_error( "No such 'comment' with id '{0}'.".format(obj_id) @@ -391,7 +410,9 @@ def feed_endpoint(request, outbox=None): return json_response(activity.serialize(request)) elif obj["objectType"] == "image": - image = MediaEntry.query.filter_by(id=obj_id).first() + image = MediaEntry.query.filter_by( + public_id=public_id + ).first() if image is None: return json_error( "No such 'image' with the id '{0}'.".format(obj["id"]) @@ -454,15 +475,22 @@ def feed_endpoint(request, outbox=None): return json_error("Object ID has not been specified.") # Parse out the object ID - obj_id = int(extract_url_arguments( + obj_id = extract_url_arguments( url=obj["id"], urlmap=request.app.url_map - )["id"]) + )["id"] + + public_id = request.urlgen( + "mediagoblin.api.object", + object_type=obj["objectType"], + id=obj_id, + qualified=True + ) if obj.get("objectType", None) == "comment": # Find the comment asked for - comment = MediaComment.query.filter_by( - id=obj_id, + comment = TextComment.query.filter_by( + public_id=public_id, actor=request.user.id ).first() @@ -491,7 +519,7 @@ def feed_endpoint(request, outbox=None): if obj.get("objectType", None) == "image": # Find the image entry = MediaEntry.query.filter_by( - id=obj_id, + public_id=public_id, actor=request.user.id ).first() @@ -500,10 +528,6 @@ def feed_endpoint(request, outbox=None): "No such 'image' with id '{0}'.".format(obj_id) ) - # Okay lets do our best to ensure there is a public_id for - # this image, there most likely is but it's important! - entry.get_public_id(request.urlgen) - # Make the delete activity generator = create_generator(request) activity = create_activity( @@ -621,7 +645,14 @@ def object_endpoint(request): status=404 ) - media = MediaEntry.query.filter_by(id=object_id).first() + public_id = request.urlgen( + "mediagoblin.api.object", + object_type=object_type, + id=object_id, + qualified=True + ) + + media = MediaEntry.query.filter_by(public_id=public_id).first() if media is None: return json_error( "Can't find '{0}' with ID '{1}'".format(object_type, object_id), @@ -633,7 +664,13 @@ def object_endpoint(request): @oauth_required def object_comments(request): """ Looks up for the comments on a object """ - media = MediaEntry.query.filter_by(id=request.matchdict["id"]).first() + public_id = request.urlgen( + "mediagoblin.api.object", + object_type=request.matchdict["object_type"], + id=request.matchdict["id"], + qualified=True + ) + media = MediaEntry.query.filter_by(public_id=public_id).first() if media is None: return json_error("Can't find '{0}' with ID '{1}'".format( request.matchdict["object_type"], 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 <http://www.gnu.org/licenses/>. +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 diff --git a/mediagoblin/decorators.py b/mediagoblin/decorators.py index 8874d2a0..a2c49bcc 100644 --- a/mediagoblin/decorators.py +++ b/mediagoblin/decorators.py @@ -23,7 +23,8 @@ from six.moves.urllib.parse import urljoin from mediagoblin import mg_globals as mgg from mediagoblin import messages -from mediagoblin.db.models import MediaEntry, LocalUser, MediaComment, AccessToken +from mediagoblin.db.models import MediaEntry, LocalUser, TextComment, \ + AccessToken, Comment from mediagoblin.tools.response import ( redirect, render_404, render_user_banned, json_response) @@ -325,11 +326,11 @@ def allow_reporting(controller): def get_optional_media_comment_by_id(controller): """ - Pass in a MediaComment based off of a url component. Because of this decor- - -ator's use in filing Media or Comment Reports, it has two valid outcomes. + Pass in a Comment based off of a url component. Because of this decor- + -ator's use in filing Reports, it has two valid outcomes. :returns The view function being wrapped with kwarg `comment` set to - the MediaComment who's id is in the URL. If there is a + the Comment who's id is in the URL. If there is a comment id in the URL and if it is valid. :returns The view function being wrapped with kwarg `comment` set to None. If there is no comment id in the URL. @@ -339,8 +340,9 @@ def get_optional_media_comment_by_id(controller): @wraps(controller) def wrapper(request, *args, **kwargs): if 'comment' in request.matchdict: - comment = MediaComment.query.filter_by( - id=request.matchdict['comment']).first() + comment = Comment.query.filter_by( + id=request.matchdict['comment'] + ).first() if comment is None: return render_404(request) diff --git a/mediagoblin/moderation/tools.py b/mediagoblin/moderation/tools.py index d1fedb0e..73afd051 100644 --- a/mediagoblin/moderation/tools.py +++ b/mediagoblin/moderation/tools.py @@ -68,14 +68,14 @@ def take_punitive_actions(request, form, report, user): if u'delete' in form.action_to_resolve.data and \ report.is_comment_report(): - deleted_comment = report.comment + deleted_comment = report.obj() Session.delete(deleted_comment) form.resolution_content.data += \ _(u"\n{mod} deleted the comment.").format( mod=request.user.username) elif u'delete' in form.action_to_resolve.data and \ report.is_media_entry_report(): - deleted_media = report.media_entry + deleted_media = report.obj() deleted_media.delete() form.resolution_content.data += \ _(u"\n{mod} deleted the media entry.").format( diff --git a/mediagoblin/moderation/views.py b/mediagoblin/moderation/views.py index a73537d6..fdcbf051 100644 --- a/mediagoblin/moderation/views.py +++ b/mediagoblin/moderation/views.py @@ -15,7 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -from mediagoblin.db.models import (MediaEntry, User, ReportBase, Privilege, +from mediagoblin.db.models import (MediaEntry, User, Report, Privilege, UserBan, LocalUser) from mediagoblin.decorators import (require_admin_or_moderator_login, active_user_from_url, user_has_privilege, @@ -83,9 +83,9 @@ def moderation_users_detail(request): LocalUser.username==request.matchdict['user'] ).first() active_reports = user.reports_filed_on.filter( - ReportBase.resolved==None).limit(5) + Report.resolved==None).limit(5) closed_reports = user.reports_filed_on.filter( - ReportBase.resolved!=None).all() + Report.resolved!=None).all() privileges = Privilege.query user_banned = UserBan.query.get(user.id) ban_form = moderation_forms.BanForm() @@ -116,23 +116,23 @@ def moderation_reports_panel(request): active_settings['current_page'] = form.active_p.data or 1 closed_settings['current_page'] = form.closed_p.data or 1 filters = [ - getattr(ReportBase,key)==val + getattr(Report,key)==val for key,val in filters.viewitems()] - all_active = ReportBase.query.filter( - ReportBase.resolved==None).filter( + all_active = Report.query.filter( + Report.resolved==None).filter( *filters) - all_closed = ReportBase.query.filter( - ReportBase.resolved!=None).filter( + all_closed = Report.query.filter( + Report.resolved!=None).filter( *filters) # report_list and closed_report_list are the two lists of up to 10 # items which are actually passed to the user in this request report_list = all_active.order_by( - ReportBase.created.desc()).offset( + Report.created.desc()).offset( (active_settings['current_page']-1)*10).limit(10) closed_report_list = all_closed.order_by( - ReportBase.created.desc()).offset( + Report.created.desc()).offset( (closed_settings['current_page']-1)*10).limit(10) active_settings['last_page'] = int(ceil(all_active.count()/10.)) @@ -155,7 +155,7 @@ def moderation_reports_detail(request): erator would go to to take an action to resolve a report. """ form = moderation_forms.ReportResolutionForm(request.form) - report = ReportBase.query.get(request.matchdict['report_id']) + report = Report.query.get(request.matchdict['report_id']) form.take_away_privileges.choices = [ (s.privilege_name,s.privilege_name.title()) \ diff --git a/mediagoblin/notifications/__init__.py b/mediagoblin/notifications/__init__.py index ea468f8e..8690aae5 100644 --- a/mediagoblin/notifications/__init__.py +++ b/mediagoblin/notifications/__init__.py @@ -16,8 +16,8 @@ import logging -from mediagoblin.db.models import Notification, \ - CommentNotification, CommentSubscription, User +from mediagoblin.db.models import Notification, CommentSubscription, User, \ + Comment, GenericModelReference from mediagoblin.notifications.task import email_notification_task from mediagoblin.notifications.tools import generate_comment_message @@ -37,10 +37,10 @@ def trigger_notification(comment, media_entry, request): if comment.get_actor == subscription.user: continue - cn = CommentNotification( + cn = Notification( user_id=subscription.user_id, - subject_id=comment.id) - + ) + cn.obj = comment cn.save() if subscription.send_email: @@ -61,9 +61,15 @@ def mark_notification_seen(notification): def mark_comment_notification_seen(comment_id, user): - notification = CommentNotification.query.filter_by( + comment = Comment.query.get(comment_id).comment() + comment_gmr = GenericModelReference.query.filter_by( + obj_pk=comment.id, + model_type=comment.__tablename__ + ).first() + notification = Notification.query.filter_by( user_id=user.id, - subject_id=comment_id).first() + object_id=comment_gmr.id + ).first() _log.debug(u'Marking {0} as seen.'.format(notification)) diff --git a/mediagoblin/notifications/task.py b/mediagoblin/notifications/task.py index d915212a..652b78e2 100644 --- a/mediagoblin/notifications/task.py +++ b/mediagoblin/notifications/task.py @@ -20,7 +20,7 @@ from celery import registry from celery.task import Task from mediagoblin.tools.mail import send_email -from mediagoblin.db.models import CommentNotification +from mediagoblin.db.models import Notification _log = logging.getLogger(__name__) @@ -34,7 +34,7 @@ class EmailNotificationTask(Task): the web server. ''' def run(self, notification_id, message): - cn = CommentNotification.query.filter_by(id=notification_id).first() + cn = Notification.query.filter_by(id=notification_id).first() _log.info(u'Sending notification email about {0}'.format(cn)) return send_email( diff --git a/mediagoblin/notifications/views.py b/mediagoblin/notifications/views.py index cfe66b2e..984b9c9b 100644 --- a/mediagoblin/notifications/views.py +++ b/mediagoblin/notifications/views.py @@ -57,7 +57,10 @@ def mark_all_comment_notifications_seen(request): Marks all comment notifications seen. """ for comment in get_notifications(request.user.id): - mark_comment_notification_seen(comment.subject_id, request.user) + mark_comment_notification_seen( + comment.obj().get_comment_link().id, + request.user + ) if request.GET.get('next'): return redirect(request, location=request.GET.get('next')) diff --git a/mediagoblin/submit/lib.py b/mediagoblin/submit/lib.py index eee5653f..2edea70f 100644 --- a/mediagoblin/submit/lib.py +++ b/mediagoblin/submit/lib.py @@ -281,6 +281,9 @@ def api_upload_request(request, file_data, entry): # This will be set later but currently we just don't have enough information entry.slug = None + # This is a MUST. + entry.get_public_id(request.urlgen) + queue_file = prepare_queue_task(request.app, entry, file_data.filename) with queue_file: queue_file.write(request.data) diff --git a/mediagoblin/templates/mediagoblin/fragments/header_notifications.html b/mediagoblin/templates/mediagoblin/fragments/header_notifications.html index 68c42bf4..99c5abba 100644 --- a/mediagoblin/templates/mediagoblin/fragments/header_notifications.html +++ b/mediagoblin/templates/mediagoblin/fragments/header_notifications.html @@ -4,9 +4,9 @@ <h3>{% trans %}New comments{% endtrans %}</h3> <ul> {% for notification in notifications %} - {% set comment = notification.subject %} + {% set comment = notification.obj() %} {% set comment_author = comment.get_actor %} - {% set media = comment.get_entry %} + {% set media = comment.get_reply_to() %} <li class="comment_wrapper"> <div class="comment_author"> <img src="{{ request.staticdirect('/images/icon_comment.png') }}" /> diff --git a/mediagoblin/templates/mediagoblin/moderation/report.html b/mediagoblin/templates/mediagoblin/moderation/report.html index 584dca99..abbd4a0c 100644 --- a/mediagoblin/templates/mediagoblin/moderation/report.html +++ b/mediagoblin/templates/mediagoblin/moderation/report.html @@ -33,10 +33,11 @@ {% trans %}Return to Reports Panel{% endtrans %}</a> </div> <h2>{% trans %}Report{% endtrans %} #{{ report.id }}</h2> - {% if report.is_comment_report() and report.comment %} + {% if report.is_comment_report() and report.object_id %} {% trans %}Reported comment{% endtrans %}: - {% set comment = report.comment %} + {% set comment = report.obj() %} + {% set target = report.obj().get_reply_to() %} {% set reported_user = comment.get_actor %} <div id="comment-{{ comment.id }}" class="comment_wrapper"> @@ -50,8 +51,8 @@ <a href="{{ request.urlgen( 'mediagoblin.user_pages.media_home.view_comment', comment=comment.id, - user=comment.get_media_entry.get_actor.username, - media=comment.get_media_entry.slug_or_id) }}#comment" + user=target.get_actor.username, + media=target.slug_or_id) }}#comment" class="comment_whenlink"> <span title='{{- comment.created.strftime("%I:%M%p %Y-%m-%d") -}}'> {%- trans formatted_time=timesince(comment.created) -%} @@ -65,9 +66,9 @@ {% endautoescape %} </div> </div> - {% elif report.is_media_entry_report() and report.media_entry %} + {% elif report.is_media_entry_report() and report.object_id %} - {% set media_entry = report.media_entry %} + {% set media_entry = report.obj() %} <div class="three columns media_thumbnail"> <a href="{{ request.urlgen('mediagoblin.user_pages.media_home', user=media_entry.get_actor.username, diff --git a/mediagoblin/templates/mediagoblin/moderation/report_panel.html b/mediagoblin/templates/mediagoblin/moderation/report_panel.html index 39ca90f5..c82cd412 100644 --- a/mediagoblin/templates/mediagoblin/moderation/report_panel.html +++ b/mediagoblin/templates/mediagoblin/moderation/report_panel.html @@ -81,7 +81,7 @@ curr_page !=p %} </tr> {% for report in report_list %} <tr> - {% if report.discriminator == "comment_report" %} + {% if report.is_comment_report %} <td> <img src="{{ request.staticdirect( @@ -97,7 +97,7 @@ curr_page !=p %} {% endtrans %} </a> </td> - {% elif report.discriminator == "media_report" %} + {% elif report.is_media_entry_report %} <td> <img src="{{ request.staticdirect( diff --git a/mediagoblin/templates/mediagoblin/moderation/user.html b/mediagoblin/templates/mediagoblin/moderation/user.html index 594f845d..1e48bf84 100644 --- a/mediagoblin/templates/mediagoblin/moderation/user.html +++ b/mediagoblin/templates/mediagoblin/moderation/user.html @@ -125,9 +125,9 @@ </a> </td> <td> - {% if report.discriminator == "comment_report" %} + {% if report.is_comment_report() %} <a>{%- trans %}Reported Comment{% endtrans -%}</a> - {% elif report.discriminator == "media_report" %} + {% elif report.is_media_entry_report() %} <a>{%- trans %}Reported Media Entry{% endtrans -%}</a> {% endif %} </td> diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index b74ca13e..1a35414f 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -93,7 +93,7 @@ <p>{{ media.description_html }}</p> {% endautoescape %} </div> - {% if comments and request.user and request.user.has_privilege('commenter') %} + {% if request.user and request.user.has_privilege('commenter') %} <div class="media_comments"> {% if app_config['allow_comments'] %} <a diff --git a/mediagoblin/tests/test_api.py b/mediagoblin/tests/test_api.py index b58e112f..205b8a69 100644 --- a/mediagoblin/tests/test_api.py +++ b/mediagoblin/tests/test_api.py @@ -25,7 +25,7 @@ from webtest import AppError from .resources import GOOD_JPG from mediagoblin import mg_globals -from mediagoblin.db.models import User, MediaEntry, MediaComment +from mediagoblin.db.models import User, MediaEntry, TextComment from mediagoblin.tools.routing import extract_url_arguments from mediagoblin.tests.tools import fixture_add_user from mediagoblin.moderation.tools import take_away_privileges @@ -188,8 +188,7 @@ class TestAPI(object): # Lets change the image uploader to be self.other_user, this is easier # than uploading the image as someone else as the way self.mocked_oauth_required # and self._upload_image. - id = int(data["object"]["id"].split("/")[-2]) - media = MediaEntry.query.filter_by(id=id).first() + media = MediaEntry.query.filter_by(public_id=data["object"]["id"]).first() media.actor = self.other_user.id media.save() @@ -232,14 +231,13 @@ class TestAPI(object): image = json.loads(response.body.decode())["object"] # Check everything has been set on the media correctly - id = int(image["id"].split("/")[-2]) - media = MediaEntry.query.filter_by(id=id).first() + media = MediaEntry.query.filter_by(public_id=image["id"]).first() assert media.title == title assert media.description == description assert media.license == license # Check we're being given back everything we should on an update - assert int(image["id"].split("/")[-2]) == media.id + assert image["id"] == media.public_id assert image["displayName"] == title assert image["content"] == description assert image["license"] == license @@ -288,8 +286,7 @@ class TestAPI(object): request = test_app.get(object_uri) image = json.loads(request.body.decode()) - entry_id = int(image["id"].split("/")[-2]) - entry = MediaEntry.query.filter_by(id=entry_id).first() + entry = MediaEntry.query.filter_by(public_id=image["id"]).first() assert request.status_code == 200 @@ -319,8 +316,7 @@ class TestAPI(object): assert response.status_code == 200 # Find the objects in the database - media_id = int(data["object"]["id"].split("/")[-2]) - media = MediaEntry.query.filter_by(id=media_id).first() + media = MediaEntry.query.filter_by(public_id=data["object"]["id"]).first() comment = media.get_comments()[0] # Tests that it matches in the database @@ -382,8 +378,7 @@ class TestAPI(object): response, comment_data = self._activity_to_feed(test_app, activity) # change who uploaded the comment as it's easier than changing - comment_id = int(comment_data["object"]["id"].split("/")[-2]) - comment = MediaComment.query.filter_by(id=comment_id).first() + comment = TextComment.query.filter_by(public_id=comment_data["object"]["id"]).first() comment.actor = self.other_user.id comment.save() @@ -510,8 +505,7 @@ class TestAPI(object): response = self._activity_to_feed(test_app, activity)[1] # Check the media is no longer in the database - media_id = int(object_id.split("/")[-2]) - media = MediaEntry.query.filter_by(id=media_id).first() + media = MediaEntry.query.filter_by(public_id=object_id).first() assert media is None @@ -552,8 +546,8 @@ class TestAPI(object): delete = self._activity_to_feed(test_app, activity)[1] # Verify the comment no longer exists - comment_id = int(comment["object"]["id"].split("/")[-2]) - assert MediaComment.query.filter_by(id=comment_id).first() is None + assert TextComment.query.filter_by(public_id=comment["object"]["id"]).first() is None + comment_id = comment["object"]["id"] # Check we've got a delete activity back assert "id" in delete @@ -593,7 +587,6 @@ class TestAPI(object): comment = self._activity_to_feed(test_app, activity)[1] # Verify the comment reflects the changes - comment_id = int(comment["object"]["id"].split("/")[-2]) - model = MediaComment.query.filter_by(id=comment_id).first() + model = TextComment.query.filter_by(public_id=comment["object"]["id"]).first() assert model.content == activity["object"]["content"] diff --git a/mediagoblin/tests/test_misc.py b/mediagoblin/tests/test_misc.py index 2bff0057..558a9bd7 100644 --- a/mediagoblin/tests/test_misc.py +++ b/mediagoblin/tests/test_misc.py @@ -24,7 +24,7 @@ from mediagoblin.db.base import Session from mediagoblin.media_types import sniff_media from mediagoblin.submit.lib import new_upload_entry from mediagoblin.submit.task import collect_garbage -from mediagoblin.db.models import User, MediaEntry, MediaComment +from mediagoblin.db.models import User, MediaEntry, TextComment, Comment from mediagoblin.tests.tools import fixture_add_user, fixture_media_entry @@ -46,25 +46,31 @@ def test_user_deletes_other_comments(test_app): Session.flush() # Create all 4 possible comments: - for u_id in (user_a.id, user_b.id): - for m_id in (media_a.id, media_b.id): - cmt = MediaComment() - cmt.media_entry = m_id - cmt.actor = u_id + for u in (user_a, user_b): + for m in (media_a, media_b): + cmt = TextComment() + cmt.actor = u.id cmt.content = u"Some Comment" Session.add(cmt) + # think i need this to get the command ID + Session.flush() + + link = Comment() + link.target = m + link.comment = cmt + Session.add(link) Session.flush() usr_cnt1 = User.query.count() med_cnt1 = MediaEntry.query.count() - cmt_cnt1 = MediaComment.query.count() + cmt_cnt1 = TextComment.query.count() User.query.get(user_a.id).delete(commit=False) usr_cnt2 = User.query.count() med_cnt2 = MediaEntry.query.count() - cmt_cnt2 = MediaComment.query.count() + cmt_cnt2 = TextComment.query.count() # One user deleted assert usr_cnt2 == usr_cnt1 - 1 @@ -77,7 +83,7 @@ def test_user_deletes_other_comments(test_app): usr_cnt2 = User.query.count() med_cnt2 = MediaEntry.query.count() - cmt_cnt2 = MediaComment.query.count() + cmt_cnt2 = TextComment.query.count() # All users gone assert usr_cnt2 == usr_cnt1 - 2 diff --git a/mediagoblin/tests/test_moderation.py b/mediagoblin/tests/test_moderation.py index 85c130ca..55bb4c4b 100644 --- a/mediagoblin/tests/test_moderation.py +++ b/mediagoblin/tests/test_moderation.py @@ -18,7 +18,8 @@ import pytest from mediagoblin.tests.tools import (fixture_add_user, fixture_add_comment_report, fixture_add_comment) -from mediagoblin.db.models import User, LocalUser, CommentReport, MediaComment, UserBan +from mediagoblin.db.models import User, LocalUser, Report, TextComment, \ + UserBan, GenericModelReference from mediagoblin.tools import template, mail from webtest import AppError @@ -102,15 +103,15 @@ class TestModerationViews: # to a reported comment #---------------------------------------------------------------------- fixture_add_comment_report(reported_user=self.user) - comment_report = CommentReport.query.filter( - CommentReport.reported_user==self.user).first() + comment_report = Report.query.filter( + Report.reported_user==self.user).first() response = self.test_app.get('/mod/reports/{0}/'.format( comment_report.id)) assert response.status == '200 OK' self.query_for_users() - comment_report = CommentReport.query.filter( - CommentReport.reported_user==self.user).first() + comment_report = Report.query.filter( + Report.reported_user==self.user).first() response, context = self.do_post({'action_to_resolve':[u'takeaway'], 'take_away_privileges':[u'commenter'], @@ -118,15 +119,15 @@ class TestModerationViews: url='/mod/reports/{0}/'.format(comment_report.id)) self.query_for_users() - comment_report = CommentReport.query.filter( - CommentReport.reported_user==self.user).first() + comment_report = Report.query.filter( + Report.reported_user==self.user).first() assert response.status == '302 FOUND' assert not self.user.has_privilege(u'commenter') assert comment_report.is_archived_report() is True fixture_add_comment_report(reported_user=self.user) - comment_report = CommentReport.query.filter( - CommentReport.reported_user==self.user).first() + comment_report = Report.query.filter( + Report.reported_user==self.user).first() # Then, test a moderator sending an email to a user in response to a # reported comment @@ -139,8 +140,8 @@ class TestModerationViews: url='/mod/reports/{0}/'.format(comment_report.id)) self.query_for_users() - comment_report = CommentReport.query.filter( - CommentReport.reported_user==self.user).first() + comment_report = Report.query.filter( + Report.reported_user==self.user).first() assert response.status == '302 FOUND' assert mail.EMAIL_TEST_MBOX_INBOX == [{'to': [u'regular@example.com'], 'message': 'Content-Type: text/plain; charset="utf-8"\n\ @@ -157,13 +158,17 @@ VGhpcyBpcyB5b3VyIGxhc3Qgd2FybmluZywgcmVndWxhci4uLi4=\n', self.query_for_users() fixture_add_comment(author=self.user.id, comment=u'Comment will be removed') - test_comment = MediaComment.query.filter( - MediaComment.actor==self.user.id).first() + test_comment = TextComment.query.filter( + TextComment.actor==self.user.id).first() fixture_add_comment_report(comment=test_comment, reported_user=self.user) - comment_report = CommentReport.query.filter( - CommentReport.comment==test_comment).filter( - CommentReport.resolved==None).first() + comment_gmr = GenericModelReference.query.filter_by( + obj_pk=test_comment.id, + model_type=test_comment.__tablename__ + ).first() + comment_report = Report.query.filter( + Report.object_id==comment_gmr.id).filter( + Report.resolved==None).first() response, context = self.do_post( {'action_to_resolve':[u'userban', u'delete'], @@ -176,17 +181,17 @@ VGhpcyBpcyB5b3VyIGxhc3Qgd2FybmluZywgcmVndWxhci4uLi4=\n', test_user_ban = UserBan.query.filter( UserBan.user_id == self.user.id).first() assert test_user_ban is not None - test_comment = MediaComment.query.filter( - MediaComment.actor==self.user.id).first() + test_comment = TextComment.query.filter( + TextComment.actor==self.user.id).first() assert test_comment is None # Then, test what happens when a moderator attempts to punish an admin # from a reported comment on an admin. #---------------------------------------------------------------------- fixture_add_comment_report(reported_user=self.admin_user) - comment_report = CommentReport.query.filter( - CommentReport.reported_user==self.admin_user).filter( - CommentReport.resolved==None).first() + comment_report = Report.query.filter( + Report.reported_user==self.admin_user).filter( + Report.resolved==None).first() response, context = self.do_post({'action_to_resolve':[u'takeaway'], 'take_away_privileges':[u'active'], diff --git a/mediagoblin/tests/test_notifications.py b/mediagoblin/tests/test_notifications.py index f7e67d80..a1dc60ed 100644 --- a/mediagoblin/tests/test_notifications.py +++ b/mediagoblin/tests/test_notifications.py @@ -20,8 +20,7 @@ import six.moves.urllib.parse as urlparse from mediagoblin.tools import template, mail -from mediagoblin.db.models import Notification, CommentNotification, \ - CommentSubscription +from mediagoblin.db.models import Notification, CommentSubscription from mediagoblin.db.base import Session from mediagoblin.notifications import mark_comment_notification_seen @@ -109,11 +108,10 @@ class TestNotifications: notification = notifications[0] - assert type(notification) == CommentNotification assert notification.seen == False assert notification.user_id == user.id - assert notification.subject.get_actor.id == self.test_user.id - assert notification.subject.content == u'Test comment #42' + assert notification.obj().get_actor.id == self.test_user.id + assert notification.obj().content == u'Test comment #42' if wants_email == True: assert mail.EMAIL_TEST_MBOX_INBOX == [ @@ -130,7 +128,7 @@ otherperson@example.com\n\nSGkgb3RoZXJwZXJzb24sCmNocmlzIGNvbW1lbnRlZCBvbiB5b3VyI # Save the ids temporarily because of DetachedInstanceError notification_id = notification.id - comment_id = notification.subject.id + comment_id = notification.obj().get_comment_link().id self.logout() self.login('otherperson', 'nosreprehto') diff --git a/mediagoblin/tests/test_reporting.py b/mediagoblin/tests/test_reporting.py index f036303a..803fc849 100644 --- a/mediagoblin/tests/test_reporting.py +++ b/mediagoblin/tests/test_reporting.py @@ -20,8 +20,7 @@ import six from mediagoblin.tools import template from mediagoblin.tests.tools import (fixture_add_user, fixture_media_entry, fixture_add_comment, fixture_add_comment_report) -from mediagoblin.db.models import (MediaReport, CommentReport, User, LocalUser, - MediaComment) +from mediagoblin.db.models import Report, User, LocalUser, TextComment class TestReportFiling: @@ -80,7 +79,7 @@ class TestReportFiling: assert response.status == "302 FOUND" - media_report = MediaReport.query.first() + media_report = Report.query.first() allie_user, natalie_user = self.query_for_users() assert media_report is not None @@ -88,7 +87,6 @@ class TestReportFiling: assert media_report.reporter_id == allie_id assert media_report.reported_user_id == natalie_user.id assert media_report.created is not None - assert media_report.discriminator == 'media_report' def testCommentReports(self): self.login(u'allie') @@ -98,9 +96,11 @@ class TestReportFiling: media_entry = fixture_media_entry(uploader=natalie_user.id, state=u'processed') mid = media_entry.id - fixture_add_comment(media_entry=mid, - author=natalie_user.id) - comment = MediaComment.query.first() + fixture_add_comment( + media_entry=media_entry, + author=natalie_user.id + ) + comment = TextComment.query.first() comment_uri_slug = '/u/{0}/m/{1}/c/{2}/'.format(natalie_user.username, media_entry.slug, @@ -115,7 +115,7 @@ class TestReportFiling: assert response.status == "302 FOUND" - comment_report = CommentReport.query.first() + comment_report = Report.query.first() allie_user, natalie_user = self.query_for_users() assert comment_report is not None @@ -123,7 +123,6 @@ class TestReportFiling: assert comment_report.reporter_id == allie_id assert comment_report.reported_user_id == natalie_user.id assert comment_report.created is not None - assert comment_report.discriminator == 'comment_report' def testArchivingReports(self): self.login(u'natalie') @@ -132,14 +131,14 @@ class TestReportFiling: fixture_add_comment(author=allie_user.id, comment=u'Comment will be removed') - test_comment = MediaComment.query.filter( - MediaComment.actor==allie_user.id).first() + test_comment = TextComment.query.filter( + TextComment.actor==allie_user.id).first() fixture_add_comment_report(comment=test_comment, reported_user=allie_user, report_content=u'Testing Archived Reports #1', reporter=natalie_user) - comment_report = CommentReport.query.filter( - CommentReport.reported_user==allie_user).first() + comment_report = Report.query.filter( + Report.reported_user==allie_user).first() assert comment_report.report_content == u'Testing Archived Reports #1' response, context = self.do_post( @@ -151,10 +150,10 @@ class TestReportFiling: assert response.status == "302 FOUND" allie_user, natalie_user = self.query_for_users() - archived_report = CommentReport.query.filter( - CommentReport.reported_user==allie_user).first() + archived_report = Report.query.filter( + Report.reported_user==allie_user).first() - assert CommentReport.query.count() != 0 + assert Report.query.count() != 0 assert archived_report is not None assert archived_report.report_content == u'Testing Archived Reports #1' assert archived_report.reporter_id == natalie_id @@ -164,4 +163,3 @@ class TestReportFiling: assert archived_report.result == u'''This is a test of archiving reports. natalie banned user allie indefinitely. natalie deleted the comment.''' - assert archived_report.discriminator == 'comment_report' diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index c0c3d3cf..f9031d37 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -99,8 +99,14 @@ class TestSubmission: return {'upload_files': [('file', filename)]} def check_comments(self, request, media_id, count): - comments = request.db.MediaComment.query.filter_by(media_entry=media_id) - assert count == len(list(comments)) + gmr = request.db.GenericModelReference.query.filter_by( + obj_pk=media_id, + model_type=request.db.MediaEntry.__tablename__ + ).first() + if gmr is None and count <= 0: + return # Yerp it's fine. + comments = request.db.Comment.query.filter_by(target_id=gmr.id) + assert count == comments.count() def test_missing_fields(self): # Test blank form diff --git a/mediagoblin/tests/tools.py b/mediagoblin/tests/tools.py index ba143acd..77a9a86c 100644 --- a/mediagoblin/tests/tools.py +++ b/mediagoblin/tests/tools.py @@ -25,9 +25,9 @@ from paste.deploy import loadapp from webtest import TestApp from mediagoblin import mg_globals -from mediagoblin.db.models import User, LocalUser, MediaEntry, Collection, MediaComment, \ - CommentSubscription, CommentNotification, Privilege, CommentReport, Client, \ - RequestToken, AccessToken, Activity, Generator +from mediagoblin.db.models import User, LocalUser, MediaEntry, Collection, TextComment, \ + CommentSubscription, Notification, Privilege, Report, Client, \ + RequestToken, AccessToken, Activity, Generator, Comment from mediagoblin.tools import testing from mediagoblin.init.config import read_mediagoblin_config from mediagoblin.db.base import Session @@ -222,14 +222,16 @@ def fixture_comment_subscription(entry, notify=True, send_email=None): return cs -def fixture_add_comment_notification(entry_id, subject_id, user_id, +def fixture_add_comment_notification(entry, subject, user, seen=False): - cn = CommentNotification(user_id=user_id, - seen=seen, - subject_id=subject_id) + cn = Notification( + user_id=user, + seen=seen, + ) + cn.obj = subject cn.save() - cn = CommentNotification.query.filter_by(id=cn.id).first() + cn = Notification.query.filter_by(id=cn.id).first() Session.expunge(cn) @@ -309,22 +311,27 @@ def fixture_add_comment(author=None, media_entry=None, comment=None): author = fixture_add_user().id if media_entry is None: - media_entry = fixture_media_entry().id + media_entry = fixture_media_entry() if comment is None: comment = \ 'Auto-generated test comment by user #{0} on media #{0}'.format( author, media_entry) - comment = MediaComment(actor=author, - media_entry=media_entry, - content=comment) + text_comment = TextComment( + actor=author, + content=comment + ) + text_comment.save() - comment.save() + comment_link = Comment() + comment_link.target = media_entry + comment_link.comment = text_comment + comment_link.save() - Session.expunge(comment) + Session.expunge(comment_link) - return comment + return text_comment def fixture_add_comment_report(comment=None, reported_user=None, reporter=None, created=None, report_content=None): @@ -344,12 +351,13 @@ def fixture_add_comment_report(comment=None, reported_user=None, report_content = \ 'Auto-generated test report' - comment_report = CommentReport(comment=comment, - reported_user = reported_user, - reporter = reporter, - created = created, - report_content=report_content) - + comment_report = Report() + comment_report.obj = comment + comment_report.reported_user = reported_user + comment_report.reporter = reporter + comment_report.created = created + comment_report.report_content = report_content + comment_report.obj = comment comment_report.save() Session.expunge(comment_report) diff --git a/mediagoblin/user_pages/lib.py b/mediagoblin/user_pages/lib.py index fc1b6a7e..b6741001 100644 --- a/mediagoblin/user_pages/lib.py +++ b/mediagoblin/user_pages/lib.py @@ -16,8 +16,8 @@ from mediagoblin import mg_globals from mediagoblin.db.base import Session -from mediagoblin.db.models import (CollectionItem, MediaReport, CommentReport, - MediaComment, MediaEntry) +from mediagoblin.db.models import CollectionItem, Report, TextComment, \ + MediaEntry from mediagoblin.tools.mail import send_email from mediagoblin.tools.pluginapi import hook_runall from mediagoblin.tools.template import render_template @@ -82,34 +82,27 @@ def add_media_to_collection(collection, media, note=None, commit=True): def build_report_object(report_form, media_entry=None, comment=None): """ This function is used to convert a form object (from a User filing a - report) into either a MediaReport or CommentReport object. + report) into a Report. :param report_form A MediaReportForm or a CommentReportForm object with valid information from a POST request. :param media_entry A MediaEntry object. The MediaEntry being repo- - -rted by a MediaReport. In a CommentReport, - this will be None. - :param comment A MediaComment object. The MediaComment being - reported by a CommentReport. In a MediaReport - this will be None. - - :returns A MediaReport object if a valid MediaReportForm is - passed as kwarg media_entry. This MediaReport has + -rted by a Report. + :param comment A Comment object. The Comment being + reported by a Report. + + :returns A Report object if a valid MediaReportForm is + passed as kwarg media_entry. This Report has not been saved. - :returns A CommentReport object if a valid CommentReportForm - is passed as kwarg comment. This CommentReport - has not been saved. :returns None if the form_dict is invalid. """ - + report_object = Report() if report_form.validate() and comment is not None: - report_object = CommentReport() - report_object.comment_id = comment.id - report_object.reported_user_id = MediaComment.query.get( + report_object.obj = comment.comment() + report_object.reported_user_id = TextComment.query.get( comment.id).get_actor.id elif report_form.validate() and media_entry is not None: - report_object = MediaReport() - report_object.media_entry_id = media_entry.id + report_object.obj = media_entry report_object.reported_user_id = MediaEntry.query.get( media_entry.id).get_actor.id else: diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index f1c8a622..ba94ec16 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -21,8 +21,9 @@ import json import six from mediagoblin import messages, mg_globals -from mediagoblin.db.models import (MediaEntry, MediaTag, Collection, - CollectionItem, LocalUser, Activity) +from mediagoblin.db.models import (MediaEntry, MediaTag, Collection, Comment, + CollectionItem, LocalUser, Activity, \ + GenericModelReference) from mediagoblin.tools.response import render_to_response, render_404, \ redirect, redirect_obj from mediagoblin.tools.text import cleaned_markdown_conversion @@ -178,8 +179,7 @@ def media_post_comment(request, media): if not request.method == 'POST': raise MethodNotAllowed() - comment = request.db.MediaComment() - comment.media_entry = media.id + comment = request.db.TextComment() comment.actor = request.user.id comment.content = six.text_type(request.form['comment_content']) @@ -199,6 +199,11 @@ def media_post_comment(request, media): add_comment_subscription(request.user, media) comment.save() + link = request.db.Comment() + link.target = media + link.comment = comment + link.save() + messages.add_message( request, messages.SUCCESS, _('Your comment has been posted!')) @@ -682,15 +687,15 @@ def processing_panel(request): @get_optional_media_comment_by_id def file_a_report(request, media, comment): """ - This view handles the filing of a MediaReport or a CommentReport. + This view handles the filing of a Report. """ if comment is not None: - if not comment.get_media_entry.id == media.id: + if not comment.target().id == media.id: return render_404(request) form = user_forms.CommentReportForm(request.form) - context = {'media': media, - 'comment':comment, + context = {'media': comment.target(), + 'comment':comment.comment(), 'form':form} else: form = user_forms.MediaReportForm(request.form) @@ -700,9 +705,11 @@ def file_a_report(request, media, comment): if request.method == "POST": - report_object = build_report_object(form, + report_object = build_report_object( + form, media_entry=media, - comment=comment) + comment=comment + ) # if the object was built successfully, report_table will not be None if report_object: |