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.
24 from sqlalchemy
import Column
, Integer
, Unicode
, UnicodeText
, DateTime
, \
25 Boolean
, ForeignKey
, UniqueConstraint
, PrimaryKeyConstraint
, \
27 from sqlalchemy
.orm
import relationship
, backref
, with_polymorphic
28 from sqlalchemy
.orm
.collections
import attribute_mapped_collection
29 from sqlalchemy
.sql
.expression
import desc
30 from sqlalchemy
.ext
.associationproxy
import association_proxy
31 from sqlalchemy
.util
import memoized_property
33 from mediagoblin
.db
.extratypes
import (PathTupleWithSlashes
, JSONEncoded
,
35 from mediagoblin
.db
.base
import Base
, DictReadAttrProxy
36 from mediagoblin
.db
.mixin
import UserMixin
, MediaEntryMixin
, \
37 MediaCommentMixin
, CollectionMixin
, CollectionItemMixin
38 from mediagoblin
.tools
.files
import delete_media_files
39 from mediagoblin
.tools
.common
import import_component
41 # It's actually kind of annoying how sqlalchemy-migrate does this, if
42 # I understand it right, but whatever. Anyway, don't remove this :P
44 # We could do migration calls more manually instead of relying on
45 # this import-based meddling...
46 from migrate
import changeset
48 _log
= logging
.getLogger(__name__
)
52 class User(Base
, UserMixin
):
54 TODO: We should consider moving some rarely used fields
55 into some sort of "shadow" table.
57 __tablename__
= "core__users"
59 id = Column(Integer
, primary_key
=True)
60 username
= Column(Unicode
, nullable
=False, unique
=True)
61 # Note: no db uniqueness constraint on email because it's not
62 # reliable (many email systems case insensitive despite against
63 # the RFC) and because it would be a mess to implement at this
65 email
= Column(Unicode
, nullable
=False)
66 pw_hash
= Column(Unicode
)
67 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
68 # Intented to be nullable=False, but migrations would not work for it
69 # set to nullable=True implicitly.
70 wants_comment_notification
= Column(Boolean
, default
=True)
71 wants_notifications
= Column(Boolean
, default
=True)
72 license_preference
= Column(Unicode
)
74 bio
= Column(UnicodeText
) # ??
75 uploaded
= Column(Integer
, default
=0)
76 upload_limit
= Column(Integer
)
79 # plugin data would be in a separate model
82 return '<{0} #{1} {2} {3} "{4}">'.format(
83 self
.__class
__.__name
__,
85 'verified' if self
.has_privilege(u
'active') else 'non-verified',
86 'admin' if self
.has_privilege(u
'admin') else 'user',
89 def delete(self
, **kwargs
):
90 """Deletes a User and all related entries/comments/files/..."""
91 # Collections get deleted by relationships.
93 media_entries
= MediaEntry
.query
.filter(MediaEntry
.uploader
== self
.id)
94 for media
in media_entries
:
95 # TODO: Make sure that "MediaEntry.delete()" also deletes
96 # all related files/Comments
97 media
.delete(del_orphan_tags
=False, commit
=False)
99 # Delete now unused tags
100 # TODO: import here due to cyclic imports!!! This cries for refactoring
101 from mediagoblin
.db
.util
import clean_orphan_tags
102 clean_orphan_tags(commit
=False)
104 # Delete user, pass through commit=False/True in kwargs
105 super(User
, self
).delete(**kwargs
)
106 _log
.info('Deleted user "{0}" account'.format(self
.username
))
108 def has_privilege(self
, privilege
, allow_admin
=True):
110 This method checks to make sure a user has all the correct privileges
111 to access a piece of content.
113 :param privilege A unicode object which represent the different
114 privileges which may give the user access to
117 :param allow_admin If this is set to True the then if the user is
118 an admin, then this will always return True
119 even if the user hasn't been given the
120 privilege. (defaults to True)
122 priv
= Privilege
.query
.filter_by(privilege_name
=privilege
).one()
123 if priv
in self
.all_privileges
:
125 elif allow_admin
and self
.has_privilege(u
'admin', allow_admin
=False):
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
443 "author": author
.serialize(request
),
444 "objectType": self
.objectType
,
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 objectType
=self
.objectType
,
471 context
["displayName"] = self
.title
474 context
["content"] = self
.description
477 context
["license"] = self
.license
480 comments
= [comment
.serialize(request
) for comment
in self
.get_comments()]
481 total
= len(comments
)
482 context
["replies"] = {
485 "url": request
.urlgen(
486 "mediagoblin.federation.object.comments",
487 objectType
=self
.objectType
,
495 def unserialize(self
, data
):
496 """ Takes API objects and unserializes on existing MediaEntry """
497 if "displayName" in data
:
498 self
.title
= data
["displayName"]
500 if "content" in data
:
501 self
.description
= data
["content"]
503 if "license" in data
:
504 self
.license
= data
["license"]
508 class FileKeynames(Base
):
510 keywords for various places.
511 currently the MediaFile keys
513 __tablename__
= "core__file_keynames"
514 id = Column(Integer
, primary_key
=True)
515 name
= Column(Unicode
, unique
=True)
518 return "<FileKeyname %r: %r>" % (self
.id, self
.name
)
521 def find_or_new(cls
, name
):
522 t
= cls
.query
.filter_by(name
=name
).first()
525 return cls(name
=name
)
528 class MediaFile(Base
):
530 TODO: Highly consider moving "name" into a new table.
531 TODO: Consider preloading said table in software
533 __tablename__
= "core__mediafiles"
535 media_entry
= Column(
536 Integer
, ForeignKey(MediaEntry
.id),
538 name_id
= Column(SmallInteger
, ForeignKey(FileKeynames
.id), nullable
=False)
539 file_path
= Column(PathTupleWithSlashes
)
540 file_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
))
543 PrimaryKeyConstraint('media_entry', 'name_id'),
547 return "<MediaFile %s: %r>" % (self
.name
, self
.file_path
)
549 name_helper
= relationship(FileKeynames
, lazy
="joined", innerjoin
=True)
550 name
= association_proxy('name_helper', 'name',
551 creator
=FileKeynames
.find_or_new
555 class MediaAttachmentFile(Base
):
556 __tablename__
= "core__attachment_files"
558 id = Column(Integer
, primary_key
=True)
559 media_entry
= Column(
560 Integer
, ForeignKey(MediaEntry
.id),
562 name
= Column(Unicode
, nullable
=False)
563 filepath
= Column(PathTupleWithSlashes
)
564 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
568 """A dict like view on this object"""
569 return DictReadAttrProxy(self
)
573 __tablename__
= "core__tags"
575 id = Column(Integer
, primary_key
=True)
576 slug
= Column(Unicode
, nullable
=False, unique
=True)
579 return "<Tag %r: %r>" % (self
.id, self
.slug
)
582 def find_or_new(cls
, slug
):
583 t
= cls
.query
.filter_by(slug
=slug
).first()
586 return cls(slug
=slug
)
589 class MediaTag(Base
):
590 __tablename__
= "core__media_tags"
592 id = Column(Integer
, primary_key
=True)
593 media_entry
= Column(
594 Integer
, ForeignKey(MediaEntry
.id),
595 nullable
=False, index
=True)
596 tag
= Column(Integer
, ForeignKey(Tag
.id), nullable
=False, index
=True)
597 name
= Column(Unicode
)
598 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
601 UniqueConstraint('tag', 'media_entry'),
604 tag_helper
= relationship(Tag
)
605 slug
= association_proxy('tag_helper', 'slug',
606 creator
=Tag
.find_or_new
609 def __init__(self
, name
=None, slug
=None):
614 self
.tag_helper
= Tag
.find_or_new(slug
)
618 """A dict like view on this object"""
619 return DictReadAttrProxy(self
)
622 class MediaComment(Base
, MediaCommentMixin
):
623 __tablename__
= "core__media_comments"
625 id = Column(Integer
, primary_key
=True)
626 media_entry
= Column(
627 Integer
, ForeignKey(MediaEntry
.id), nullable
=False, index
=True)
628 author
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
629 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
630 content
= Column(UnicodeText
, nullable
=False)
632 # Cascade: Comments are owned by their creator. So do the full thing.
633 # lazy=dynamic: People might post a *lot* of comments,
634 # so make the "posted_comments" a query-like thing.
635 get_author
= relationship(User
,
636 backref
=backref("posted_comments",
638 cascade
="all, delete-orphan"))
639 get_entry
= relationship(MediaEntry
,
640 backref
=backref("comments",
642 cascade
="all, delete-orphan"))
644 # Cascade: Comments are somewhat owned by their MediaEntry.
645 # So do the full thing.
646 # lazy=dynamic: MediaEntries might have many comments,
647 # so make the "all_comments" a query-like thing.
648 get_media_entry
= relationship(MediaEntry
,
649 backref
=backref("all_comments",
651 cascade
="all, delete-orphan"))
653 def serialize(self
, request
):
654 """ Unserialize to python dictionary for API """
655 media
= MediaEntry
.query
.filter_by(id=self
.media_entry
).first()
656 author
= self
.get_author
659 "objectType": "comment",
660 "content": self
.content
,
661 "inReplyTo": media
.serialize(request
, show_comments
=False),
662 "author": author
.serialize(request
)
667 def unserialize(self
, data
):
668 """ Takes API objects and unserializes on existing comment """
669 # Do initial checks to verify the object is correct
670 required_attributes
= ["content", "inReplyTo"]
671 for attr
in required_attributes
:
675 # Validate inReplyTo has ID
676 if "id" not in data
["inReplyTo"]:
679 # Validate that the ID is correct
681 media_id
= int(data
["inReplyTo"]["id"])
685 media
= MediaEntry
.query
.filter_by(id=media_id
).first()
689 self
.media_entry
= media
.id
690 self
.content
= data
["content"]
695 class Collection(Base
, CollectionMixin
):
696 """An 'album' or 'set' of media by a user.
698 On deletion, contained CollectionItems get automatically reaped via
700 __tablename__
= "core__collections"
702 id = Column(Integer
, primary_key
=True)
703 title
= Column(Unicode
, nullable
=False)
704 slug
= Column(Unicode
)
705 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
,
707 description
= Column(UnicodeText
)
708 creator
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
709 # TODO: No of items in Collection. Badly named, can we migrate to num_items?
710 items
= Column(Integer
, default
=0)
712 # Cascade: Collections are owned by their creator. So do the full thing.
713 get_creator
= relationship(User
,
714 backref
=backref("collections",
715 cascade
="all, delete-orphan"))
718 UniqueConstraint('creator', 'slug'),
721 def get_collection_items(self
, ascending
=False):
722 #TODO, is this still needed with self.collection_items being available?
723 order_col
= CollectionItem
.position
725 order_col
= desc(order_col
)
726 return CollectionItem
.query
.filter_by(
727 collection
=self
.id).order_by(order_col
)
730 safe_title
= self
.title
.encode('ascii', 'replace')
731 return '<{classname} #{id}: {title} by {creator}>'.format(
733 classname
=self
.__class
__.__name
__,
734 creator
=self
.creator
,
738 class CollectionItem(Base
, CollectionItemMixin
):
739 __tablename__
= "core__collection_items"
741 id = Column(Integer
, primary_key
=True)
742 media_entry
= Column(
743 Integer
, ForeignKey(MediaEntry
.id), nullable
=False, index
=True)
744 collection
= Column(Integer
, ForeignKey(Collection
.id), nullable
=False)
745 note
= Column(UnicodeText
, nullable
=True)
746 added
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
747 position
= Column(Integer
)
749 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
750 in_collection
= relationship(Collection
,
753 cascade
="all, delete-orphan"))
755 get_media_entry
= relationship(MediaEntry
)
758 UniqueConstraint('collection', 'media_entry'),
763 """A dict like view on this object"""
764 return DictReadAttrProxy(self
)
767 return '<{classname} #{id}: Entry {entry} in {collection}>'.format(
769 classname
=self
.__class
__.__name
__,
770 collection
=self
.collection
,
771 entry
=self
.media_entry
)
774 class ProcessingMetaData(Base
):
775 __tablename__
= 'core__processing_metadata'
777 id = Column(Integer
, primary_key
=True)
778 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False,
780 media_entry
= relationship(MediaEntry
,
781 backref
=backref('processing_metadata',
782 cascade
='all, delete-orphan'))
783 callback_url
= Column(Unicode
)
787 """A dict like view on this object"""
788 return DictReadAttrProxy(self
)
791 class CommentSubscription(Base
):
792 __tablename__
= 'core__comment_subscriptions'
793 id = Column(Integer
, primary_key
=True)
795 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
797 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False)
798 media_entry
= relationship(MediaEntry
,
799 backref
=backref('comment_subscriptions',
800 cascade
='all, delete-orphan'))
802 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
803 user
= relationship(User
,
804 backref
=backref('comment_subscriptions',
805 cascade
='all, delete-orphan'))
807 notify
= Column(Boolean
, nullable
=False, default
=True)
808 send_email
= Column(Boolean
, nullable
=False, default
=True)
811 return ('<{classname} #{id}: {user} {media} notify: '
812 '{notify} email: {email}>').format(
814 classname
=self
.__class
__.__name
__,
816 media
=self
.media_entry
,
818 email
=self
.send_email
)
821 class Notification(Base
):
822 __tablename__
= 'core__notifications'
823 id = Column(Integer
, primary_key
=True)
824 type = Column(Unicode
)
826 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
828 user_id
= Column(Integer
, ForeignKey('core__users.id'), nullable
=False,
830 seen
= Column(Boolean
, default
=lambda: False, index
=True)
833 backref
=backref('notifications', cascade
='all, delete-orphan'))
836 'polymorphic_identity': 'notification',
837 'polymorphic_on': type
841 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
843 klass
=self
.__class
__.__name
__,
845 subject
=getattr(self
, 'subject', None),
846 seen
='unseen' if not self
.seen
else 'seen')
848 def __unicode__(self
):
849 return u
'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
851 klass
=self
.__class
__.__name
__,
853 subject
=getattr(self
, 'subject', None),
854 seen
='unseen' if not self
.seen
else 'seen')
857 class CommentNotification(Notification
):
858 __tablename__
= 'core__comment_notifications'
859 id = Column(Integer
, ForeignKey(Notification
.id), primary_key
=True)
861 subject_id
= Column(Integer
, ForeignKey(MediaComment
.id))
862 subject
= relationship(
864 backref
=backref('comment_notifications', cascade
='all, delete-orphan'))
867 'polymorphic_identity': 'comment_notification'
871 class ProcessingNotification(Notification
):
872 __tablename__
= 'core__processing_notifications'
874 id = Column(Integer
, ForeignKey(Notification
.id), primary_key
=True)
876 subject_id
= Column(Integer
, ForeignKey(MediaEntry
.id))
877 subject
= relationship(
879 backref
=backref('processing_notifications',
880 cascade
='all, delete-orphan'))
883 'polymorphic_identity': 'processing_notification'
888 [ProcessingNotification
, CommentNotification
])
890 class ReportBase(Base
):
892 This is the basic report object which the other reports are based off of.
894 :keyword reporter_id Holds the id of the user who created
895 the report, as an Integer column.
896 :keyword report_content Hold the explanation left by the repor-
897 -ter to indicate why they filed the
898 report in the first place, as a
900 :keyword reported_user_id Holds the id of the user who created
901 the content which was reported, as
903 :keyword created Holds a datetime column of when the re-
905 :keyword discriminator This column distinguishes between the
906 different types of reports.
907 :keyword resolver_id Holds the id of the moderator/admin who
909 :keyword resolved Holds the DateTime object which descri-
910 -bes when this report was resolved
911 :keyword result Holds the UnicodeText column of the
912 resolver's reasons for resolving
913 the report this way. Some of this
916 __tablename__
= 'core__reports'
917 id = Column(Integer
, primary_key
=True)
918 reporter_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
919 reporter
= relationship(
921 backref
=backref("reports_filed_by",
923 cascade
="all, delete-orphan"),
924 primaryjoin
="User.id==ReportBase.reporter_id")
925 report_content
= Column(UnicodeText
)
926 reported_user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
927 reported_user
= relationship(
929 backref
=backref("reports_filed_on",
931 cascade
="all, delete-orphan"),
932 primaryjoin
="User.id==ReportBase.reported_user_id")
933 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now())
934 discriminator
= Column('type', Unicode(50))
935 resolver_id
= Column(Integer
, ForeignKey(User
.id))
936 resolver
= relationship(
938 backref
=backref("reports_resolved_by",
940 cascade
="all, delete-orphan"),
941 primaryjoin
="User.id==ReportBase.resolver_id")
943 resolved
= Column(DateTime
)
944 result
= Column(UnicodeText
)
945 __mapper_args__
= {'polymorphic_on': discriminator
}
947 def is_comment_report(self
):
948 return self
.discriminator
=='comment_report'
950 def is_media_entry_report(self
):
951 return self
.discriminator
=='media_report'
953 def is_archived_report(self
):
954 return self
.resolved
is not None
956 def archive(self
,resolver_id
, resolved
, result
):
957 self
.resolver_id
= resolver_id
958 self
.resolved
= resolved
962 class CommentReport(ReportBase
):
964 Reports that have been filed on comments.
965 :keyword comment_id Holds the integer value of the reported
968 __tablename__
= 'core__reports_on_comments'
969 __mapper_args__
= {'polymorphic_identity': 'comment_report'}
971 id = Column('id',Integer
, ForeignKey('core__reports.id'),
973 comment_id
= Column(Integer
, ForeignKey(MediaComment
.id), nullable
=True)
974 comment
= relationship(
975 MediaComment
, backref
=backref("reports_filed_on",
979 class MediaReport(ReportBase
):
981 Reports that have been filed on media entries
982 :keyword media_entry_id Holds the integer value of the reported
985 __tablename__
= 'core__reports_on_media'
986 __mapper_args__
= {'polymorphic_identity': 'media_report'}
988 id = Column('id',Integer
, ForeignKey('core__reports.id'),
990 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=True)
991 media_entry
= relationship(
993 backref
=backref("reports_filed_on",
998 Holds the information on a specific user's ban-state. As long as one of
999 these is attached to a user, they are banned from accessing mediagoblin.
1000 When they try to log in, they are greeted with a page that tells them
1001 the reason why they are banned and when (if ever) the ban will be
1004 :keyword user_id Holds the id of the user this object is
1005 attached to. This is a one-to-one
1007 :keyword expiration_date Holds the date that the ban will be lifted.
1008 If this is null, the ban is permanent
1009 unless a moderator manually lifts it.
1010 :keyword reason Holds the reason why the user was banned.
1012 __tablename__
= 'core__user_bans'
1014 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False,
1016 expiration_date
= Column(Date
)
1017 reason
= Column(UnicodeText
, nullable
=False)
1020 class Privilege(Base
):
1022 The Privilege table holds all of the different privileges a user can hold.
1023 If a user 'has' a privilege, the User object is in a relationship with the
1026 :keyword privilege_name Holds a unicode object that is the recognizable
1027 name of this privilege. This is the column
1028 used for identifying whether or not a user
1029 has a necessary privilege or not.
1032 __tablename__
= 'core__privileges'
1034 id = Column(Integer
, nullable
=False, primary_key
=True)
1035 privilege_name
= Column(Unicode
, nullable
=False, unique
=True)
1036 all_users
= relationship(
1038 backref
='all_privileges',
1039 secondary
="core__privileges_users")
1041 def __init__(self
, privilege_name
):
1043 Currently consructors are required for tables that are initialized thru
1044 the FOUNDATIONS system. This is because they need to be able to be con-
1045 -structed by a list object holding their arg*s
1047 self
.privilege_name
= privilege_name
1050 return "<Privilege %s>" % (self
.privilege_name
)
1053 class PrivilegeUserAssociation(Base
):
1055 This table holds the many-to-many relationship between User and Privilege
1058 __tablename__
= 'core__privileges_users'
1063 ForeignKey(User
.id),
1068 ForeignKey(Privilege
.id),
1072 User
, MediaEntry
, Tag
, MediaTag
, MediaComment
, Collection
, CollectionItem
,
1073 MediaFile
, FileKeynames
, MediaAttachmentFile
, ProcessingMetaData
,
1074 Notification
, CommentNotification
, ProcessingNotification
, Client
,
1075 CommentSubscription
, ReportBase
, CommentReport
, MediaReport
, UserBan
,
1076 Privilege
, PrivilegeUserAssociation
,
1077 RequestToken
, AccessToken
, NonceTimestamp
]
1080 Foundations are the default rows that are created immediately after the tables
1081 are initialized. Each entry to this dictionary should be in the format of:
1082 ModelConstructorObject:List of Dictionaries
1083 (Each Dictionary represents a row on the Table to be created, containing each
1084 of the columns' names as a key string, and each of the columns' values as a
1087 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1088 user_foundations = [{'name':u'Joanna', 'age':24},
1089 {'name':u'Andrea', 'age':41}]
1091 FOUNDATIONS = {User:user_foundations}
1093 privilege_foundations
= [{'privilege_name':u
'admin'},
1094 {'privilege_name':u
'moderator'},
1095 {'privilege_name':u
'uploader'},
1096 {'privilege_name':u
'reporter'},
1097 {'privilege_name':u
'commenter'},
1098 {'privilege_name':u
'active'}]
1099 FOUNDATIONS
= {Privilege
:privilege_foundations
}
1101 ######################################################
1102 # Special, migrations-tracking table
1104 # Not listed in MODELS because this is special and not
1105 # really migrated, but used for migrations (for now)
1106 ######################################################
1108 class MigrationData(Base
):
1109 __tablename__
= "core__migrations"
1111 name
= Column(Unicode
, primary_key
=True)
1112 version
= Column(Integer
, nullable
=False, default
=0)
1114 ######################################################
1117 def show_table_init(engine_uri
):
1118 if engine_uri
is None:
1119 engine_uri
= 'sqlite:///:memory:'
1120 from sqlalchemy
import create_engine
1121 engine
= create_engine(engine_uri
, echo
=True)
1123 Base
.metadata
.create_all(engine
)
1126 if __name__
== '__main__':
1127 from sys
import argv
1133 show_table_init(uri
)