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)
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
, privilege
, allow_admin
=True):
111 This method checks to make sure a user has all the correct privileges
112 to access a piece of content.
114 :param privilege A unicode object which represent the different
115 privileges which may give the user access to
118 :param allow_admin If this is set to True the then if the user is
119 an admin, then this will always return True
120 even if the user hasn't been given the
121 privilege. (defaults to True)
123 priv
= Privilege
.query
.filter_by(privilege_name
=privilege
).one()
124 if priv
in self
.all_privileges
:
126 elif allow_admin
and self
.has_privilege(u
'admin', allow_admin
=False):
133 Checks if this user is banned.
135 :returns True if self is banned
136 :returns False if self is not
138 return UserBan
.query
.get(self
.id) is not None
141 def serialize(self
, request
):
143 "id": "acct:{0}@{1}".format(self
.username
, request
.host
),
144 "preferredUsername": self
.username
,
145 "displayName": "{0}@{1}".format(self
.username
, request
.host
),
146 "objectType": "person",
153 "href": request
.urlgen(
154 "mediagoblin.federation.user.profile",
155 username
=self
.username
,
160 "href": request
.urlgen(
161 "mediagoblin.federation.inbox",
162 username
=self
.username
,
167 "href": request
.urlgen(
168 "mediagoblin.federation.feed",
169 username
=self
.username
,
177 user
.update({"summary": self
.bio
})
179 user
.update({"url": self
.url
})
185 Model representing a client - Used for API Auth
187 __tablename__
= "core__clients"
189 id = Column(Unicode
, nullable
=True, primary_key
=True)
190 secret
= Column(Unicode
, nullable
=False)
191 expirey
= Column(DateTime
, nullable
=True)
192 application_type
= Column(Unicode
, nullable
=False)
193 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
194 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
197 redirect_uri
= Column(JSONEncoded
, nullable
=True)
198 logo_url
= Column(Unicode
, nullable
=True)
199 application_name
= Column(Unicode
, nullable
=True)
200 contacts
= Column(JSONEncoded
, nullable
=True)
203 if self
.application_name
:
204 return "<Client {0} - {1}>".format(self
.application_name
, self
.id)
206 return "<Client {0}>".format(self
.id)
208 class RequestToken(Base
):
210 Model for representing the request tokens
212 __tablename__
= "core__request_tokens"
214 token
= Column(Unicode
, primary_key
=True)
215 secret
= Column(Unicode
, nullable
=False)
216 client
= Column(Unicode
, ForeignKey(Client
.id))
217 user
= Column(Integer
, ForeignKey(User
.id), nullable
=True)
218 used
= Column(Boolean
, default
=False)
219 authenticated
= Column(Boolean
, default
=False)
220 verifier
= Column(Unicode
, nullable
=True)
221 callback
= Column(Unicode
, nullable
=False, default
=u
"oob")
222 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
223 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
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
)
239 class NonceTimestamp(Base
):
241 A place the timestamp and nonce can be stored - this is for OAuth1
243 __tablename__
= "core__nonce_timestamps"
245 nonce
= Column(Unicode
, nullable
=False, primary_key
=True)
246 timestamp
= Column(DateTime
, nullable
=False, primary_key
=True)
248 class MediaEntry(Base
, MediaEntryMixin
):
250 TODO: Consider fetching the media_files using join
252 __tablename__
= "core__media_entries"
254 id = Column(Integer
, primary_key
=True)
255 uploader
= Column(Integer
, ForeignKey(User
.id), nullable
=False, index
=True)
256 title
= Column(Unicode
, nullable
=False)
257 slug
= Column(Unicode
)
258 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
,
260 description
= Column(UnicodeText
) # ??
261 media_type
= Column(Unicode
, nullable
=False)
262 state
= Column(Unicode
, default
=u
'unprocessed', nullable
=False)
263 # or use sqlalchemy.types.Enum?
264 license
= Column(Unicode
)
265 file_size
= Column(Integer
, default
=0)
267 fail_error
= Column(Unicode
)
268 fail_metadata
= Column(JSONEncoded
)
270 transcoding_progress
= Column(SmallInteger
)
272 queued_media_file
= Column(PathTupleWithSlashes
)
274 queued_task_id
= Column(Unicode
)
277 UniqueConstraint('uploader', 'slug'),
280 get_uploader
= relationship(User
)
282 media_files_helper
= relationship("MediaFile",
283 collection_class
=attribute_mapped_collection("name"),
284 cascade
="all, delete-orphan"
286 media_files
= association_proxy('media_files_helper', 'file_path',
287 creator
=lambda k
, v
: MediaFile(name
=k
, file_path
=v
)
290 attachment_files_helper
= relationship("MediaAttachmentFile",
291 cascade
="all, delete-orphan",
292 order_by
="MediaAttachmentFile.created"
294 attachment_files
= association_proxy("attachment_files_helper", "dict_view",
295 creator
=lambda v
: MediaAttachmentFile(
296 name
=v
["name"], filepath
=v
["filepath"])
299 tags_helper
= relationship("MediaTag",
300 cascade
="all, delete-orphan" # should be automatically deleted
302 tags
= association_proxy("tags_helper", "dict_view",
303 creator
=lambda v
: MediaTag(name
=v
["name"], slug
=v
["slug"])
306 collections_helper
= relationship("CollectionItem",
307 cascade
="all, delete-orphan"
309 collections
= association_proxy("collections_helper", "in_collection")
310 media_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
),
311 default
=MutationDict())
316 def get_comments(self
, ascending
=False):
317 order_col
= MediaComment
.created
319 order_col
= desc(order_col
)
320 return self
.all_comments
.order_by(order_col
)
322 def url_to_prev(self
, urlgen
):
323 """get the next 'newer' entry by this user"""
324 media
= MediaEntry
.query
.filter(
325 (MediaEntry
.uploader
== self
.uploader
)
326 & (MediaEntry
.state
== u
'processed')
327 & (MediaEntry
.id > self
.id)).order_by(MediaEntry
.id).first()
329 if media
is not None:
330 return media
.url_for_self(urlgen
)
332 def url_to_next(self
, urlgen
):
333 """get the next 'older' entry by this user"""
334 media
= MediaEntry
.query
.filter(
335 (MediaEntry
.uploader
== self
.uploader
)
336 & (MediaEntry
.state
== u
'processed')
337 & (MediaEntry
.id < self
.id)).order_by(desc(MediaEntry
.id)).first()
339 if media
is not None:
340 return media
.url_for_self(urlgen
)
342 def get_file_metadata(self
, file_key
, metadata_key
=None):
344 Return the file_metadata dict of a MediaFile. If metadata_key is given,
345 return the value of the key.
347 media_file
= MediaFile
.query
.filter_by(media_entry
=self
.id,
348 name
=unicode(file_key
)).first()
352 return media_file
.file_metadata
.get(metadata_key
, None)
354 return media_file
.file_metadata
356 def set_file_metadata(self
, file_key
, **kwargs
):
358 Update the file_metadata of a MediaFile.
360 media_file
= MediaFile
.query
.filter_by(media_entry
=self
.id,
361 name
=unicode(file_key
)).first()
363 file_metadata
= media_file
.file_metadata
or {}
365 for key
, value
in kwargs
.iteritems():
366 file_metadata
[key
] = value
368 media_file
.file_metadata
= file_metadata
372 def media_data(self
):
373 return getattr(self
, self
.media_data_ref
)
375 def media_data_init(self
, **kwargs
):
377 Initialize or update the contents of a media entry's media_data row
379 media_data
= self
.media_data
381 if media_data
is None:
382 # Get the correct table:
383 table
= import_component(self
.media_type
+ '.models:DATA_MODEL')
384 # No media data, so actually add a new one
385 media_data
= table(**kwargs
)
386 # Get the relationship set up.
387 media_data
.get_media_entry
= self
389 # Update old media data
390 for field
, value
in kwargs
.iteritems():
391 setattr(media_data
, field
, value
)
394 def media_data_ref(self
):
395 return import_component(self
.media_type
+ '.models:BACKREF_NAME')
398 safe_title
= self
.title
.encode('ascii', 'replace')
400 return '<{classname} {id}: {title}>'.format(
401 classname
=self
.__class
__.__name
__,
405 def delete(self
, del_orphan_tags
=True, **kwargs
):
406 """Delete MediaEntry and all related files/attachments/comments
408 This will *not* automatically delete unused collections, which
411 :param del_orphan_tags: True/false if we delete unused Tags too
412 :param commit: True/False if this should end the db transaction"""
413 # User's CollectionItems are automatically deleted via "cascade".
414 # Comments on this Media are deleted by cascade, hopefully.
416 # Delete all related files/attachments
418 delete_media_files(self
)
419 except OSError, error
:
420 # Returns list of files we failed to delete
421 _log
.error('No such files from the user "{1}" to delete: '
422 '{0}'.format(str(error
), self
.get_uploader
))
423 _log
.info('Deleted Media entry id "{0}"'.format(self
.id))
424 # Related MediaTag's are automatically cleaned, but we might
425 # want to clean out unused Tag's too.
427 # TODO: Import here due to cyclic imports!!!
428 # This cries for refactoring
429 from mediagoblin
.db
.util
import clean_orphan_tags
430 clean_orphan_tags(commit
=False)
431 # pass through commit=False/True in kwargs
432 super(MediaEntry
, self
).delete(**kwargs
)
435 def objectType(self
):
436 """ Converts media_type to pump-like type - don't use internally """
437 return self
.media_type
.split(".")[-1]
439 def serialize(self
, request
, show_comments
=True):
440 """ Unserialize MediaEntry to object """
441 author
= self
.get_uploader
442 url
= request
.urlgen(
443 "mediagoblin.user_pages.media_home",
444 user
=author
.username
,
451 "author": author
.serialize(request
),
452 "objectType": self
.objectType
,
455 "url": request
.host_url
+ self
.thumb_url
[1:],
458 "url": request
.host_url
+ self
.original_url
[1:],
460 "published": self
.created
.isoformat(),
461 "updated": self
.created
.isoformat(),
467 "href": request
.urlgen(
468 "mediagoblin.federation.object",
469 objectType
=self
.objectType
,
479 context
["displayName"] = self
.title
482 context
["content"] = self
.description
485 context
["license"] = self
.license
488 comments
= [comment
.serialize(request
) for comment
in self
.get_comments()]
489 total
= len(comments
)
490 context
["replies"] = {
493 "url": request
.urlgen(
494 "mediagoblin.federation.object.comments",
495 objectType
=self
.objectType
,
503 def unserialize(self
, data
):
504 """ Takes API objects and unserializes on existing MediaEntry """
505 if "displayName" in data
:
506 self
.title
= data
["displayName"]
508 if "content" in data
:
509 self
.description
= data
["content"]
511 if "license" in data
:
512 self
.license
= data
["license"]
516 class FileKeynames(Base
):
518 keywords for various places.
519 currently the MediaFile keys
521 __tablename__
= "core__file_keynames"
522 id = Column(Integer
, primary_key
=True)
523 name
= Column(Unicode
, unique
=True)
526 return "<FileKeyname %r: %r>" % (self
.id, self
.name
)
529 def find_or_new(cls
, name
):
530 t
= cls
.query
.filter_by(name
=name
).first()
533 return cls(name
=name
)
536 class MediaFile(Base
):
538 TODO: Highly consider moving "name" into a new table.
539 TODO: Consider preloading said table in software
541 __tablename__
= "core__mediafiles"
543 media_entry
= Column(
544 Integer
, ForeignKey(MediaEntry
.id),
546 name_id
= Column(SmallInteger
, ForeignKey(FileKeynames
.id), nullable
=False)
547 file_path
= Column(PathTupleWithSlashes
)
548 file_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
))
551 PrimaryKeyConstraint('media_entry', 'name_id'),
555 return "<MediaFile %s: %r>" % (self
.name
, self
.file_path
)
557 name_helper
= relationship(FileKeynames
, lazy
="joined", innerjoin
=True)
558 name
= association_proxy('name_helper', 'name',
559 creator
=FileKeynames
.find_or_new
563 class MediaAttachmentFile(Base
):
564 __tablename__
= "core__attachment_files"
566 id = Column(Integer
, primary_key
=True)
567 media_entry
= Column(
568 Integer
, ForeignKey(MediaEntry
.id),
570 name
= Column(Unicode
, nullable
=False)
571 filepath
= Column(PathTupleWithSlashes
)
572 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
576 """A dict like view on this object"""
577 return DictReadAttrProxy(self
)
581 __tablename__
= "core__tags"
583 id = Column(Integer
, primary_key
=True)
584 slug
= Column(Unicode
, nullable
=False, unique
=True)
587 return "<Tag %r: %r>" % (self
.id, self
.slug
)
590 def find_or_new(cls
, slug
):
591 t
= cls
.query
.filter_by(slug
=slug
).first()
594 return cls(slug
=slug
)
597 class MediaTag(Base
):
598 __tablename__
= "core__media_tags"
600 id = Column(Integer
, primary_key
=True)
601 media_entry
= Column(
602 Integer
, ForeignKey(MediaEntry
.id),
603 nullable
=False, index
=True)
604 tag
= Column(Integer
, ForeignKey(Tag
.id), nullable
=False, index
=True)
605 name
= Column(Unicode
)
606 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
609 UniqueConstraint('tag', 'media_entry'),
612 tag_helper
= relationship(Tag
)
613 slug
= association_proxy('tag_helper', 'slug',
614 creator
=Tag
.find_or_new
617 def __init__(self
, name
=None, slug
=None):
622 self
.tag_helper
= Tag
.find_or_new(slug
)
626 """A dict like view on this object"""
627 return DictReadAttrProxy(self
)
630 class MediaComment(Base
, MediaCommentMixin
):
631 __tablename__
= "core__media_comments"
633 id = Column(Integer
, primary_key
=True)
634 media_entry
= Column(
635 Integer
, ForeignKey(MediaEntry
.id), nullable
=False, index
=True)
636 author
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
637 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
638 content
= Column(UnicodeText
, nullable
=False)
640 # Cascade: Comments are owned by their creator. So do the full thing.
641 # lazy=dynamic: People might post a *lot* of comments,
642 # so make the "posted_comments" a query-like thing.
643 get_author
= relationship(User
,
644 backref
=backref("posted_comments",
646 cascade
="all, delete-orphan"))
647 get_entry
= relationship(MediaEntry
,
648 backref
=backref("comments",
650 cascade
="all, delete-orphan"))
652 # Cascade: Comments are somewhat owned by their MediaEntry.
653 # So do the full thing.
654 # lazy=dynamic: MediaEntries might have many comments,
655 # so make the "all_comments" a query-like thing.
656 get_media_entry
= relationship(MediaEntry
,
657 backref
=backref("all_comments",
659 cascade
="all, delete-orphan"))
661 def serialize(self
, request
):
662 """ Unserialize to python dictionary for API """
663 media
= MediaEntry
.query
.filter_by(id=self
.media_entry
).first()
664 author
= self
.get_author
667 "objectType": "comment",
668 "content": self
.content
,
669 "inReplyTo": media
.serialize(request
, show_comments
=False),
670 "author": author
.serialize(request
)
675 def unserialize(self
, data
):
676 """ Takes API objects and unserializes on existing comment """
677 # Do initial checks to verify the object is correct
678 required_attributes
= ["content", "inReplyTo"]
679 for attr
in required_attributes
:
683 # Validate inReplyTo has ID
684 if "id" not in data
["inReplyTo"]:
687 # Validate that the ID is correct
689 media_id
= int(data
["inReplyTo"]["id"])
693 media
= MediaEntry
.query
.filter_by(id=media_id
).first()
697 self
.media_entry
= media
.id
698 self
.content
= data
["content"]
703 class Collection(Base
, CollectionMixin
):
704 """An 'album' or 'set' of media by a user.
706 On deletion, contained CollectionItems get automatically reaped via
708 __tablename__
= "core__collections"
710 id = Column(Integer
, primary_key
=True)
711 title
= Column(Unicode
, nullable
=False)
712 slug
= Column(Unicode
)
713 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
,
715 description
= Column(UnicodeText
)
716 creator
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
717 # TODO: No of items in Collection. Badly named, can we migrate to num_items?
718 items
= Column(Integer
, default
=0)
720 # Cascade: Collections are owned by their creator. So do the full thing.
721 get_creator
= relationship(User
,
722 backref
=backref("collections",
723 cascade
="all, delete-orphan"))
726 UniqueConstraint('creator', 'slug'),
729 def get_collection_items(self
, ascending
=False):
730 #TODO, is this still needed with self.collection_items being available?
731 order_col
= CollectionItem
.position
733 order_col
= desc(order_col
)
734 return CollectionItem
.query
.filter_by(
735 collection
=self
.id).order_by(order_col
)
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 class ProcessingMetaData(Base
):
768 __tablename__
= 'core__processing_metadata'
770 id = Column(Integer
, primary_key
=True)
771 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False,
773 media_entry
= relationship(MediaEntry
,
774 backref
=backref('processing_metadata',
775 cascade
='all, delete-orphan'))
776 callback_url
= Column(Unicode
)
780 """A dict like view on this object"""
781 return DictReadAttrProxy(self
)
784 class CommentSubscription(Base
):
785 __tablename__
= 'core__comment_subscriptions'
786 id = Column(Integer
, primary_key
=True)
788 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
790 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False)
791 media_entry
= relationship(MediaEntry
,
792 backref
=backref('comment_subscriptions',
793 cascade
='all, delete-orphan'))
795 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
796 user
= relationship(User
,
797 backref
=backref('comment_subscriptions',
798 cascade
='all, delete-orphan'))
800 notify
= Column(Boolean
, nullable
=False, default
=True)
801 send_email
= Column(Boolean
, nullable
=False, default
=True)
804 return ('<{classname} #{id}: {user} {media} notify: '
805 '{notify} email: {email}>').format(
807 classname
=self
.__class
__.__name
__,
809 media
=self
.media_entry
,
811 email
=self
.send_email
)
814 class Notification(Base
):
815 __tablename__
= 'core__notifications'
816 id = Column(Integer
, primary_key
=True)
817 type = Column(Unicode
)
819 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now
)
821 user_id
= Column(Integer
, ForeignKey('core__users.id'), nullable
=False,
823 seen
= Column(Boolean
, default
=lambda: False, index
=True)
826 backref
=backref('notifications', cascade
='all, delete-orphan'))
829 'polymorphic_identity': 'notification',
830 'polymorphic_on': type
834 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
836 klass
=self
.__class
__.__name
__,
838 subject
=getattr(self
, 'subject', None),
839 seen
='unseen' if not self
.seen
else 'seen')
841 def __unicode__(self
):
842 return u
'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
844 klass
=self
.__class
__.__name
__,
846 subject
=getattr(self
, 'subject', None),
847 seen
='unseen' if not self
.seen
else 'seen')
850 class CommentNotification(Notification
):
851 __tablename__
= 'core__comment_notifications'
852 id = Column(Integer
, ForeignKey(Notification
.id), primary_key
=True)
854 subject_id
= Column(Integer
, ForeignKey(MediaComment
.id))
855 subject
= relationship(
857 backref
=backref('comment_notifications', cascade
='all, delete-orphan'))
860 'polymorphic_identity': 'comment_notification'
864 class ProcessingNotification(Notification
):
865 __tablename__
= 'core__processing_notifications'
867 id = Column(Integer
, ForeignKey(Notification
.id), primary_key
=True)
869 subject_id
= Column(Integer
, ForeignKey(MediaEntry
.id))
870 subject
= relationship(
872 backref
=backref('processing_notifications',
873 cascade
='all, delete-orphan'))
876 'polymorphic_identity': 'processing_notification'
881 [ProcessingNotification
, CommentNotification
])
883 class ReportBase(Base
):
885 This is the basic report object which the other reports are based off of.
887 :keyword reporter_id Holds the id of the user who created
888 the report, as an Integer column.
889 :keyword report_content Hold the explanation left by the repor-
890 -ter to indicate why they filed the
891 report in the first place, as a
893 :keyword reported_user_id Holds the id of the user who created
894 the content which was reported, as
896 :keyword created Holds a datetime column of when the re-
898 :keyword discriminator This column distinguishes between the
899 different types of reports.
900 :keyword resolver_id Holds the id of the moderator/admin who
902 :keyword resolved Holds the DateTime object which descri-
903 -bes when this report was resolved
904 :keyword result Holds the UnicodeText column of the
905 resolver's reasons for resolving
906 the report this way. Some of this
909 __tablename__
= 'core__reports'
910 id = Column(Integer
, primary_key
=True)
911 reporter_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
912 reporter
= relationship(
914 backref
=backref("reports_filed_by",
916 cascade
="all, delete-orphan"),
917 primaryjoin
="User.id==ReportBase.reporter_id")
918 report_content
= Column(UnicodeText
)
919 reported_user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
920 reported_user
= relationship(
922 backref
=backref("reports_filed_on",
924 cascade
="all, delete-orphan"),
925 primaryjoin
="User.id==ReportBase.reported_user_id")
926 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.now())
927 discriminator
= Column('type', Unicode(50))
928 resolver_id
= Column(Integer
, ForeignKey(User
.id))
929 resolver
= relationship(
931 backref
=backref("reports_resolved_by",
933 cascade
="all, delete-orphan"),
934 primaryjoin
="User.id==ReportBase.resolver_id")
936 resolved
= Column(DateTime
)
937 result
= Column(UnicodeText
)
938 __mapper_args__
= {'polymorphic_on': discriminator
}
940 def is_comment_report(self
):
941 return self
.discriminator
=='comment_report'
943 def is_media_entry_report(self
):
944 return self
.discriminator
=='media_report'
946 def is_archived_report(self
):
947 return self
.resolved
is not None
949 def archive(self
,resolver_id
, resolved
, result
):
950 self
.resolver_id
= resolver_id
951 self
.resolved
= resolved
955 class CommentReport(ReportBase
):
957 Reports that have been filed on comments.
958 :keyword comment_id Holds the integer value of the reported
961 __tablename__
= 'core__reports_on_comments'
962 __mapper_args__
= {'polymorphic_identity': 'comment_report'}
964 id = Column('id',Integer
, ForeignKey('core__reports.id'),
966 comment_id
= Column(Integer
, ForeignKey(MediaComment
.id), nullable
=True)
967 comment
= relationship(
968 MediaComment
, backref
=backref("reports_filed_on",
972 class MediaReport(ReportBase
):
974 Reports that have been filed on media entries
975 :keyword media_entry_id Holds the integer value of the reported
978 __tablename__
= 'core__reports_on_media'
979 __mapper_args__
= {'polymorphic_identity': 'media_report'}
981 id = Column('id',Integer
, ForeignKey('core__reports.id'),
983 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=True)
984 media_entry
= relationship(
986 backref
=backref("reports_filed_on",
991 Holds the information on a specific user's ban-state. As long as one of
992 these is attached to a user, they are banned from accessing mediagoblin.
993 When they try to log in, they are greeted with a page that tells them
994 the reason why they are banned and when (if ever) the ban will be
997 :keyword user_id Holds the id of the user this object is
998 attached to. This is a one-to-one
1000 :keyword expiration_date Holds the date that the ban will be lifted.
1001 If this is null, the ban is permanent
1002 unless a moderator manually lifts it.
1003 :keyword reason Holds the reason why the user was banned.
1005 __tablename__
= 'core__user_bans'
1007 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False,
1009 expiration_date
= Column(Date
)
1010 reason
= Column(UnicodeText
, nullable
=False)
1013 class Privilege(Base
):
1015 The Privilege table holds all of the different privileges a user can hold.
1016 If a user 'has' a privilege, the User object is in a relationship with the
1019 :keyword privilege_name Holds a unicode object that is the recognizable
1020 name of this privilege. This is the column
1021 used for identifying whether or not a user
1022 has a necessary privilege or not.
1025 __tablename__
= 'core__privileges'
1027 id = Column(Integer
, nullable
=False, primary_key
=True)
1028 privilege_name
= Column(Unicode
, nullable
=False, unique
=True)
1029 all_users
= relationship(
1031 backref
='all_privileges',
1032 secondary
="core__privileges_users")
1034 def __init__(self
, privilege_name
):
1036 Currently consructors are required for tables that are initialized thru
1037 the FOUNDATIONS system. This is because they need to be able to be con-
1038 -structed by a list object holding their arg*s
1040 self
.privilege_name
= privilege_name
1043 return "<Privilege %s>" % (self
.privilege_name
)
1046 class PrivilegeUserAssociation(Base
):
1048 This table holds the many-to-many relationship between User and Privilege
1051 __tablename__
= 'core__privileges_users'
1056 ForeignKey(User
.id),
1061 ForeignKey(Privilege
.id),
1065 User
, MediaEntry
, Tag
, MediaTag
, MediaComment
, Collection
, CollectionItem
,
1066 MediaFile
, FileKeynames
, MediaAttachmentFile
, ProcessingMetaData
,
1067 Notification
, CommentNotification
, ProcessingNotification
, Client
,
1068 CommentSubscription
, ReportBase
, CommentReport
, MediaReport
, UserBan
,
1069 Privilege
, PrivilegeUserAssociation
,
1070 RequestToken
, AccessToken
, NonceTimestamp
]
1073 Foundations are the default rows that are created immediately after the tables
1074 are initialized. Each entry to this dictionary should be in the format of:
1075 ModelConstructorObject:List of Dictionaries
1076 (Each Dictionary represents a row on the Table to be created, containing each
1077 of the columns' names as a key string, and each of the columns' values as a
1080 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1081 user_foundations = [{'name':u'Joanna', 'age':24},
1082 {'name':u'Andrea', 'age':41}]
1084 FOUNDATIONS = {User:user_foundations}
1086 privilege_foundations
= [{'privilege_name':u
'admin'},
1087 {'privilege_name':u
'moderator'},
1088 {'privilege_name':u
'uploader'},
1089 {'privilege_name':u
'reporter'},
1090 {'privilege_name':u
'commenter'},
1091 {'privilege_name':u
'active'}]
1092 FOUNDATIONS
= {Privilege
:privilege_foundations
}
1094 ######################################################
1095 # Special, migrations-tracking table
1097 # Not listed in MODELS because this is special and not
1098 # really migrated, but used for migrations (for now)
1099 ######################################################
1101 class MigrationData(Base
):
1102 __tablename__
= "core__migrations"
1104 name
= Column(Unicode
, primary_key
=True)
1105 version
= Column(Integer
, nullable
=False, default
=0)
1107 ######################################################
1110 def show_table_init(engine_uri
):
1111 if engine_uri
is None:
1112 engine_uri
= 'sqlite:///:memory:'
1113 from sqlalchemy
import create_engine
1114 engine
= create_engine(engine_uri
, echo
=True)
1116 Base
.metadata
.create_all(engine
)
1119 if __name__
== '__main__':
1120 from sys
import argv
1126 show_table_init(uri
)