1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 TODO: indexes on foreignkeys, where useful.
21 from __future__
import print_function
26 from sqlalchemy
import Column
, Integer
, Unicode
, UnicodeText
, DateTime
, \
27 Boolean
, ForeignKey
, UniqueConstraint
, PrimaryKeyConstraint
, \
29 from sqlalchemy
.orm
import relationship
, backref
, with_polymorphic
30 from sqlalchemy
.orm
.collections
import attribute_mapped_collection
31 from sqlalchemy
.sql
.expression
import desc
32 from sqlalchemy
.ext
.associationproxy
import association_proxy
33 from sqlalchemy
.util
import memoized_property
35 from mediagoblin
.db
.extratypes
import (PathTupleWithSlashes
, JSONEncoded
,
37 from mediagoblin
.db
.base
import Base
, DictReadAttrProxy
38 from mediagoblin
.db
.mixin
import UserMixin
, MediaEntryMixin
, \
39 MediaCommentMixin
, CollectionMixin
, CollectionItemMixin
, \
41 from mediagoblin
.tools
.files
import delete_media_files
42 from mediagoblin
.tools
.common
import import_component
46 _log
= logging
.getLogger(__name__
)
49 class User(Base
, UserMixin
):
51 TODO: We should consider moving some rarely used fields
52 into some sort of "shadow" table.
54 __tablename__
= "core__users"
56 id = Column(Integer
, primary_key
=True)
57 username
= Column(Unicode
, nullable
=False, unique
=True)
58 # Note: no db uniqueness constraint on email because it's not
59 # reliable (many email systems case insensitive despite against
60 # the RFC) and because it would be a mess to implement at this
62 email
= Column(Unicode
, nullable
=False)
63 pw_hash
= Column(Unicode
)
64 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
65 # Intented to be nullable=False, but migrations would not work for it
66 # set to nullable=True implicitly.
67 wants_comment_notification
= Column(Boolean
, default
=True)
68 wants_notifications
= Column(Boolean
, default
=True)
69 license_preference
= Column(Unicode
)
71 bio
= Column(UnicodeText
) # ??
72 uploaded
= Column(Integer
, default
=0)
73 upload_limit
= Column(Integer
)
75 activity
= Column(Integer
, ForeignKey("core__activity_intermediators.id"))
78 # plugin data would be in a separate model
81 return '<{0} #{1} {2} {3} "{4}">'.format(
82 self
.__class
__.__name
__,
84 'verified' if self
.has_privilege(u
'active') else 'non-verified',
85 'admin' if self
.has_privilege(u
'admin') else 'user',
88 def delete(self
, **kwargs
):
89 """Deletes a User and all related entries/comments/files/..."""
90 # Collections get deleted by relationships.
92 media_entries
= MediaEntry
.query
.filter(MediaEntry
.uploader
== self
.id)
93 for media
in media_entries
:
94 # TODO: Make sure that "MediaEntry.delete()" also deletes
95 # all related files/Comments
96 media
.delete(del_orphan_tags
=False, commit
=False)
98 # Delete now unused tags
99 # TODO: import here due to cyclic imports!!! This cries for refactoring
100 from mediagoblin
.db
.util
import clean_orphan_tags
101 clean_orphan_tags(commit
=False)
103 # Delete user, pass through commit=False/True in kwargs
104 super(User
, self
).delete(**kwargs
)
105 _log
.info('Deleted user "{0}" account'.format(self
.username
))
107 def has_privilege(self
, privilege
, allow_admin
=True):
109 This method checks to make sure a user has all the correct privileges
110 to access a piece of content.
112 :param privilege A unicode object which represent the different
113 privileges which may give the user access to
116 :param allow_admin If this is set to True the then if the user is
117 an admin, then this will always return True
118 even if the user hasn't been given the
119 privilege. (defaults to True)
121 priv
= Privilege
.query
.filter_by(privilege_name
=privilege
).one()
122 if priv
in self
.all_privileges
:
124 elif allow_admin
and self
.has_privilege(u
'admin', allow_admin
=False):
131 Checks if this user is banned.
133 :returns True if self is banned
134 :returns False if self is not
136 return UserBan
.query
.get(self
.id) is not None
139 def serialize(self
, request
):
141 "id": "acct:{0}@{1}".format(self
.username
, request
.host
),
142 "preferredUsername": self
.username
,
143 "displayName": "{0}@{1}".format(self
.username
, request
.host
),
144 "objectType": self
.object_type
,
151 "href": request
.urlgen(
152 "mediagoblin.federation.user.profile",
153 username
=self
.username
,
158 "href": request
.urlgen(
159 "mediagoblin.federation.inbox",
160 username
=self
.username
,
165 "href": request
.urlgen(
166 "mediagoblin.federation.feed",
167 username
=self
.username
,
175 user
.update({"summary": self
.bio
})
177 user
.update({"url": self
.url
})
183 Model representing a client - Used for API Auth
185 __tablename__
= "core__clients"
187 id = Column(Unicode
, nullable
=True, primary_key
=True)
188 secret
= Column(Unicode
, nullable
=False)
189 expirey
= Column(DateTime
, nullable
=True)
190 application_type
= Column(Unicode
, nullable
=False)
191 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
192 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
195 redirect_uri
= Column(JSONEncoded
, nullable
=True)
196 logo_url
= Column(Unicode
, nullable
=True)
197 application_name
= Column(Unicode
, nullable
=True)
198 contacts
= Column(JSONEncoded
, nullable
=True)
201 if self
.application_name
:
202 return "<Client {0} - {1}>".format(self
.application_name
, self
.id)
204 return "<Client {0}>".format(self
.id)
206 class RequestToken(Base
):
208 Model for representing the request tokens
210 __tablename__
= "core__request_tokens"
212 token
= Column(Unicode
, primary_key
=True)
213 secret
= Column(Unicode
, nullable
=False)
214 client
= Column(Unicode
, ForeignKey(Client
.id))
215 user
= Column(Integer
, ForeignKey(User
.id), nullable
=True)
216 used
= Column(Boolean
, default
=False)
217 authenticated
= Column(Boolean
, default
=False)
218 verifier
= Column(Unicode
, nullable
=True)
219 callback
= Column(Unicode
, nullable
=False, default
=u
"oob")
220 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
221 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
223 get_client
= relationship(Client
)
225 class AccessToken(Base
):
227 Model for representing the access tokens
229 __tablename__
= "core__access_tokens"
231 token
= Column(Unicode
, nullable
=False, primary_key
=True)
232 secret
= Column(Unicode
, nullable
=False)
233 user
= Column(Integer
, ForeignKey(User
.id))
234 request_token
= Column(Unicode
, ForeignKey(RequestToken
.token
))
235 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
236 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
238 get_requesttoken
= relationship(RequestToken
)
241 class NonceTimestamp(Base
):
243 A place the timestamp and nonce can be stored - this is for OAuth1
245 __tablename__
= "core__nonce_timestamps"
247 nonce
= Column(Unicode
, nullable
=False, primary_key
=True)
248 timestamp
= Column(DateTime
, nullable
=False, primary_key
=True)
250 class MediaEntry(Base
, MediaEntryMixin
):
252 TODO: Consider fetching the media_files using join
254 __tablename__
= "core__media_entries"
256 id = Column(Integer
, primary_key
=True)
257 uploader
= Column(Integer
, ForeignKey(User
.id), nullable
=False, index
=True)
258 title
= Column(Unicode
, nullable
=False)
259 slug
= Column(Unicode
)
260 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
,
262 description
= Column(UnicodeText
) # ??
263 media_type
= Column(Unicode
, nullable
=False)
264 state
= Column(Unicode
, default
=u
'unprocessed', nullable
=False)
265 # or use sqlalchemy.types.Enum?
266 license
= Column(Unicode
)
267 file_size
= Column(Integer
, default
=0)
269 fail_error
= Column(Unicode
)
270 fail_metadata
= Column(JSONEncoded
)
272 transcoding_progress
= Column(SmallInteger
)
274 queued_media_file
= Column(PathTupleWithSlashes
)
276 queued_task_id
= Column(Unicode
)
279 UniqueConstraint('uploader', 'slug'),
282 get_uploader
= relationship(User
)
284 media_files_helper
= relationship("MediaFile",
285 collection_class
=attribute_mapped_collection("name"),
286 cascade
="all, delete-orphan"
288 media_files
= association_proxy('media_files_helper', 'file_path',
289 creator
=lambda k
, v
: MediaFile(name
=k
, file_path
=v
)
292 attachment_files_helper
= relationship("MediaAttachmentFile",
293 cascade
="all, delete-orphan",
294 order_by
="MediaAttachmentFile.created"
296 attachment_files
= association_proxy("attachment_files_helper", "dict_view",
297 creator
=lambda v
: MediaAttachmentFile(
298 name
=v
["name"], filepath
=v
["filepath"])
301 tags_helper
= relationship("MediaTag",
302 cascade
="all, delete-orphan" # should be automatically deleted
304 tags
= association_proxy("tags_helper", "dict_view",
305 creator
=lambda v
: MediaTag(name
=v
["name"], slug
=v
["slug"])
308 collections_helper
= relationship("CollectionItem",
309 cascade
="all, delete-orphan"
311 collections
= association_proxy("collections_helper", "in_collection")
312 media_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
),
313 default
=MutationDict())
315 activity
= Column(Integer
, ForeignKey("core__activity_intermediators.id"))
320 def get_comments(self
, ascending
=False):
321 order_col
= MediaComment
.created
323 order_col
= desc(order_col
)
324 return self
.all_comments
.order_by(order_col
)
326 def url_to_prev(self
, urlgen
):
327 """get the next 'newer' entry by this user"""
328 media
= MediaEntry
.query
.filter(
329 (MediaEntry
.uploader
== self
.uploader
)
330 & (MediaEntry
.state
== u
'processed')
331 & (MediaEntry
.id > self
.id)).order_by(MediaEntry
.id).first()
333 if media
is not None:
334 return media
.url_for_self(urlgen
)
336 def url_to_next(self
, urlgen
):
337 """get the next 'older' entry by this user"""
338 media
= MediaEntry
.query
.filter(
339 (MediaEntry
.uploader
== self
.uploader
)
340 & (MediaEntry
.state
== u
'processed')
341 & (MediaEntry
.id < self
.id)).order_by(desc(MediaEntry
.id)).first()
343 if media
is not None:
344 return media
.url_for_self(urlgen
)
346 def get_file_metadata(self
, file_key
, metadata_key
=None):
348 Return the file_metadata dict of a MediaFile. If metadata_key is given,
349 return the value of the key.
351 media_file
= MediaFile
.query
.filter_by(media_entry
=self
.id,
352 name
=six
.text_type(file_key
)).first()
356 return media_file
.file_metadata
.get(metadata_key
, None)
358 return media_file
.file_metadata
360 def set_file_metadata(self
, file_key
, **kwargs
):
362 Update the file_metadata of a MediaFile.
364 media_file
= MediaFile
.query
.filter_by(media_entry
=self
.id,
365 name
=six
.text_type(file_key
)).first()
367 file_metadata
= media_file
.file_metadata
or {}
369 for key
, value
in six
.iteritems(kwargs
):
370 file_metadata
[key
] = value
372 media_file
.file_metadata
= file_metadata
376 def media_data(self
):
377 return getattr(self
, self
.media_data_ref
)
379 def media_data_init(self
, **kwargs
):
381 Initialize or update the contents of a media entry's media_data row
383 media_data
= self
.media_data
385 if media_data
is None:
386 # Get the correct table:
387 table
= import_component(self
.media_type
+ '.models:DATA_MODEL')
388 # No media data, so actually add a new one
389 media_data
= table(**kwargs
)
390 # Get the relationship set up.
391 media_data
.get_media_entry
= self
393 # Update old media data
394 for field
, value
in six
.iteritems(kwargs
):
395 setattr(media_data
, field
, value
)
398 def media_data_ref(self
):
399 return import_component(self
.media_type
+ '.models:BACKREF_NAME')
402 safe_title
= self
.title
.encode('ascii', 'replace')
404 return '<{classname} {id}: {title}>'.format(
405 classname
=self
.__class
__.__name
__,
409 def delete(self
, del_orphan_tags
=True, **kwargs
):
410 """Delete MediaEntry and all related files/attachments/comments
412 This will *not* automatically delete unused collections, which
415 :param del_orphan_tags: True/false if we delete unused Tags too
416 :param commit: True/False if this should end the db transaction"""
417 # User's CollectionItems are automatically deleted via "cascade".
418 # Comments on this Media are deleted by cascade, hopefully.
420 # Delete all related files/attachments
422 delete_media_files(self
)
423 except OSError as error
:
424 # Returns list of files we failed to delete
425 _log
.error('No such files from the user "{1}" to delete: '
426 '{0}'.format(str(error
), self
.get_uploader
))
427 _log
.info('Deleted Media entry id "{0}"'.format(self
.id))
428 # Related MediaTag's are automatically cleaned, but we might
429 # want to clean out unused Tag's too.
431 # TODO: Import here due to cyclic imports!!!
432 # This cries for refactoring
433 from mediagoblin
.db
.util
import clean_orphan_tags
434 clean_orphan_tags(commit
=False)
435 # pass through commit=False/True in kwargs
436 super(MediaEntry
, self
).delete(**kwargs
)
438 def serialize(self
, request
, show_comments
=True):
439 """ Unserialize MediaEntry to object """
440 author
= self
.get_uploader
443 "author": author
.serialize(request
),
444 "objectType": self
.object_type
,
445 "url": self
.url_for_self(request
.urlgen
),
447 "url": request
.host_url
+ self
.thumb_url
[1:],
450 "url": request
.host_url
+ self
.original_url
[1:],
452 "published": self
.created
.isoformat(),
453 "updated": self
.created
.isoformat(),
459 "href": request
.urlgen(
460 "mediagoblin.federation.object",
461 object_type
=self
.object_type
,
471 context
["displayName"] = self
.title
474 context
["content"] = self
.description
477 context
["license"] = self
.license
481 comment
.serialize(request
) for comment
in self
.get_comments()]
482 total
= len(comments
)
483 context
["replies"] = {
486 "url": request
.urlgen(
487 "mediagoblin.federation.object.comments",
488 object_type
=self
.object_type
,
496 def unserialize(self
, data
):
497 """ Takes API objects and unserializes on existing MediaEntry """
498 if "displayName" in data
:
499 self
.title
= data
["displayName"]
501 if "content" in data
:
502 self
.description
= data
["content"]
504 if "license" in data
:
505 self
.license
= data
["license"]
509 class FileKeynames(Base
):
511 keywords for various places.
512 currently the MediaFile keys
514 __tablename__
= "core__file_keynames"
515 id = Column(Integer
, primary_key
=True)
516 name
= Column(Unicode
, unique
=True)
519 return "<FileKeyname %r: %r>" % (self
.id, self
.name
)
522 def find_or_new(cls
, name
):
523 t
= cls
.query
.filter_by(name
=name
).first()
526 return cls(name
=name
)
529 class MediaFile(Base
):
531 TODO: Highly consider moving "name" into a new table.
532 TODO: Consider preloading said table in software
534 __tablename__
= "core__mediafiles"
536 media_entry
= Column(
537 Integer
, ForeignKey(MediaEntry
.id),
539 name_id
= Column(SmallInteger
, ForeignKey(FileKeynames
.id), nullable
=False)
540 file_path
= Column(PathTupleWithSlashes
)
541 file_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
))
544 PrimaryKeyConstraint('media_entry', 'name_id'),
548 return "<MediaFile %s: %r>" % (self
.name
, self
.file_path
)
550 name_helper
= relationship(FileKeynames
, lazy
="joined", innerjoin
=True)
551 name
= association_proxy('name_helper', 'name',
552 creator
=FileKeynames
.find_or_new
556 class MediaAttachmentFile(Base
):
557 __tablename__
= "core__attachment_files"
559 id = Column(Integer
, primary_key
=True)
560 media_entry
= Column(
561 Integer
, ForeignKey(MediaEntry
.id),
563 name
= Column(Unicode
, nullable
=False)
564 filepath
= Column(PathTupleWithSlashes
)
565 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
569 """A dict like view on this object"""
570 return DictReadAttrProxy(self
)
574 __tablename__
= "core__tags"
576 id = Column(Integer
, primary_key
=True)
577 slug
= Column(Unicode
, nullable
=False, unique
=True)
580 return "<Tag %r: %r>" % (self
.id, self
.slug
)
583 def find_or_new(cls
, slug
):
584 t
= cls
.query
.filter_by(slug
=slug
).first()
587 return cls(slug
=slug
)
590 class MediaTag(Base
):
591 __tablename__
= "core__media_tags"
593 id = Column(Integer
, primary_key
=True)
594 media_entry
= Column(
595 Integer
, ForeignKey(MediaEntry
.id),
596 nullable
=False, index
=True)
597 tag
= Column(Integer
, ForeignKey(Tag
.id), nullable
=False, index
=True)
598 name
= Column(Unicode
)
599 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
602 UniqueConstraint('tag', 'media_entry'),
605 tag_helper
= relationship(Tag
)
606 slug
= association_proxy('tag_helper', 'slug',
607 creator
=Tag
.find_or_new
610 def __init__(self
, name
=None, slug
=None):
615 self
.tag_helper
= Tag
.find_or_new(slug
)
619 """A dict like view on this object"""
620 return DictReadAttrProxy(self
)
623 class MediaComment(Base
, MediaCommentMixin
):
624 __tablename__
= "core__media_comments"
626 id = Column(Integer
, primary_key
=True)
627 media_entry
= Column(
628 Integer
, ForeignKey(MediaEntry
.id), nullable
=False, index
=True)
629 author
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
630 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
631 content
= Column(UnicodeText
, nullable
=False)
633 # Cascade: Comments are owned by their creator. So do the full thing.
634 # lazy=dynamic: People might post a *lot* of comments,
635 # so make the "posted_comments" a query-like thing.
636 get_author
= relationship(User
,
637 backref
=backref("posted_comments",
639 cascade
="all, delete-orphan"))
640 get_entry
= relationship(MediaEntry
,
641 backref
=backref("comments",
643 cascade
="all, delete-orphan"))
645 # Cascade: Comments are somewhat owned by their MediaEntry.
646 # So do the full thing.
647 # lazy=dynamic: MediaEntries might have many comments,
648 # so make the "all_comments" a query-like thing.
649 get_media_entry
= relationship(MediaEntry
,
650 backref
=backref("all_comments",
652 cascade
="all, delete-orphan"))
655 activity
= Column(Integer
, ForeignKey("core__activity_intermediators.id"))
657 def serialize(self
, request
):
658 """ Unserialize to python dictionary for API """
659 media
= MediaEntry
.query
.filter_by(id=self
.media_entry
).first()
660 author
= self
.get_author
663 "objectType": self
.object_type
,
664 "content": self
.content
,
665 "inReplyTo": media
.serialize(request
, show_comments
=False),
666 "author": author
.serialize(request
)
671 def unserialize(self
, data
):
672 """ Takes API objects and unserializes on existing comment """
673 # Do initial checks to verify the object is correct
674 required_attributes
= ["content", "inReplyTo"]
675 for attr
in required_attributes
:
679 # Validate inReplyTo has ID
680 if "id" not in data
["inReplyTo"]:
683 # Validate that the ID is correct
685 media_id
= int(data
["inReplyTo"]["id"])
689 media
= MediaEntry
.query
.filter_by(id=media_id
).first()
693 self
.media_entry
= media
.id
694 self
.content
= data
["content"]
699 class Collection(Base
, CollectionMixin
):
700 """An 'album' or 'set' of media by a user.
702 On deletion, contained CollectionItems get automatically reaped via
704 __tablename__
= "core__collections"
706 id = Column(Integer
, primary_key
=True)
707 title
= Column(Unicode
, nullable
=False)
708 slug
= Column(Unicode
)
709 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
,
711 description
= Column(UnicodeText
)
712 creator
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
713 # TODO: No of items in Collection. Badly named, can we migrate to num_items?
714 items
= Column(Integer
, default
=0)
716 # Cascade: Collections are owned by their creator. So do the full thing.
717 get_creator
= relationship(User
,
718 backref
=backref("collections",
719 cascade
="all, delete-orphan"))
721 activity
= Column(Integer
, ForeignKey("core__activity_intermediators.id"))
724 UniqueConstraint('creator', 'slug'),
727 def get_collection_items(self
, ascending
=False):
728 #TODO, is this still needed with self.collection_items being available?
729 order_col
= CollectionItem
.position
731 order_col
= desc(order_col
)
732 return CollectionItem
.query
.filter_by(
733 collection
=self
.id).order_by(order_col
)
736 safe_title
= self
.title
.encode('ascii', 'replace')
737 return '<{classname} #{id}: {title} by {creator}>'.format(
739 classname
=self
.__class
__.__name
__,
740 creator
=self
.creator
,
744 class CollectionItem(Base
, CollectionItemMixin
):
745 __tablename__
= "core__collection_items"
747 id = Column(Integer
, primary_key
=True)
748 media_entry
= Column(
749 Integer
, ForeignKey(MediaEntry
.id), nullable
=False, index
=True)
750 collection
= Column(Integer
, ForeignKey(Collection
.id), nullable
=False)
751 note
= Column(UnicodeText
, nullable
=True)
752 added
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
753 position
= Column(Integer
)
755 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
756 in_collection
= relationship(Collection
,
759 cascade
="all, delete-orphan"))
761 get_media_entry
= relationship(MediaEntry
)
764 UniqueConstraint('collection', 'media_entry'),
769 """A dict like view on this object"""
770 return DictReadAttrProxy(self
)
773 return '<{classname} #{id}: Entry {entry} in {collection}>'.format(
775 classname
=self
.__class
__.__name
__,
776 collection
=self
.collection
,
777 entry
=self
.media_entry
)
780 class ProcessingMetaData(Base
):
781 __tablename__
= 'core__processing_metadata'
783 id = Column(Integer
, primary_key
=True)
784 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False,
786 media_entry
= relationship(MediaEntry
,
787 backref
=backref('processing_metadata',
788 cascade
='all, delete-orphan'))
789 callback_url
= Column(Unicode
)
793 """A dict like view on this object"""
794 return DictReadAttrProxy(self
)
797 class CommentSubscription(Base
):
798 __tablename__
= 'core__comment_subscriptions'
799 id = Column(Integer
, primary_key
=True)
801 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
803 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False)
804 media_entry
= relationship(MediaEntry
,
805 backref
=backref('comment_subscriptions',
806 cascade
='all, delete-orphan'))
808 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
809 user
= relationship(User
,
810 backref
=backref('comment_subscriptions',
811 cascade
='all, delete-orphan'))
813 notify
= Column(Boolean
, nullable
=False, default
=True)
814 send_email
= Column(Boolean
, nullable
=False, default
=True)
817 return ('<{classname} #{id}: {user} {media} notify: '
818 '{notify} email: {email}>').format(
820 classname
=self
.__class
__.__name
__,
822 media
=self
.media_entry
,
824 email
=self
.send_email
)
827 class Notification(Base
):
828 __tablename__
= 'core__notifications'
829 id = Column(Integer
, primary_key
=True)
830 type = Column(Unicode
)
832 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
834 user_id
= Column(Integer
, ForeignKey('core__users.id'), nullable
=False,
836 seen
= Column(Boolean
, default
=lambda: False, index
=True)
839 backref
=backref('notifications', cascade
='all, delete-orphan'))
842 'polymorphic_identity': 'notification',
843 'polymorphic_on': type
847 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
849 klass
=self
.__class
__.__name
__,
851 subject
=getattr(self
, 'subject', None),
852 seen
='unseen' if not self
.seen
else 'seen')
854 def __unicode__(self
):
855 return u
'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
857 klass
=self
.__class
__.__name
__,
859 subject
=getattr(self
, 'subject', None),
860 seen
='unseen' if not self
.seen
else 'seen')
863 class CommentNotification(Notification
):
864 __tablename__
= 'core__comment_notifications'
865 id = Column(Integer
, ForeignKey(Notification
.id), primary_key
=True)
867 subject_id
= Column(Integer
, ForeignKey(MediaComment
.id))
868 subject
= relationship(
870 backref
=backref('comment_notifications', cascade
='all, delete-orphan'))
873 'polymorphic_identity': 'comment_notification'
877 class ProcessingNotification(Notification
):
878 __tablename__
= 'core__processing_notifications'
880 id = Column(Integer
, ForeignKey(Notification
.id), primary_key
=True)
882 subject_id
= Column(Integer
, ForeignKey(MediaEntry
.id))
883 subject
= relationship(
885 backref
=backref('processing_notifications',
886 cascade
='all, delete-orphan'))
889 'polymorphic_identity': 'processing_notification'
894 [ProcessingNotification
, CommentNotification
])
896 class ReportBase(Base
):
898 This is the basic report object which the other reports are based off of.
900 :keyword reporter_id Holds the id of the user who created
901 the report, as an Integer column.
902 :keyword report_content Hold the explanation left by the repor-
903 -ter to indicate why they filed the
904 report in the first place, as a
906 :keyword reported_user_id Holds the id of the user who created
907 the content which was reported, as
909 :keyword created Holds a datetime column of when the re-
911 :keyword discriminator This column distinguishes between the
912 different types of reports.
913 :keyword resolver_id Holds the id of the moderator/admin who
915 :keyword resolved Holds the DateTime object which descri-
916 -bes when this report was resolved
917 :keyword result Holds the UnicodeText column of the
918 resolver's reasons for resolving
919 the report this way. Some of this
922 __tablename__
= 'core__reports'
923 id = Column(Integer
, primary_key
=True)
924 reporter_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
925 reporter
= relationship(
927 backref
=backref("reports_filed_by",
929 cascade
="all, delete-orphan"),
930 primaryjoin
="User.id==ReportBase.reporter_id")
931 report_content
= Column(UnicodeText
)
932 reported_user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
933 reported_user
= relationship(
935 backref
=backref("reports_filed_on",
937 cascade
="all, delete-orphan"),
938 primaryjoin
="User.id==ReportBase.reported_user_id")
939 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now())
940 discriminator
= Column('type', Unicode(50))
941 resolver_id
= Column(Integer
, ForeignKey(User
.id))
942 resolver
= relationship(
944 backref
=backref("reports_resolved_by",
946 cascade
="all, delete-orphan"),
947 primaryjoin
="User.id==ReportBase.resolver_id")
949 resolved
= Column(DateTime
)
950 result
= Column(UnicodeText
)
951 __mapper_args__
= {'polymorphic_on': discriminator
}
953 def is_comment_report(self
):
954 return self
.discriminator
=='comment_report'
956 def is_media_entry_report(self
):
957 return self
.discriminator
=='media_report'
959 def is_archived_report(self
):
960 return self
.resolved
is not None
962 def archive(self
,resolver_id
, resolved
, result
):
963 self
.resolver_id
= resolver_id
964 self
.resolved
= resolved
968 class CommentReport(ReportBase
):
970 Reports that have been filed on comments.
971 :keyword comment_id Holds the integer value of the reported
974 __tablename__
= 'core__reports_on_comments'
975 __mapper_args__
= {'polymorphic_identity': 'comment_report'}
977 id = Column('id',Integer
, ForeignKey('core__reports.id'),
979 comment_id
= Column(Integer
, ForeignKey(MediaComment
.id), nullable
=True)
980 comment
= relationship(
981 MediaComment
, backref
=backref("reports_filed_on",
985 class MediaReport(ReportBase
):
987 Reports that have been filed on media entries
988 :keyword media_entry_id Holds the integer value of the reported
991 __tablename__
= 'core__reports_on_media'
992 __mapper_args__
= {'polymorphic_identity': 'media_report'}
994 id = Column('id',Integer
, ForeignKey('core__reports.id'),
996 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=True)
997 media_entry
= relationship(
999 backref
=backref("reports_filed_on",
1002 class UserBan(Base
):
1004 Holds the information on a specific user's ban-state. As long as one of
1005 these is attached to a user, they are banned from accessing mediagoblin.
1006 When they try to log in, they are greeted with a page that tells them
1007 the reason why they are banned and when (if ever) the ban will be
1010 :keyword user_id Holds the id of the user this object is
1011 attached to. This is a one-to-one
1013 :keyword expiration_date Holds the date that the ban will be lifted.
1014 If this is null, the ban is permanent
1015 unless a moderator manually lifts it.
1016 :keyword reason Holds the reason why the user was banned.
1018 __tablename__
= 'core__user_bans'
1020 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False,
1022 expiration_date
= Column(Date
)
1023 reason
= Column(UnicodeText
, nullable
=False)
1026 class Privilege(Base
):
1028 The Privilege table holds all of the different privileges a user can hold.
1029 If a user 'has' a privilege, the User object is in a relationship with the
1032 :keyword privilege_name Holds a unicode object that is the recognizable
1033 name of this privilege. This is the column
1034 used for identifying whether or not a user
1035 has a necessary privilege or not.
1038 __tablename__
= 'core__privileges'
1040 id = Column(Integer
, nullable
=False, primary_key
=True)
1041 privilege_name
= Column(Unicode
, nullable
=False, unique
=True)
1042 all_users
= relationship(
1044 backref
='all_privileges',
1045 secondary
="core__privileges_users")
1047 def __init__(self
, privilege_name
):
1049 Currently consructors are required for tables that are initialized thru
1050 the FOUNDATIONS system. This is because they need to be able to be con-
1051 -structed by a list object holding their arg*s
1053 self
.privilege_name
= privilege_name
1056 return "<Privilege %s>" % (self
.privilege_name
)
1059 class PrivilegeUserAssociation(Base
):
1061 This table holds the many-to-many relationship between User and Privilege
1064 __tablename__
= 'core__privileges_users'
1069 ForeignKey(User
.id),
1074 ForeignKey(Privilege
.id),
1077 class Generator(Base
):
1078 """ Information about what created an activity """
1079 __tablename__
= "core__generators"
1081 id = Column(Integer
, primary_key
=True)
1082 name
= Column(Unicode
, nullable
=False)
1083 published
= Column(DateTime
, default
=datetime
.datetime
.now
)
1084 updated
= Column(DateTime
, default
=datetime
.datetime
.now
)
1085 object_type
= Column(Unicode
, nullable
=False)
1088 return "<{klass} {name}>".format(
1089 klass
=self
.__class
__.__name
__,
1093 def serialize(self
, request
):
1096 "displayName": self
.name
,
1097 "published": self
.published
.isoformat(),
1098 "updated": self
.updated
.isoformat(),
1099 "objectType": self
.object_type
,
1102 def unserialize(self
, data
):
1103 if "displayName" in data
:
1104 self
.name
= data
["displayName"]
1107 class ActivityIntermediator(Base
):
1109 This is used so that objects/targets can have a foreign key back to this
1110 object and activities can a foreign key to this object. This objects to be
1111 used multiple times for the activity object or target and also allows for
1112 different types of objects to be used as an Activity.
1114 __tablename__
= "core__activity_intermediators"
1116 id = Column(Integer
, primary_key
=True)
1117 type = Column(Unicode
, nullable
=False)
1121 "media": MediaEntry
,
1122 "comment": MediaComment
,
1123 "collection": Collection
,
1126 def _find_model(self
, obj
):
1127 """ Finds the model for a given object """
1128 for key
, model
in self
.TYPES
.items():
1129 if isinstance(obj
, model
):
1135 """ This sets itself as the activity """
1136 key
, model
= self
._find
_model
(obj
)
1138 raise ValueError("Invalid type of object given")
1140 # We need to save so that self.id is populated
1144 # First set self as activity
1145 obj
.activity
= self
.id
1149 """ Finds the object for an activity """
1150 if self
.type is None:
1153 model
= self
.TYPES
[self
.type]
1154 return model
.query
.filter_by(activity
=self
.id).first()
1156 def save(self
, *args
, **kwargs
):
1157 if self
.type not in self
.TYPES
.keys():
1158 raise ValueError("Invalid type set")
1159 Base
.save(self
, *args
, **kwargs
)
1161 class Activity(Base
, ActivityMixin
):
1163 This holds all the metadata about an activity such as uploading an image,
1164 posting a comment, etc.
1166 __tablename__
= "core__activities"
1168 id = Column(Integer
, primary_key
=True)
1169 actor
= Column(Integer
,
1170 ForeignKey("core__users.id"),
1172 published
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
1173 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
1174 verb
= Column(Unicode
, nullable
=False)
1175 content
= Column(Unicode
, nullable
=True)
1176 title
= Column(Unicode
, nullable
=True)
1177 generator
= Column(Integer
,
1178 ForeignKey("core__generators.id"),
1180 object = Column(Integer
,
1181 ForeignKey("core__activity_intermediators.id"),
1183 target
= Column(Integer
,
1184 ForeignKey("core__activity_intermediators.id"),
1187 get_actor
= relationship(User
,
1188 foreign_keys
="Activity.actor", post_update
=True)
1189 get_generator
= relationship(Generator
)
1192 if self
.content
is None:
1193 return "<{klass} verb:{verb}>".format(
1194 klass
=self
.__class
__.__name
__,
1198 return "<{klass} {content}>".format(
1199 klass
=self
.__class
__.__name
__,
1200 content
=self
.content
1204 def get_object(self
):
1205 if self
.object is None:
1208 ai
= ActivityIntermediator
.query
.filter_by(id=self
.object).first()
1211 def set_object(self
, obj
):
1212 self
.object = self
._set
_model
(obj
)
1215 def get_target(self
):
1216 if self
.target
is None:
1219 ai
= ActivityIntermediator
.query
.filter_by(id=self
.target
).first()
1222 def set_target(self
, obj
):
1223 self
.target
= self
._set
_model
(obj
)
1225 def _set_model(self
, obj
):
1226 # Firstly can we set obj
1227 if not hasattr(obj
, "activity"):
1229 "{0!r} is unable to be set on activity".format(obj
))
1231 if obj
.activity
is None:
1232 # We need to create a new AI
1233 ai
= ActivityIntermediator()
1238 # Okay we should have an existing AI
1239 return ActivityIntermediator
.query
.filter_by(id=obj
.activity
).first().id
1241 def save(self
, set_updated
=True, *args
, **kwargs
):
1243 self
.updated
= datetime
.datetime
.now()
1244 super(Activity
, self
).save(*args
, **kwargs
)
1247 User
, MediaEntry
, Tag
, MediaTag
, MediaComment
, Collection
, CollectionItem
,
1248 MediaFile
, FileKeynames
, MediaAttachmentFile
, ProcessingMetaData
,
1249 Notification
, CommentNotification
, ProcessingNotification
, Client
,
1250 CommentSubscription
, ReportBase
, CommentReport
, MediaReport
, UserBan
,
1251 Privilege
, PrivilegeUserAssociation
,
1252 RequestToken
, AccessToken
, NonceTimestamp
,
1253 Activity
, ActivityIntermediator
, Generator
]
1256 Foundations are the default rows that are created immediately after the tables
1257 are initialized. Each entry to this dictionary should be in the format of:
1258 ModelConstructorObject:List of Dictionaries
1259 (Each Dictionary represents a row on the Table to be created, containing each
1260 of the columns' names as a key string, and each of the columns' values as a
1263 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1264 user_foundations = [{'name':u'Joanna', 'age':24},
1265 {'name':u'Andrea', 'age':41}]
1267 FOUNDATIONS = {User:user_foundations}
1269 privilege_foundations
= [{'privilege_name':u
'admin'},
1270 {'privilege_name':u
'moderator'},
1271 {'privilege_name':u
'uploader'},
1272 {'privilege_name':u
'reporter'},
1273 {'privilege_name':u
'commenter'},
1274 {'privilege_name':u
'active'}]
1275 FOUNDATIONS
= {Privilege
:privilege_foundations
}
1277 ######################################################
1278 # Special, migrations-tracking table
1280 # Not listed in MODELS because this is special and not
1281 # really migrated, but used for migrations (for now)
1282 ######################################################
1284 class MigrationData(Base
):
1285 __tablename__
= "core__migrations"
1287 name
= Column(Unicode
, primary_key
=True)
1288 version
= Column(Integer
, nullable
=False, default
=0)
1290 ######################################################
1293 def show_table_init(engine_uri
):
1294 if engine_uri
is None:
1295 engine_uri
= 'sqlite:///:memory:'
1296 from sqlalchemy
import create_engine
1297 engine
= create_engine(engine_uri
, echo
=True)
1299 Base
.metadata
.create_all(engine
)
1302 if __name__
== '__main__':
1303 from sys
import argv
1309 show_table_init(uri
)