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 .
+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.
-class MediaComment(Base, MediaCommentMixin):
+ 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 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 """
- # Handle changing the reply ID
- if "inReplyTo" in data:
- # Validate that the ID is correct
- try:
- media_id = int(extract_url_arguments(
- url=data["inReplyTo"]["id"],
- urlmap=request.app.url_map
- )["id"])
- except ValueError:
- return False
-
- media = MediaEntry.query.filter_by(id=media_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)
+
+ # Handle changing the reply ID
+ if "inReplyTo" in data:
+ # Validate that the ID is correct
+ try:
+ id = extract_url_arguments(
+ url=data["inReplyTo"]["id"],
+ urlmap=request.app.url_map
+ )["id"]
+ except ValueError:
+ raise False
+
+ public_id = request.urlgen(
+ "mediagoblin.api.object",
+ id=id,
+ object_type=data["inReplyTo"]["objectType"],
+ qualified=True
+ )
+
+ media = MediaEntry.query.filter_by(public_id=public_id).first()
+ if media is None:
+ return False
+
+ # 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)
+
+ 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}
-
- def is_comment_report(self):
- return self.discriminator=='comment_report'
-
- def is_media_entry_report(self):
- return self.discriminator=='media_report'
+
+ 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):
+ if self.object_id is None:
+ return False
+ return isinstance(self.obj(), TextComment)
+
+ def is_media_entry_report(self):
+ 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 .
-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 @@
{% trans %}New comments{% endtrans %}
{% 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() %}