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.
25 from sqlalchemy
import Column
, Integer
, Unicode
, UnicodeText
, DateTime
, \
26 Boolean
, ForeignKey
, UniqueConstraint
, PrimaryKeyConstraint
, \
28 from sqlalchemy
.orm
import relationship
, backref
, with_polymorphic
29 from sqlalchemy
.orm
.collections
import attribute_mapped_collection
30 from sqlalchemy
.sql
.expression
import desc
31 from sqlalchemy
.ext
.associationproxy
import association_proxy
32 from sqlalchemy
.util
import memoized_property
34 from mediagoblin
.db
.extratypes
import (PathTupleWithSlashes
, JSONEncoded
,
36 from mediagoblin
.db
.base
import Base
, DictReadAttrProxy
37 from mediagoblin
.db
.mixin
import UserMixin
, MediaEntryMixin
, \
38 MediaCommentMixin
, CollectionMixin
, CollectionItemMixin
39 from mediagoblin
.tools
.files
import delete_media_files
40 from mediagoblin
.tools
.common
import import_component
42 # It's actually kind of annoying how sqlalchemy-migrate does this, if
43 # I understand it right, but whatever. Anyway, don't remove this :P
45 # We could do migration calls more manually instead of relying on
46 # this import-based meddling...
47 from migrate
import changeset
49 _log
= logging
.getLogger(__name__
)
53 class User(Base
, UserMixin
):
55 TODO: We should consider moving some rarely used fields
56 into some sort of "shadow" table.
58 __tablename__
= "core__users"
60 id = Column(Integer
, primary_key
=True)
61 username
= Column(Unicode
, nullable
=False, unique
=True, index
=True)
62 # Note: no db uniqueness constraint on email because it's not
63 # reliable (many email systems case insensitive despite against
64 # the RFC) and because it would be a mess to implement at this
66 email
= Column(Unicode
, nullable
=False)
67 pw_hash
= Column(Unicode
)
68 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
69 # Intented to be nullable=False, but migrations would not work for it
70 # set to nullable=True implicitly.
71 wants_comment_notification
= Column(Boolean
, default
=True)
72 wants_notifications
= Column(Boolean
, default
=True)
73 license_preference
= Column(Unicode
)
75 bio
= Column(UnicodeText
) # ??
76 uploaded
= Column(Integer
, default
=0)
77 upload_limit
= Column(Integer
)
80 # plugin data would be in a separate model
83 return '<{0} #{1} {2} {3} "{4}">'.format(
84 self
.__class
__.__name
__,
86 'verified' if self
.has_privilege(u
'active') else 'non-verified',
87 'admin' if self
.has_privilege(u
'admin') else 'user',
90 def delete(self
, **kwargs
):
91 """Deletes a User and all related entries/comments/files/..."""
92 # Collections get deleted by relationships.
94 media_entries
= MediaEntry
.query
.filter(MediaEntry
.uploader
== self
.id)
95 for media
in media_entries
:
96 # TODO: Make sure that "MediaEntry.delete()" also deletes
97 # all related files/Comments
98 media
.delete(del_orphan_tags
=False, commit
=False)
100 # Delete now unused tags
101 # TODO: import here due to cyclic imports!!! This cries for refactoring
102 from mediagoblin
.db
.util
import clean_orphan_tags
103 clean_orphan_tags(commit
=False)
105 # Delete user, pass through commit=False/True in kwargs
106 super(User
, self
).delete(**kwargs
)
107 _log
.info('Deleted user "{0}" account'.format(self
.username
))
109 def has_privilege(self
,*priv_names
):
111 This method checks to make sure a user has all the correct privileges
112 to access a piece of content.
114 :param priv_names A variable number of unicode objects which rep-
115 -resent the different privileges which may give
116 the user access to this content. If you pass
117 multiple arguments, the user will be granted
118 access if they have ANY of the privileges
121 if len(priv_names
) == 1:
122 priv
= Privilege
.query
.filter(
123 Privilege
.privilege_name
==priv_names
[0]).one()
124 return (priv
in self
.all_privileges
)
125 elif len(priv_names
) > 1:
126 return self
.has_privilege(priv_names
[0]) or \
127 self
.has_privilege(*priv_names
[1:])
132 Checks if this user is banned.
134 :returns True if self is banned
135 :returns False if self is not
137 return UserBan
.query
.get(self
.id) is not None
140 def serialize(self
, request
):
142 "id": "acct:{0}@{1}".format(self
.username
, request
.host
),
143 "preferredUsername": self
.username
,
144 "displayName": "{0}@{1}".format(self
.username
, request
.host
),
145 "objectType": "person",
152 "href": request
.urlgen(
153 "mediagoblin.federation.user.profile",
154 username
=self
.username
,
159 "href": request
.urlgen(
160 "mediagoblin.federation.inbox",
161 username
=self
.username
,
166 "href": request
.urlgen(
167 "mediagoblin.federation.feed",
168 username
=self
.username
,
176 user
.update({"summary": self
.bio
})
178 user
.update({"url": self
.url
})
184 Model representing a client - Used for API Auth
186 __tablename__
= "core__clients"
188 id = Column(Unicode
, nullable
=True, primary_key
=True)
189 secret
= Column(Unicode
, nullable
=False)
190 expirey
= Column(DateTime
, nullable
=True)
191 application_type
= Column(Unicode
, nullable
=False)
192 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
193 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
196 redirect_uri
= Column(JSONEncoded
, nullable
=True)
197 logo_url
= Column(Unicode
, nullable
=True)
198 application_name
= Column(Unicode
, nullable
=True)
199 contacts
= Column(JSONEncoded
, nullable
=True)
202 if self
.application_name
:
203 return "<Client {0} - {1}>".format(self
.application_name
, self
.id)
205 return "<Client {0}>".format(self
.id)
207 class RequestToken(Base
):
209 Model for representing the request tokens
211 __tablename__
= "core__request_tokens"
213 token
= Column(Unicode
, primary_key
=True)
214 secret
= Column(Unicode
, nullable
=False)
215 client
= Column(Unicode
, ForeignKey(Client
.id))
216 user
= Column(Integer
, ForeignKey(User
.id), nullable
=True)
217 used
= Column(Boolean
, default
=False)
218 authenticated
= Column(Boolean
, default
=False)
219 verifier
= Column(Unicode
, nullable
=True)
220 callback
= Column(Unicode
, nullable
=False, default
=u
"oob")
221 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
222 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
224 class AccessToken(Base
):
226 Model for representing the access tokens
228 __tablename__
= "core__access_tokens"
230 token
= Column(Unicode
, nullable
=False, primary_key
=True)
231 secret
= Column(Unicode
, nullable
=False)
232 user
= Column(Integer
, ForeignKey(User
.id))
233 request_token
= Column(Unicode
, ForeignKey(RequestToken
.token
))
234 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
235 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
238 class NonceTimestamp(Base
):
240 A place the timestamp and nonce can be stored - this is for OAuth1
242 __tablename__
= "core__nonce_timestamps"
244 nonce
= Column(Unicode
, nullable
=False, primary_key
=True)
245 timestamp
= Column(DateTime
, nullable
=False, primary_key
=True)
247 class MediaEntry(Base
, MediaEntryMixin
):
249 TODO: Consider fetching the media_files using join
251 __tablename__
= "core__media_entries"
253 id = Column(Integer
, primary_key
=True)
254 uploader
= Column(Integer
, ForeignKey(User
.id), nullable
=False, index
=True)
255 title
= Column(Unicode
, nullable
=False)
256 slug
= Column(Unicode
)
257 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
,
259 description
= Column(UnicodeText
) # ??
260 media_type
= Column(Unicode
, nullable
=False)
261 state
= Column(Unicode
, default
=u
'unprocessed', nullable
=False)
262 # or use sqlalchemy.types.Enum?
263 license
= Column(Unicode
)
264 file_size
= Column(Integer
, default
=0)
266 fail_error
= Column(Unicode
)
267 fail_metadata
= Column(JSONEncoded
)
269 transcoding_progress
= Column(SmallInteger
)
271 queued_media_file
= Column(PathTupleWithSlashes
)
273 queued_task_id
= Column(Unicode
)
276 UniqueConstraint('uploader', 'slug'),
279 get_uploader
= relationship(User
)
281 media_files_helper
= relationship("MediaFile",
282 collection_class
=attribute_mapped_collection("name"),
283 cascade
="all, delete-orphan"
285 media_files
= association_proxy('media_files_helper', 'file_path',
286 creator
=lambda k
, v
: MediaFile(name
=k
, file_path
=v
)
289 attachment_files_helper
= relationship("MediaAttachmentFile",
290 cascade
="all, delete-orphan",
291 order_by
="MediaAttachmentFile.created"
293 attachment_files
= association_proxy("attachment_files_helper", "dict_view",
294 creator
=lambda v
: MediaAttachmentFile(
295 name
=v
["name"], filepath
=v
["filepath"])
298 tags_helper
= relationship("MediaTag",
299 cascade
="all, delete-orphan" # should be automatically deleted
301 tags
= association_proxy("tags_helper", "dict_view",
302 creator
=lambda v
: MediaTag(name
=v
["name"], slug
=v
["slug"])
305 collections_helper
= relationship("CollectionItem",
306 cascade
="all, delete-orphan"
308 collections
= association_proxy("collections_helper", "in_collection")
309 media_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
),
310 default
=MutationDict())
315 def get_comments(self
, ascending
=False):
316 order_col
= MediaComment
.created
318 order_col
= desc(order_col
)
319 return self
.all_comments
.order_by(order_col
)
321 def url_to_prev(self
, urlgen
):
322 """get the next 'newer' entry by this user"""
323 media
= MediaEntry
.query
.filter(
324 (MediaEntry
.uploader
== self
.uploader
)
325 & (MediaEntry
.state
== u
'processed')
326 & (MediaEntry
.id > self
.id)).order_by(MediaEntry
.id).first()
328 if media
is not None:
329 return media
.url_for_self(urlgen
)
331 def url_to_next(self
, urlgen
):
332 """get the next 'older' entry by this user"""
333 media
= MediaEntry
.query
.filter(
334 (MediaEntry
.uploader
== self
.uploader
)
335 & (MediaEntry
.state
== u
'processed')
336 & (MediaEntry
.id < self
.id)).order_by(desc(MediaEntry
.id)).first()
338 if media
is not None:
339 return media
.url_for_self(urlgen
)
341 def get_file_metadata(self
, file_key
, metadata_key
=None):
343 Return the file_metadata dict of a MediaFile. If metadata_key is given,
344 return the value of the key.
346 media_file
= MediaFile
.query
.filter_by(media_entry
=self
.id,
347 name
=unicode(file_key
)).first()
351 return media_file
.file_metadata
.get(metadata_key
, None)
353 return media_file
.file_metadata
355 def set_file_metadata(self
, file_key
, **kwargs
):
357 Update the file_metadata of a MediaFile.
359 media_file
= MediaFile
.query
.filter_by(media_entry
=self
.id,
360 name
=unicode(file_key
)).first()
362 file_metadata
= media_file
.file_metadata
or {}
364 for key
, value
in kwargs
.iteritems():
365 file_metadata
[key
] = value
367 media_file
.file_metadata
= file_metadata
371 def media_data(self
):
372 return getattr(self
, self
.media_data_ref
)
374 def media_data_init(self
, **kwargs
):
376 Initialize or update the contents of a media entry's media_data row
378 media_data
= self
.media_data
380 if media_data
is None:
381 # Get the correct table:
382 table
= import_component(self
.media_type
+ '.models:DATA_MODEL')
383 # No media data, so actually add a new one
384 media_data
= table(**kwargs
)
385 # Get the relationship set up.
386 media_data
.get_media_entry
= self
388 # Update old media data
389 for field
, value
in kwargs
.iteritems():
390 setattr(media_data
, field
, value
)
393 def media_data_ref(self
):
394 return import_component(self
.media_type
+ '.models:BACKREF_NAME')
397 safe_title
= self
.title
.encode('ascii', 'replace')
399 return '<{classname} {id}: {title}>'.format(
400 classname
=self
.__class
__.__name
__,
404 def delete(self
, del_orphan_tags
=True, **kwargs
):
405 """Delete MediaEntry and all related files/attachments/comments
407 This will *not* automatically delete unused collections, which
410 :param del_orphan_tags: True/false if we delete unused Tags too
411 :param commit: True/False if this should end the db transaction"""
412 # User's CollectionItems are automatically deleted via "cascade".
413 # Comments on this Media are deleted by cascade, hopefully.
415 # Delete all related files/attachments
417 delete_media_files(self
)
418 except OSError, error
:
419 # Returns list of files we failed to delete
420 _log
.error('No such files from the user "{1}" to delete: '
421 '{0}'.format(str(error
), self
.get_uploader
))
422 _log
.info('Deleted Media entry id "{0}"'.format(self
.id))
423 # Related MediaTag's are automatically cleaned, but we might
424 # want to clean out unused Tag's too.
426 # TODO: Import here due to cyclic imports!!!
427 # This cries for refactoring
428 from mediagoblin
.db
.util
import clean_orphan_tags
429 clean_orphan_tags(commit
=False)
430 # pass through commit=False/True in kwargs
431 super(MediaEntry
, self
).delete(**kwargs
)
434 def objectType(self
):
435 """ Converts media_type to pump-like type - don't use internally """
436 return self
.media_type
.split(".")[-1]
438 def serialize(self
, request
, show_comments
=True):
439 """ Unserialize MediaEntry to object """
440 author
= self
.get_uploader
441 url
= request
.urlgen(
442 "mediagoblin.user_pages.media_home",
443 user
=author
.username
,
450 "author": author
.serialize(request
),
451 "objectType": self
.objectType
,
454 "url": request
.host_url
+ self
.thumb_url
[1:],
457 "url": request
.host_url
+ self
.original_url
[1:],
459 "published": self
.created
.isoformat(),
460 "updated": self
.created
.isoformat(),
466 "href": request
.urlgen(
467 "mediagoblin.federation.object",
468 objectType
=self
.objectType
,
478 context
["displayName"] = self
.title
481 context
["content"] = self
.description
484 context
["license"] = self
.license
487 comments
= [comment
.serialize(request
) for comment
in self
.get_comments()]
488 total
= len(comments
)
489 context
["replies"] = {
492 "url": request
.urlgen(
493 "mediagoblin.federation.object.comments",
494 objectType
=self
.objectType
,
502 def unserialize(self
, data
):
503 """ Takes API objects and unserializes on existing MediaEntry """
504 if "displayName" in data
:
505 self
.title
= data
["displayName"]
507 if "content" in data
:
508 self
.description
= data
["content"]
510 if "license" in data
:
511 self
.license
= data
["license"]
515 class FileKeynames(Base
):
517 keywords for various places.
518 currently the MediaFile keys
520 __tablename__
= "core__file_keynames"
521 id = Column(Integer
, primary_key
=True)
522 name
= Column(Unicode
, unique
=True)
525 return "<FileKeyname %r: %r>" % (self
.id, self
.name
)
528 def find_or_new(cls
, name
):
529 t
= cls
.query
.filter_by(name
=name
).first()
532 return cls(name
=name
)
535 class MediaFile(Base
):
537 TODO: Highly consider moving "name" into a new table.
538 TODO: Consider preloading said table in software
540 __tablename__
= "core__mediafiles"
542 media_entry
= Column(
543 Integer
, ForeignKey(MediaEntry
.id),
545 name_id
= Column(SmallInteger
, ForeignKey(FileKeynames
.id), nullable
=False)
546 file_path
= Column(PathTupleWithSlashes
)
547 file_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
))
550 PrimaryKeyConstraint('media_entry', 'name_id'),
554 return "<MediaFile %s: %r>" % (self
.name
, self
.file_path
)
556 name_helper
= relationship(FileKeynames
, lazy
="joined", innerjoin
=True)
557 name
= association_proxy('name_helper', 'name',
558 creator
=FileKeynames
.find_or_new
562 class MediaAttachmentFile(Base
):
563 __tablename__
= "core__attachment_files"
565 id = Column(Integer
, primary_key
=True)
566 media_entry
= Column(
567 Integer
, ForeignKey(MediaEntry
.id),
569 name
= Column(Unicode
, nullable
=False)
570 filepath
= Column(PathTupleWithSlashes
)
571 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
575 """A dict like view on this object"""
576 return DictReadAttrProxy(self
)
580 __tablename__
= "core__tags"
582 id = Column(Integer
, primary_key
=True)
583 slug
= Column(Unicode
, nullable
=False, unique
=True)
586 return "<Tag %r: %r>" % (self
.id, self
.slug
)
589 def find_or_new(cls
, slug
):
590 t
= cls
.query
.filter_by(slug
=slug
).first()
593 return cls(slug
=slug
)
596 class MediaTag(Base
):
597 __tablename__
= "core__media_tags"
599 id = Column(Integer
, primary_key
=True)
600 media_entry
= Column(
601 Integer
, ForeignKey(MediaEntry
.id),
602 nullable
=False, index
=True)
603 tag
= Column(Integer
, ForeignKey(Tag
.id), nullable
=False, index
=True)
604 name
= Column(Unicode
)
605 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
608 UniqueConstraint('tag', 'media_entry'),
611 tag_helper
= relationship(Tag
)
612 slug
= association_proxy('tag_helper', 'slug',
613 creator
=Tag
.find_or_new
616 def __init__(self
, name
=None, slug
=None):
621 self
.tag_helper
= Tag
.find_or_new(slug
)
625 """A dict like view on this object"""
626 return DictReadAttrProxy(self
)
629 class MediaComment(Base
, MediaCommentMixin
):
630 __tablename__
= "core__media_comments"
632 id = Column(Integer
, primary_key
=True)
633 media_entry
= Column(
634 Integer
, ForeignKey(MediaEntry
.id), nullable
=False, index
=True)
635 author
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
636 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
637 content
= Column(UnicodeText
, nullable
=False)
639 # Cascade: Comments are owned by their creator. So do the full thing.
640 # lazy=dynamic: People might post a *lot* of comments,
641 # so make the "posted_comments" a query-like thing.
642 get_author
= relationship(User
,
643 backref
=backref("posted_comments",
645 cascade
="all, delete-orphan"))
646 get_entry
= relationship(MediaEntry
,
647 backref
=backref("comments",
649 cascade
="all, delete-orphan"))
651 # Cascade: Comments are somewhat owned by their MediaEntry.
652 # So do the full thing.
653 # lazy=dynamic: MediaEntries might have many comments,
654 # so make the "all_comments" a query-like thing.
655 get_media_entry
= relationship(MediaEntry
,
656 backref
=backref("all_comments",
658 cascade
="all, delete-orphan"))
660 def serialize(self
, request
):
661 """ Unserialize to python dictionary for API """
662 media
= MediaEntry
.query
.filter_by(id=self
.media_entry
).first()
663 author
= self
.get_author
666 "objectType": "comment",
667 "content": self
.content
,
668 "inReplyTo": media
.serialize(request
, show_comments
=False),
669 "author": author
.serialize(request
)
674 def unserialize(self
, data
):
675 """ Takes API objects and unserializes on existing comment """
676 # Do initial checks to verify the object is correct
677 required_attributes
= ["content", "inReplyTo"]
678 for attr
in required_attributes
:
682 # Validate inReplyTo has ID
683 if "id" not in data
["inReplyTo"]:
686 self
.media_entry
= data
["inReplyTo"]["id"]
687 self
.content
= data
["content"]
692 class Collection(Base
, CollectionMixin
):
693 """An 'album' or 'set' of media by a user.
695 On deletion, contained CollectionItems get automatically reaped via
697 __tablename__
= "core__collections"
699 id = Column(Integer
, primary_key
=True)
700 title
= Column(Unicode
, nullable
=False)
701 slug
= Column(Unicode
)
702 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
,
704 description
= Column(UnicodeText
)
705 creator
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
706 # TODO: No of items in Collection. Badly named, can we migrate to num_items?
707 items
= Column(Integer
, default
=0)
709 # Cascade: Collections are owned by their creator. So do the full thing.
710 get_creator
= relationship(User
,
711 backref
=backref("collections",
712 cascade
="all, delete-orphan"))
715 UniqueConstraint('creator', 'slug'),
718 def get_collection_items(self
, ascending
=False):
719 #TODO, is this still needed with self.collection_items being available?
720 order_col
= CollectionItem
.position
722 order_col
= desc(order_col
)
723 return CollectionItem
.query
.filter_by(
724 collection
=self
.id).order_by(order_col
)
727 class CollectionItem(Base
, CollectionItemMixin
):
728 __tablename__
= "core__collection_items"
730 id = Column(Integer
, primary_key
=True)
731 media_entry
= Column(
732 Integer
, ForeignKey(MediaEntry
.id), nullable
=False, index
=True)
733 collection
= Column(Integer
, ForeignKey(Collection
.id), nullable
=False)
734 note
= Column(UnicodeText
, nullable
=True)
735 added
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
736 position
= Column(Integer
)
738 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
739 in_collection
= relationship(Collection
,
742 cascade
="all, delete-orphan"))
744 get_media_entry
= relationship(MediaEntry
)
747 UniqueConstraint('collection', 'media_entry'),
752 """A dict like view on this object"""
753 return DictReadAttrProxy(self
)
756 class ProcessingMetaData(Base
):
757 __tablename__
= 'core__processing_metadata'
759 id = Column(Integer
, primary_key
=True)
760 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False,
762 media_entry
= relationship(MediaEntry
,
763 backref
=backref('processing_metadata',
764 cascade
='all, delete-orphan'))
765 callback_url
= Column(Unicode
)
769 """A dict like view on this object"""
770 return DictReadAttrProxy(self
)
773 class CommentSubscription(Base
):
774 __tablename__
= 'core__comment_subscriptions'
775 id = Column(Integer
, primary_key
=True)
777 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
779 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False)
780 media_entry
= relationship(MediaEntry
,
781 backref
=backref('comment_subscriptions',
782 cascade
='all, delete-orphan'))
784 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
785 user
= relationship(User
,
786 backref
=backref('comment_subscriptions',
787 cascade
='all, delete-orphan'))
789 notify
= Column(Boolean
, nullable
=False, default
=True)
790 send_email
= Column(Boolean
, nullable
=False, default
=True)
793 return ('<{classname} #{id}: {user} {media} notify: '
794 '{notify} email: {email}>').format(
796 classname
=self
.__class
__.__name
__,
798 media
=self
.media_entry
,
800 email
=self
.send_email
)
803 class Notification(Base
):
804 __tablename__
= 'core__notifications'
805 id = Column(Integer
, primary_key
=True)
806 type = Column(Unicode
)
808 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
810 user_id
= Column(Integer
, ForeignKey('core__users.id'), nullable
=False,
812 seen
= Column(Boolean
, default
=lambda: False, index
=True)
815 backref
=backref('notifications', cascade
='all, delete-orphan'))
818 'polymorphic_identity': 'notification',
819 'polymorphic_on': type
823 return u
'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
825 klass
=self
.__class
__.__name
__,
827 subject
=getattr(self
, 'subject', None),
828 seen
='unseen' if not self
.seen
else 'seen')
831 class CommentNotification(Notification
):
832 __tablename__
= 'core__comment_notifications'
833 id = Column(Integer
, ForeignKey(Notification
.id), primary_key
=True)
835 subject_id
= Column(Integer
, ForeignKey(MediaComment
.id))
836 subject
= relationship(
838 backref
=backref('comment_notifications', cascade
='all, delete-orphan'))
841 'polymorphic_identity': 'comment_notification'
845 class ProcessingNotification(Notification
):
846 __tablename__
= 'core__processing_notifications'
848 id = Column(Integer
, ForeignKey(Notification
.id), primary_key
=True)
850 subject_id
= Column(Integer
, ForeignKey(MediaEntry
.id))
851 subject
= relationship(
853 backref
=backref('processing_notifications',
854 cascade
='all, delete-orphan'))
857 'polymorphic_identity': 'processing_notification'
862 [ProcessingNotification
, CommentNotification
])
864 class ReportBase(Base
):
866 This is the basic report object which the other reports are based off of.
868 :keyword reporter_id Holds the id of the user who created
869 the report, as an Integer column.
870 :keyword report_content Hold the explanation left by the repor-
871 -ter to indicate why they filed the
872 report in the first place, as a
874 :keyword reported_user_id Holds the id of the user who created
875 the content which was reported, as
877 :keyword created Holds a datetime column of when the re-
879 :keyword discriminator This column distinguishes between the
880 different types of reports.
881 :keyword resolver_id Holds the id of the moderator/admin who
883 :keyword resolved Holds the DateTime object which descri-
884 -bes when this report was resolved
885 :keyword result Holds the UnicodeText column of the
886 resolver's reasons for resolving
887 the report this way. Some of this
890 __tablename__
= 'core__reports'
891 id = Column(Integer
, primary_key
=True)
892 reporter_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
893 reporter
= relationship(
895 backref
=backref("reports_filed_by",
897 cascade
="all, delete-orphan"),
898 primaryjoin
="User.id==ReportBase.reporter_id")
899 report_content
= Column(UnicodeText
)
900 reported_user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
901 reported_user
= relationship(
903 backref
=backref("reports_filed_on",
905 cascade
="all, delete-orphan"),
906 primaryjoin
="User.id==ReportBase.reported_user_id")
907 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now())
908 discriminator
= Column('type', Unicode(50))
909 resolver_id
= Column(Integer
, ForeignKey(User
.id))
910 resolver
= relationship(
912 backref
=backref("reports_resolved_by",
914 cascade
="all, delete-orphan"),
915 primaryjoin
="User.id==ReportBase.resolver_id")
917 resolved
= Column(DateTime
)
918 result
= Column(UnicodeText
)
919 __mapper_args__
= {'polymorphic_on': discriminator
}
921 def is_comment_report(self
):
922 return self
.discriminator
=='comment_report'
924 def is_media_entry_report(self
):
925 return self
.discriminator
=='media_report'
927 def is_archived_report(self
):
928 return self
.resolved
is not None
930 def archive(self
,resolver_id
, resolved
, result
):
931 self
.resolver_id
= resolver_id
932 self
.resolved
= resolved
936 class CommentReport(ReportBase
):
938 Reports that have been filed on comments.
939 :keyword comment_id Holds the integer value of the reported
942 __tablename__
= 'core__reports_on_comments'
943 __mapper_args__
= {'polymorphic_identity': 'comment_report'}
945 id = Column('id',Integer
, ForeignKey('core__reports.id'),
947 comment_id
= Column(Integer
, ForeignKey(MediaComment
.id), nullable
=True)
948 comment
= relationship(
949 MediaComment
, backref
=backref("reports_filed_on",
953 class MediaReport(ReportBase
):
955 Reports that have been filed on media entries
956 :keyword media_entry_id Holds the integer value of the reported
959 __tablename__
= 'core__reports_on_media'
960 __mapper_args__
= {'polymorphic_identity': 'media_report'}
962 id = Column('id',Integer
, ForeignKey('core__reports.id'),
964 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=True)
965 media_entry
= relationship(
967 backref
=backref("reports_filed_on",
972 Holds the information on a specific user's ban-state. As long as one of
973 these is attached to a user, they are banned from accessing mediagoblin.
974 When they try to log in, they are greeted with a page that tells them
975 the reason why they are banned and when (if ever) the ban will be
978 :keyword user_id Holds the id of the user this object is
979 attached to. This is a one-to-one
981 :keyword expiration_date Holds the date that the ban will be lifted.
982 If this is null, the ban is permanent
983 unless a moderator manually lifts it.
984 :keyword reason Holds the reason why the user was banned.
986 __tablename__
= 'core__user_bans'
988 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False,
990 expiration_date
= Column(Date
)
991 reason
= Column(UnicodeText
, nullable
=False)
994 class Privilege(Base
):
996 The Privilege table holds all of the different privileges a user can hold.
997 If a user 'has' a privilege, the User object is in a relationship with the
1000 :keyword privilege_name Holds a unicode object that is the recognizable
1001 name of this privilege. This is the column
1002 used for identifying whether or not a user
1003 has a necessary privilege or not.
1006 __tablename__
= 'core__privileges'
1008 id = Column(Integer
, nullable
=False, primary_key
=True)
1009 privilege_name
= Column(Unicode
, nullable
=False, unique
=True)
1010 all_users
= relationship(
1012 backref
='all_privileges',
1013 secondary
="core__privileges_users")
1015 def __init__(self
, privilege_name
):
1017 Currently consructors are required for tables that are initialized thru
1018 the FOUNDATIONS system. This is because they need to be able to be con-
1019 -structed by a list object holding their arg*s
1021 self
.privilege_name
= privilege_name
1024 return "<Privilege %s>" % (self
.privilege_name
)
1027 class PrivilegeUserAssociation(Base
):
1029 This table holds the many-to-many relationship between User and Privilege
1032 __tablename__
= 'core__privileges_users'
1037 ForeignKey(User
.id),
1042 ForeignKey(Privilege
.id),
1046 User
, MediaEntry
, Tag
, MediaTag
, MediaComment
, Collection
, CollectionItem
,
1047 MediaFile
, FileKeynames
, MediaAttachmentFile
, ProcessingMetaData
,
1048 Notification
, CommentNotification
, ProcessingNotification
, Client
,
1049 CommentSubscription
, ReportBase
, CommentReport
, MediaReport
, UserBan
,
1050 Privilege
, PrivilegeUserAssociation
,
1051 RequestToken
, AccessToken
, NonceTimestamp
]
1054 Foundations are the default rows that are created immediately after the tables
1055 are initialized. Each entry to this dictionary should be in the format of:
1056 ModelConstructorObject:List of Dictionaries
1057 (Each Dictionary represents a row on the Table to be created, containing each
1058 of the columns' names as a key string, and each of the columns' values as a
1061 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1062 user_foundations = [{'name':u'Joanna', 'age':24},
1063 {'name':u'Andrea', 'age':41}]
1065 FOUNDATIONS = {User:user_foundations}
1067 privilege_foundations
= [{'privilege_name':u
'admin'},
1068 {'privilege_name':u
'moderator'},
1069 {'privilege_name':u
'uploader'},
1070 {'privilege_name':u
'reporter'},
1071 {'privilege_name':u
'commenter'},
1072 {'privilege_name':u
'active'}]
1073 FOUNDATIONS
= {Privilege
:privilege_foundations
}
1075 ######################################################
1076 # Special, migrations-tracking table
1078 # Not listed in MODELS because this is special and not
1079 # really migrated, but used for migrations (for now)
1080 ######################################################
1082 class MigrationData(Base
):
1083 __tablename__
= "core__migrations"
1085 name
= Column(Unicode
, primary_key
=True)
1086 version
= Column(Integer
, nullable
=False, default
=0)
1088 ######################################################
1091 def show_table_init(engine_uri
):
1092 if engine_uri
is None:
1093 engine_uri
= 'sqlite:///:memory:'
1094 from sqlalchemy
import create_engine
1095 engine
= create_engine(engine_uri
, echo
=True)
1097 Base
.metadata
.create_all(engine
)
1100 if __name__
== '__main__':
1101 from sys
import argv
1107 show_table_init(uri
)