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
import six
+from six.moves.urllib.parse import urljoin
from pytz import UTC
_log = logging.getLogger(__name__)
'polymorphic_on': type,
}
- def delete(self, **kwargs):
+ deletion_mode = Base.SOFT_DELETE
+
+ def soft_delete(self, *args, **kwargs):
+ # Find all the Collections and delete those
+ for collection in Collection.query.filter_by(actor=self.id):
+ collection.delete(**kwargs)
+
+ # Find all the comments and delete those too
+ for comment in TextComment.query.filter_by(actor=self.id):
+ comment.delete(**kwargs)
+
+ # Find all the activities and delete those too
+ for activity in Activity.query.filter_by(actor=self.id):
+ activity.delete(**kwargs)
+
+ super(User, self).soft_delete(*args, **kwargs)
+
+
+ def delete(self, *args, **kwargs):
"""Deletes a User and all related entries/comments/files/..."""
# Collections get deleted by relationships.
# Delete user, pass through commit=False/True in kwargs
username = self.username
- super(User, self).delete(**kwargs)
+ super(User, self).delete(*args, **kwargs)
_log.info('Deleted user "{0}" account'.format(username))
def has_privilege(self, privilege, allow_admin=True):
'admin' if self.has_privilege(u'admin') else 'user',
self.username)
+ def get_public_id(self, host):
+ return "acct:{0}@{1}".format(self.username, host)
+
def serialize(self, request):
user = {
- "id": "acct:{0}@{1}".format(self.username, request.host),
+ "id": self.get_public_id(request.host),
"preferredUsername": self.username,
- "displayName": "{0}@{1}".format(self.username, request.host),
+ "displayName": self.get_public_id(request.host).split(":", 1)[1],
"links": {
"self": {
"href": request.urlgen(
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
"""
UniqueConstraint('actor', 'slug'),
{})
+ deletion_mode = Base.SOFT_DELETE
+
get_actor = relationship(User)
media_files_helper = relationship("MediaFile",
))
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:
+ query = 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(
id=self.id,
title=safe_title)
+ def soft_delete(self, *args, **kwargs):
+ # Find all of the media comments for this and delete them
+ for comment in self.get_comments():
+ comment.delete(*args, **kwargs)
+
+ super(MediaEntry, self).soft_delete(*args, **kwargs)
+
def delete(self, del_orphan_tags=True, **kwargs):
"""Delete MediaEntry and all related files/attachments/comments
author = self.get_actor
published = UTC.localize(self.created)
updated = UTC.localize(self.updated)
- public_id = self.get_public_id(request)
+ public_id = self.get_public_id(request.urlgen)
context = {
"id": public_id,
"author": author.serialize(request),
"objectType": self.object_type,
"url": self.url_for_self(request.urlgen, qualified=True),
"image": {
- "url": request.host_url + self.thumb_url[1:],
+ "url": urljoin(request.host_url, self.thumb_url),
},
"fullImage":{
- "url": request.host_url + self.original_url[1:],
+ "url": urljoin(request.host_url, self.original_url),
},
"published": published.isoformat(),
"updated": updated.isoformat(),
"""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")
backref=backref("posted_comments",
lazy="dynamic",
cascade="all, delete-orphan"))
- get_entry = relationship(MediaEntry,
- backref=backref("comments",
- lazy="dynamic",
- cascade="all, delete-orphan"))
+ deletion_mode = Base.SOFT_DELETE
- # 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"))
+ def soft_delete(self, *args, **kwargs):
+ # Find the GMR for this model.
+ gmr = GenericModelReference.query.filter_by(
+ obj_pk=self.id,
+ model_type=self.__tablename__
+ ).first()
+
+ # Delete the Comment object for this comment
+ Comment.query.filter_by(comment_id=gmr.id).delete()
+ return super(TextComment, self).soft_delete(*args, **kwargs)
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(),
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
__tablename__ = "core__collections"
id = Column(Integer, primary_key=True)
+ public_id = Column(Unicode, unique=True)
title = Column(Unicode, nullable=False)
slug = Column(Unicode)
created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow,
backref=backref("collections",
cascade="all, delete-orphan"))
__table_args__ = (
- UniqueConstraint('actor', 'slug'),
+ UniqueConstraint("actor", "slug"),
{})
+ deletion_mode = Base.SOFT_DELETE
+
# These are the types, It's strongly suggested if new ones are invented they
# are prefixed to ensure they're unique from other types. Any types used in
# the main mediagoblin should be prefixed "core-"
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):
# Get all serialized output in a list
items = [i.serialize(request) for i in self.get_collection_items()]
return {
- "totalItems": self.items,
+ "totalItems": self.num_items,
"url": self.url_for_self(request.urlgen, qualified=True),
"items": items,
}
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(
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.
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-
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(
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(
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=True)
+ 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
updated = Column(DateTime, default=datetime.datetime.utcnow)
object_type = Column(Unicode, nullable=False)
+ deletion_mode = Base.SOFT_DELETE
+
def __repr__(self):
return "<{klass} {name}>".format(
klass=self.__class__.__name__,
__tablename__ = "core__activities"
id = Column(Integer, primary_key=True)
+ public_id = Column(Unicode, unique=True)
actor = Column(Integer,
ForeignKey("core__users.id"),
nullable=False)
published = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
+
verb = Column(Unicode, nullable=False)
content = Column(Unicode, nullable=True)
title = Column(Unicode, nullable=True)
cascade="all, delete-orphan"))
get_generator = relationship(Generator)
+ deletion_mode = Base.SOFT_DELETE
+
def __repr__(self):
if self.content is None:
return "<{klass} verb:{verb}>".format(
self.updated = datetime.datetime.now()
super(Activity, self).save(*args, **kwargs)
-with_polymorphic(
- Notification,
- [ProcessingNotification, CommentNotification])
+class Graveyard(Base):
+ """ Where models come to die """
+ __tablename__ = "core__graveyard"
+
+ id = Column(Integer, primary_key=True)
+ public_id = Column(Unicode, nullable=True, unique=True)
+
+ deleted = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
+ object_type = Column(Unicode, nullable=False)
+
+ # This could either be a deleted actor or a real actor, this must be
+ # nullable as it we shouldn't have it set for deleted actor
+ actor_id = Column(Integer, ForeignKey(GenericModelReference.id))
+ actor_helper = relationship(GenericModelReference)
+ actor = association_proxy("actor_helper", "get_object",
+ creator=GenericModelReference.find_or_new)
+
+ def __repr__(self):
+ return "<{klass} deleted {obj_type}>".format(
+ klass=type(self).__name__,
+ obj_type=self.object_type
+ )
+ def serialize(self, request):
+ deleted = UTC.localize(self.deleted).isoformat()
+ context = {
+ "id": self.public_id,
+ "objectType": self.object_type,
+ "published": deleted,
+ "updated": deleted,
+ "deleted": deleted,
+ }
+
+ if self.actor_id is not None:
+ context["actor"] = self.actor().serialize(request)
+
+ return context
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]
+ 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