1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 TODO: indexes on foreignkeys, where useful.
21 from __future__
import print_function
26 from sqlalchemy
import Column
, Integer
, Unicode
, UnicodeText
, DateTime
, \
27 Boolean
, ForeignKey
, UniqueConstraint
, PrimaryKeyConstraint
, \
28 SmallInteger
, Date
, types
29 from sqlalchemy
.orm
import relationship
, backref
, with_polymorphic
, validates
, \
31 from sqlalchemy
.orm
.collections
import attribute_mapped_collection
32 from sqlalchemy
.sql
.expression
import desc
33 from sqlalchemy
.ext
.associationproxy
import association_proxy
34 from sqlalchemy
.util
import memoized_property
36 from mediagoblin
.db
.extratypes
import (PathTupleWithSlashes
, JSONEncoded
,
38 from mediagoblin
.db
.base
import Base
, DictReadAttrProxy
39 from mediagoblin
.db
.mixin
import UserMixin
, MediaEntryMixin
, \
40 MediaCommentMixin
, CollectionMixin
, CollectionItemMixin
, \
42 from mediagoblin
.tools
.files
import delete_media_files
43 from mediagoblin
.tools
.common
import import_component
44 from mediagoblin
.tools
.routing
import extract_url_arguments
49 _log
= logging
.getLogger(__name__
)
51 class GenericModelReference(Base
):
53 Represents a relationship to any model that is defined with a integer pk
55 NB: This model should not be used directly but through the GenericForeignKey
58 __tablename__
= "core__generic_model_reference"
60 id = Column(Integer
, primary_key
=True)
61 obj_pk
= Column(Integer
, nullable
=False)
63 # This will be the tablename of the model
64 model_type
= Column(Unicode
, nullable
=False)
67 # This can happen if it's yet to be saved
68 if self
.model_type
is None or self
.obj_pk
is None:
71 model
= self
._get
_model
_from
_type
(self
.model_type
)
72 return model
.query
.filter_by(id=self
.obj_pk
).first()
74 def set_object(self
, obj
):
77 # Check we've been given a object
78 if not issubclass(model
, Base
):
79 raise ValueError("Only models can be set as GenericForeignKeys")
81 # Check that the model has an explicit __tablename__ declaration
82 if getattr(model
, "__tablename__", None) is None:
83 raise ValueError("Models must have __tablename__ attribute")
85 # Check that it's not a composite primary key
86 primary_keys
= [key
.name
for key
in class_mapper(model
).primary_key
]
87 if len(primary_keys
) > 1:
88 raise ValueError("Models can not have composite primary keys")
90 # Check that the field on the model is a an integer field
91 pk_column
= getattr(model
, primary_keys
[0])
92 if issubclass(Integer
, pk_column
):
93 raise ValueError("Only models with integer pks can be set")
95 # Ensure that everything has it's ID set
96 obj
.save(commit
=False)
99 self
.model_type
= obj
.__tablename
__
101 def _get_model_from_type(self
, model_type
):
102 """ Gets a model from a tablename (model type) """
103 if getattr(self
.__class
__, "_TYPE_MAP", None) is None:
104 # We want to build on the class (not the instance) a map of all the
105 # models by the table name (type) for easy lookup, this is done on
106 # the class so it can be shared between all instances
108 # to prevent circular imports do import here
109 self
._TYPE
_MAP
= dict(((m
.__tablename
__, m
) for m
in MODELS
))
110 setattr(self
.__class
__, "_TYPE_MAP", self
._TYPE
_MAP
)
112 return self
.__class
__._TYPE
_MAP
[model_type
]
115 class GenericForeignKey(types
.TypeDecorator
):
119 def process_result_value(self
, value
, *args
, **kwargs
):
120 """ Looks up GenericModelReference and model for field """
121 # If this hasn't been set yet return None
125 # Look up the GenericModelReference for this.
126 gmr
= GenericModelReference
.query
.filter_by(id=value
).first()
128 # If it's set to something invalid (i.e. no GMR exists return None)
132 # Ask the GMR for the corresponding model
133 return gmr
.get_object()
135 def process_bind_param(self
, value
, *args
, **kwargs
):
136 """ Save the foreign key """
140 # Is there one for this already.
142 pk
= getattr(value
, "id")
144 gmr
= GenericModelReference
.query
.filter_by(id=pk
).first()
146 # We need to create one
147 gmr
= GenericModelReference(
149 model_type
=model
.__tablename
__
155 def _set_parent_with_dispatch(self
, parent
):
160 class Location(Base
):
161 """ Represents a physical location """
162 __tablename__
= "core__locations"
164 id = Column(Integer
, primary_key
=True)
165 name
= Column(Unicode
)
168 position
= Column(MutationDict
.as_mutable(JSONEncoded
))
169 address
= Column(MutationDict
.as_mutable(JSONEncoded
))
172 def create(cls
, data
, obj
):
174 location
.unserialize(data
)
176 obj
.location
= location
.id
179 def serialize(self
, request
):
180 location
= {"objectType": "place"}
182 if self
.name
is not None:
183 location
["displayName"] = self
.name
186 location
["position"] = self
.position
189 location
["address"] = self
.address
193 def unserialize(self
, data
):
194 if "displayName" in data
:
195 self
.name
= data
["displayName"]
200 # nicer way to do this?
201 if "position" in data
:
202 # TODO: deal with ISO 9709 formatted string as position
203 if "altitude" in data
["position"]:
204 self
.position
["altitude"] = data
["position"]["altitude"]
206 if "direction" in data
["position"]:
207 self
.position
["direction"] = data
["position"]["direction"]
209 if "longitude" in data
["position"]:
210 self
.position
["longitude"] = data
["position"]["longitude"]
212 if "latitude" in data
["position"]:
213 self
.position
["latitude"] = data
["position"]["latitude"]
215 if "address" in data
:
216 if "formatted" in data
["address"]:
217 self
.address
["formatted"] = data
["address"]["formatted"]
219 if "streetAddress" in data
["address"]:
220 self
.address
["streetAddress"] = data
["address"]["streetAddress"]
222 if "locality" in data
["address"]:
223 self
.address
["locality"] = data
["address"]["locality"]
225 if "region" in data
["address"]:
226 self
.address
["region"] = data
["address"]["region"]
228 if "postalCode" in data
["address"]:
229 self
.address
["postalCode"] = data
["addresss"]["postalCode"]
231 if "country" in data
["address"]:
232 self
.address
["country"] = data
["address"]["country"]
234 class User(Base
, UserMixin
):
236 TODO: We should consider moving some rarely used fields
237 into some sort of "shadow" table.
239 __tablename__
= "core__users"
241 id = Column(Integer
, primary_key
=True)
242 username
= Column(Unicode
, nullable
=False, unique
=True)
243 # Note: no db uniqueness constraint on email because it's not
244 # reliable (many email systems case insensitive despite against
245 # the RFC) and because it would be a mess to implement at this
247 email
= Column(Unicode
, nullable
=False)
248 pw_hash
= Column(Unicode
)
249 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
250 # Intented to be nullable=False, but migrations would not work for it
251 # set to nullable=True implicitly.
252 wants_comment_notification
= Column(Boolean
, default
=True)
253 wants_notifications
= Column(Boolean
, default
=True)
254 license_preference
= Column(Unicode
)
255 url
= Column(Unicode
)
256 bio
= Column(UnicodeText
) # ??
257 uploaded
= Column(Integer
, default
=0)
258 upload_limit
= Column(Integer
)
259 location
= Column(Integer
, ForeignKey("core__locations.id"))
260 get_location
= relationship("Location", lazy
="joined")
262 activity
= Column(Integer
, ForeignKey("core__activity_intermediators.id"))
265 # plugin data would be in a separate model
268 return '<{0} #{1} {2} {3} "{4}">'.format(
269 self
.__class
__.__name
__,
271 'verified' if self
.has_privilege(u
'active') else 'non-verified',
272 'admin' if self
.has_privilege(u
'admin') else 'user',
275 def delete(self
, **kwargs
):
276 """Deletes a User and all related entries/comments/files/..."""
277 # Collections get deleted by relationships.
279 media_entries
= MediaEntry
.query
.filter(MediaEntry
.uploader
== self
.id)
280 for media
in media_entries
:
281 # TODO: Make sure that "MediaEntry.delete()" also deletes
282 # all related files/Comments
283 media
.delete(del_orphan_tags
=False, commit
=False)
285 # Delete now unused tags
286 # TODO: import here due to cyclic imports!!! This cries for refactoring
287 from mediagoblin
.db
.util
import clean_orphan_tags
288 clean_orphan_tags(commit
=False)
290 # Delete user, pass through commit=False/True in kwargs
291 super(User
, self
).delete(**kwargs
)
292 _log
.info('Deleted user "{0}" account'.format(self
.username
))
294 def has_privilege(self
, privilege
, allow_admin
=True):
296 This method checks to make sure a user has all the correct privileges
297 to access a piece of content.
299 :param privilege A unicode object which represent the different
300 privileges which may give the user access to
303 :param allow_admin If this is set to True the then if the user is
304 an admin, then this will always return True
305 even if the user hasn't been given the
306 privilege. (defaults to True)
308 priv
= Privilege
.query
.filter_by(privilege_name
=privilege
).one()
309 if priv
in self
.all_privileges
:
311 elif allow_admin
and self
.has_privilege(u
'admin', allow_admin
=False):
318 Checks if this user is banned.
320 :returns True if self is banned
321 :returns False if self is not
323 return UserBan
.query
.get(self
.id) is not None
326 def serialize(self
, request
):
327 published
= UTC
.localize(self
.created
)
329 "id": "acct:{0}@{1}".format(self
.username
, request
.host
),
330 "published": published
.isoformat(),
331 "preferredUsername": self
.username
,
332 "displayName": "{0}@{1}".format(self
.username
, request
.host
),
333 "objectType": self
.object_type
,
340 "href": request
.urlgen(
341 "mediagoblin.api.user.profile",
342 username
=self
.username
,
347 "href": request
.urlgen(
348 "mediagoblin.api.inbox",
349 username
=self
.username
,
354 "href": request
.urlgen(
355 "mediagoblin.api.feed",
356 username
=self
.username
,
364 user
.update({"summary": self
.bio
})
366 user
.update({"url": self
.url
})
368 user
.update({"location": self
.get_location
.serialize(request
)})
372 def unserialize(self
, data
):
373 if "summary" in data
:
374 self
.bio
= data
["summary"]
376 if "location" in data
:
377 Location
.create(data
, self
)
381 Model representing a client - Used for API Auth
383 __tablename__
= "core__clients"
385 id = Column(Unicode
, nullable
=True, primary_key
=True)
386 secret
= Column(Unicode
, nullable
=False)
387 expirey
= Column(DateTime
, nullable
=True)
388 application_type
= Column(Unicode
, nullable
=False)
389 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
390 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
393 redirect_uri
= Column(JSONEncoded
, nullable
=True)
394 logo_url
= Column(Unicode
, nullable
=True)
395 application_name
= Column(Unicode
, nullable
=True)
396 contacts
= Column(JSONEncoded
, nullable
=True)
399 if self
.application_name
:
400 return "<Client {0} - {1}>".format(self
.application_name
, self
.id)
402 return "<Client {0}>".format(self
.id)
404 class RequestToken(Base
):
406 Model for representing the request tokens
408 __tablename__
= "core__request_tokens"
410 token
= Column(Unicode
, primary_key
=True)
411 secret
= Column(Unicode
, nullable
=False)
412 client
= Column(Unicode
, ForeignKey(Client
.id))
413 user
= Column(Integer
, ForeignKey(User
.id), nullable
=True)
414 used
= Column(Boolean
, default
=False)
415 authenticated
= Column(Boolean
, default
=False)
416 verifier
= Column(Unicode
, nullable
=True)
417 callback
= Column(Unicode
, nullable
=False, default
=u
"oob")
418 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
419 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
421 get_client
= relationship(Client
)
423 class AccessToken(Base
):
425 Model for representing the access tokens
427 __tablename__
= "core__access_tokens"
429 token
= Column(Unicode
, nullable
=False, primary_key
=True)
430 secret
= Column(Unicode
, nullable
=False)
431 user
= Column(Integer
, ForeignKey(User
.id))
432 request_token
= Column(Unicode
, ForeignKey(RequestToken
.token
))
433 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
434 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
436 get_requesttoken
= relationship(RequestToken
)
439 class NonceTimestamp(Base
):
441 A place the timestamp and nonce can be stored - this is for OAuth1
443 __tablename__
= "core__nonce_timestamps"
445 nonce
= Column(Unicode
, nullable
=False, primary_key
=True)
446 timestamp
= Column(DateTime
, nullable
=False, primary_key
=True)
448 class MediaEntry(Base
, MediaEntryMixin
):
450 TODO: Consider fetching the media_files using join
452 __tablename__
= "core__media_entries"
454 id = Column(Integer
, primary_key
=True)
455 uploader
= Column(Integer
, ForeignKey(User
.id), nullable
=False, index
=True)
456 title
= Column(Unicode
, nullable
=False)
457 slug
= Column(Unicode
)
458 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
,
460 description
= Column(UnicodeText
) # ??
461 media_type
= Column(Unicode
, nullable
=False)
462 state
= Column(Unicode
, default
=u
'unprocessed', nullable
=False)
463 # or use sqlalchemy.types.Enum?
464 license
= Column(Unicode
)
465 file_size
= Column(Integer
, default
=0)
466 location
= Column(Integer
, ForeignKey("core__locations.id"))
467 get_location
= relationship("Location", lazy
="joined")
469 fail_error
= Column(Unicode
)
470 fail_metadata
= Column(JSONEncoded
)
472 transcoding_progress
= Column(SmallInteger
)
474 queued_media_file
= Column(PathTupleWithSlashes
)
476 queued_task_id
= Column(Unicode
)
479 UniqueConstraint('uploader', 'slug'),
482 get_uploader
= relationship(User
)
484 media_files_helper
= relationship("MediaFile",
485 collection_class
=attribute_mapped_collection("name"),
486 cascade
="all, delete-orphan"
488 media_files
= association_proxy('media_files_helper', 'file_path',
489 creator
=lambda k
, v
: MediaFile(name
=k
, file_path
=v
)
492 attachment_files_helper
= relationship("MediaAttachmentFile",
493 cascade
="all, delete-orphan",
494 order_by
="MediaAttachmentFile.created"
496 attachment_files
= association_proxy("attachment_files_helper", "dict_view",
497 creator
=lambda v
: MediaAttachmentFile(
498 name
=v
["name"], filepath
=v
["filepath"])
501 tags_helper
= relationship("MediaTag",
502 cascade
="all, delete-orphan" # should be automatically deleted
504 tags
= association_proxy("tags_helper", "dict_view",
505 creator
=lambda v
: MediaTag(name
=v
["name"], slug
=v
["slug"])
508 collections_helper
= relationship("CollectionItem",
509 cascade
="all, delete-orphan"
511 collections
= association_proxy("collections_helper", "in_collection")
512 media_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
),
513 default
=MutationDict())
515 activity
= Column(Integer
, ForeignKey("core__activity_intermediators.id"))
520 def get_comments(self
, ascending
=False):
521 order_col
= MediaComment
.created
523 order_col
= desc(order_col
)
524 return self
.all_comments
.order_by(order_col
)
526 def url_to_prev(self
, urlgen
):
527 """get the next 'newer' entry by this user"""
528 media
= MediaEntry
.query
.filter(
529 (MediaEntry
.uploader
== self
.uploader
)
530 & (MediaEntry
.state
== u
'processed')
531 & (MediaEntry
.id > self
.id)).order_by(MediaEntry
.id).first()
533 if media
is not None:
534 return media
.url_for_self(urlgen
)
536 def url_to_next(self
, urlgen
):
537 """get the next 'older' entry by this user"""
538 media
= MediaEntry
.query
.filter(
539 (MediaEntry
.uploader
== self
.uploader
)
540 & (MediaEntry
.state
== u
'processed')
541 & (MediaEntry
.id < self
.id)).order_by(desc(MediaEntry
.id)).first()
543 if media
is not None:
544 return media
.url_for_self(urlgen
)
546 def get_file_metadata(self
, file_key
, metadata_key
=None):
548 Return the file_metadata dict of a MediaFile. If metadata_key is given,
549 return the value of the key.
551 media_file
= MediaFile
.query
.filter_by(media_entry
=self
.id,
552 name
=six
.text_type(file_key
)).first()
556 return media_file
.file_metadata
.get(metadata_key
, None)
558 return media_file
.file_metadata
560 def set_file_metadata(self
, file_key
, **kwargs
):
562 Update the file_metadata of a MediaFile.
564 media_file
= MediaFile
.query
.filter_by(media_entry
=self
.id,
565 name
=six
.text_type(file_key
)).first()
567 file_metadata
= media_file
.file_metadata
or {}
569 for key
, value
in six
.iteritems(kwargs
):
570 file_metadata
[key
] = value
572 media_file
.file_metadata
= file_metadata
576 def media_data(self
):
577 return getattr(self
, self
.media_data_ref
)
579 def media_data_init(self
, **kwargs
):
581 Initialize or update the contents of a media entry's media_data row
583 media_data
= self
.media_data
585 if media_data
is None:
586 # Get the correct table:
587 table
= import_component(self
.media_type
+ '.models:DATA_MODEL')
588 # No media data, so actually add a new one
589 media_data
= table(**kwargs
)
590 # Get the relationship set up.
591 media_data
.get_media_entry
= self
593 # Update old media data
594 for field
, value
in six
.iteritems(kwargs
):
595 setattr(media_data
, field
, value
)
598 def media_data_ref(self
):
599 return import_component(self
.media_type
+ '.models:BACKREF_NAME')
603 # obj.__repr__() should return a str on Python 2
604 safe_title
= self
.title
.encode('utf-8', 'replace')
606 safe_title
= self
.title
608 return '<{classname} {id}: {title}>'.format(
609 classname
=self
.__class
__.__name
__,
613 def delete(self
, del_orphan_tags
=True, **kwargs
):
614 """Delete MediaEntry and all related files/attachments/comments
616 This will *not* automatically delete unused collections, which
619 :param del_orphan_tags: True/false if we delete unused Tags too
620 :param commit: True/False if this should end the db transaction"""
621 # User's CollectionItems are automatically deleted via "cascade".
622 # Comments on this Media are deleted by cascade, hopefully.
624 # Delete all related files/attachments
626 delete_media_files(self
)
627 except OSError as error
:
628 # Returns list of files we failed to delete
629 _log
.error('No such files from the user "{1}" to delete: '
630 '{0}'.format(str(error
), self
.get_uploader
))
631 _log
.info('Deleted Media entry id "{0}"'.format(self
.id))
632 # Related MediaTag's are automatically cleaned, but we might
633 # want to clean out unused Tag's too.
635 # TODO: Import here due to cyclic imports!!!
636 # This cries for refactoring
637 from mediagoblin
.db
.util
import clean_orphan_tags
638 clean_orphan_tags(commit
=False)
639 # pass through commit=False/True in kwargs
640 super(MediaEntry
, self
).delete(**kwargs
)
642 def serialize(self
, request
, show_comments
=True):
643 """ Unserialize MediaEntry to object """
644 href
= request
.urlgen(
645 "mediagoblin.api.object",
646 object_type
=self
.object_type
,
650 author
= self
.get_uploader
651 published
= UTC
.localize(self
.created
)
652 updated
= UTC
.localize(self
.created
)
655 "author": author
.serialize(request
),
656 "objectType": self
.object_type
,
657 "url": self
.url_for_self(request
.urlgen
, qualified
=True),
659 "url": request
.host_url
+ self
.thumb_url
[1:],
662 "url": request
.host_url
+ self
.original_url
[1:],
664 "published": published
.isoformat(),
665 "updated": updated
.isoformat(),
678 context
["displayName"] = self
.title
681 context
["content"] = self
.description
684 context
["license"] = self
.license
687 context
["location"] = self
.get_location
.serialize(request
)
691 comment
.serialize(request
) for comment
in self
.get_comments()]
692 total
= len(comments
)
693 context
["replies"] = {
696 "url": request
.urlgen(
697 "mediagoblin.api.object.comments",
698 object_type
=self
.object_type
,
704 # Add image height and width if possible. We didn't use to store this
705 # data and we're not able (and maybe not willing) to re-process all
706 # images so it's possible this might not exist.
707 if self
.get_file_metadata("thumb", "height"):
708 height
= self
.get_file_metadata("thumb", "height")
709 context
["image"]["height"] = height
710 if self
.get_file_metadata("thumb", "width"):
711 width
= self
.get_file_metadata("thumb", "width")
712 context
["image"]["width"] = width
713 if self
.get_file_metadata("original", "height"):
714 height
= self
.get_file_metadata("original", "height")
715 context
["fullImage"]["height"] = height
716 if self
.get_file_metadata("original", "height"):
717 width
= self
.get_file_metadata("original", "width")
718 context
["fullImage"]["width"] = width
722 def unserialize(self
, data
):
723 """ Takes API objects and unserializes on existing MediaEntry """
724 if "displayName" in data
:
725 self
.title
= data
["displayName"]
727 if "content" in data
:
728 self
.description
= data
["content"]
730 if "license" in data
:
731 self
.license
= data
["license"]
733 if "location" in data
:
734 Licence
.create(data
["location"], self
)
738 class FileKeynames(Base
):
740 keywords for various places.
741 currently the MediaFile keys
743 __tablename__
= "core__file_keynames"
744 id = Column(Integer
, primary_key
=True)
745 name
= Column(Unicode
, unique
=True)
748 return "<FileKeyname %r: %r>" % (self
.id, self
.name
)
751 def find_or_new(cls
, name
):
752 t
= cls
.query
.filter_by(name
=name
).first()
755 return cls(name
=name
)
758 class MediaFile(Base
):
760 TODO: Highly consider moving "name" into a new table.
761 TODO: Consider preloading said table in software
763 __tablename__
= "core__mediafiles"
765 media_entry
= Column(
766 Integer
, ForeignKey(MediaEntry
.id),
768 name_id
= Column(SmallInteger
, ForeignKey(FileKeynames
.id), nullable
=False)
769 file_path
= Column(PathTupleWithSlashes
)
770 file_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
))
773 PrimaryKeyConstraint('media_entry', 'name_id'),
777 return "<MediaFile %s: %r>" % (self
.name
, self
.file_path
)
779 name_helper
= relationship(FileKeynames
, lazy
="joined", innerjoin
=True)
780 name
= association_proxy('name_helper', 'name',
781 creator
=FileKeynames
.find_or_new
785 class MediaAttachmentFile(Base
):
786 __tablename__
= "core__attachment_files"
788 id = Column(Integer
, primary_key
=True)
789 media_entry
= Column(
790 Integer
, ForeignKey(MediaEntry
.id),
792 name
= Column(Unicode
, nullable
=False)
793 filepath
= Column(PathTupleWithSlashes
)
794 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
798 """A dict like view on this object"""
799 return DictReadAttrProxy(self
)
803 __tablename__
= "core__tags"
805 id = Column(Integer
, primary_key
=True)
806 slug
= Column(Unicode
, nullable
=False, unique
=True)
809 return "<Tag %r: %r>" % (self
.id, self
.slug
)
812 def find_or_new(cls
, slug
):
813 t
= cls
.query
.filter_by(slug
=slug
).first()
816 return cls(slug
=slug
)
819 class MediaTag(Base
):
820 __tablename__
= "core__media_tags"
822 id = Column(Integer
, primary_key
=True)
823 media_entry
= Column(
824 Integer
, ForeignKey(MediaEntry
.id),
825 nullable
=False, index
=True)
826 tag
= Column(Integer
, ForeignKey(Tag
.id), nullable
=False, index
=True)
827 name
= Column(Unicode
)
828 # created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
831 UniqueConstraint('tag', 'media_entry'),
834 tag_helper
= relationship(Tag
)
835 slug
= association_proxy('tag_helper', 'slug',
836 creator
=Tag
.find_or_new
839 def __init__(self
, name
=None, slug
=None):
844 self
.tag_helper
= Tag
.find_or_new(slug
)
848 """A dict like view on this object"""
849 return DictReadAttrProxy(self
)
852 class MediaComment(Base
, MediaCommentMixin
):
853 __tablename__
= "core__media_comments"
855 id = Column(Integer
, primary_key
=True)
856 media_entry
= Column(
857 Integer
, ForeignKey(MediaEntry
.id), nullable
=False, index
=True)
858 author
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
859 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
860 content
= Column(UnicodeText
, nullable
=False)
861 location
= Column(Integer
, ForeignKey("core__locations.id"))
862 get_location
= relationship("Location", lazy
="joined")
864 # Cascade: Comments are owned by their creator. So do the full thing.
865 # lazy=dynamic: People might post a *lot* of comments,
866 # so make the "posted_comments" a query-like thing.
867 get_author
= relationship(User
,
868 backref
=backref("posted_comments",
870 cascade
="all, delete-orphan"))
871 get_entry
= relationship(MediaEntry
,
872 backref
=backref("comments",
874 cascade
="all, delete-orphan"))
876 # Cascade: Comments are somewhat owned by their MediaEntry.
877 # So do the full thing.
878 # lazy=dynamic: MediaEntries might have many comments,
879 # so make the "all_comments" a query-like thing.
880 get_media_entry
= relationship(MediaEntry
,
881 backref
=backref("all_comments",
883 cascade
="all, delete-orphan"))
886 activity
= Column(Integer
, ForeignKey("core__activity_intermediators.id"))
888 def serialize(self
, request
):
889 """ Unserialize to python dictionary for API """
890 href
= request
.urlgen(
891 "mediagoblin.api.object",
892 object_type
=self
.object_type
,
896 media
= MediaEntry
.query
.filter_by(id=self
.media_entry
).first()
897 author
= self
.get_author
898 published
= UTC
.localize(self
.created
)
901 "objectType": self
.object_type
,
902 "content": self
.content
,
903 "inReplyTo": media
.serialize(request
, show_comments
=False),
904 "author": author
.serialize(request
),
905 "published": published
.isoformat(),
906 "updated": published
.isoformat(),
910 context
["location"] = self
.get_location
.seralize(request
)
914 def unserialize(self
, data
, request
):
915 """ Takes API objects and unserializes on existing comment """
916 # Handle changing the reply ID
917 if "inReplyTo" in data
:
918 # Validate that the ID is correct
920 media_id
= int(extract_url_arguments(
921 url
=data
["inReplyTo"]["id"],
922 urlmap
=request
.app
.url_map
927 media
= MediaEntry
.query
.filter_by(id=media_id
).first()
931 self
.media_entry
= media
.id
933 if "content" in data
:
934 self
.content
= data
["content"]
936 if "location" in data
:
937 Location
.create(data
["location"], self
)
943 class Collection(Base
, CollectionMixin
):
944 """An 'album' or 'set' of media by a user.
946 On deletion, contained CollectionItems get automatically reaped via
948 __tablename__
= "core__collections"
950 id = Column(Integer
, primary_key
=True)
951 title
= Column(Unicode
, nullable
=False)
952 slug
= Column(Unicode
)
953 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
,
955 description
= Column(UnicodeText
)
956 creator
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
957 location
= Column(Integer
, ForeignKey("core__locations.id"))
958 get_location
= relationship("Location", lazy
="joined")
960 # TODO: No of items in Collection. Badly named, can we migrate to num_items?
961 items
= Column(Integer
, default
=0)
963 # Cascade: Collections are owned by their creator. So do the full thing.
964 get_creator
= relationship(User
,
965 backref
=backref("collections",
966 cascade
="all, delete-orphan"))
968 activity
= Column(Integer
, ForeignKey("core__activity_intermediators.id"))
971 UniqueConstraint('creator', 'slug'),
974 def get_collection_items(self
, ascending
=False):
975 #TODO, is this still needed with self.collection_items being available?
976 order_col
= CollectionItem
.position
978 order_col
= desc(order_col
)
979 return CollectionItem
.query
.filter_by(
980 collection
=self
.id).order_by(order_col
)
983 safe_title
= self
.title
.encode('ascii', 'replace')
984 return '<{classname} #{id}: {title} by {creator}>'.format(
986 classname
=self
.__class
__.__name
__,
987 creator
=self
.creator
,
990 def serialize(self
, request
):
991 # Get all serialized output in a list
993 for item
in self
.get_collection_items():
994 items
.append(item
.serialize(request
))
997 "totalItems": self
.items
,
998 "url": self
.url_for_self(request
.urlgen
, qualified
=True),
1003 class CollectionItem(Base
, CollectionItemMixin
):
1004 __tablename__
= "core__collection_items"
1006 id = Column(Integer
, primary_key
=True)
1007 media_entry
= Column(
1008 Integer
, ForeignKey(MediaEntry
.id), nullable
=False, index
=True)
1009 collection
= Column(Integer
, ForeignKey(Collection
.id), nullable
=False)
1010 note
= Column(UnicodeText
, nullable
=True)
1011 added
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1012 position
= Column(Integer
)
1014 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
1015 in_collection
= relationship(Collection
,
1018 cascade
="all, delete-orphan"))
1020 get_media_entry
= relationship(MediaEntry
)
1023 UniqueConstraint('collection', 'media_entry'),
1027 def dict_view(self
):
1028 """A dict like view on this object"""
1029 return DictReadAttrProxy(self
)
1032 return '<{classname} #{id}: Entry {entry} in {collection}>'.format(
1034 classname
=self
.__class
__.__name
__,
1035 collection
=self
.collection
,
1036 entry
=self
.media_entry
)
1038 def serialize(self
, request
):
1039 return self
.get_media_entry
.serialize(request
)
1042 class ProcessingMetaData(Base
):
1043 __tablename__
= 'core__processing_metadata'
1045 id = Column(Integer
, primary_key
=True)
1046 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False,
1048 media_entry
= relationship(MediaEntry
,
1049 backref
=backref('processing_metadata',
1050 cascade
='all, delete-orphan'))
1051 callback_url
= Column(Unicode
)
1054 def dict_view(self
):
1055 """A dict like view on this object"""
1056 return DictReadAttrProxy(self
)
1059 class CommentSubscription(Base
):
1060 __tablename__
= 'core__comment_subscriptions'
1061 id = Column(Integer
, primary_key
=True)
1063 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1065 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False)
1066 media_entry
= relationship(MediaEntry
,
1067 backref
=backref('comment_subscriptions',
1068 cascade
='all, delete-orphan'))
1070 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
1071 user
= relationship(User
,
1072 backref
=backref('comment_subscriptions',
1073 cascade
='all, delete-orphan'))
1075 notify
= Column(Boolean
, nullable
=False, default
=True)
1076 send_email
= Column(Boolean
, nullable
=False, default
=True)
1079 return ('<{classname} #{id}: {user} {media} notify: '
1080 '{notify} email: {email}>').format(
1082 classname
=self
.__class
__.__name
__,
1084 media
=self
.media_entry
,
1086 email
=self
.send_email
)
1089 class Notification(Base
):
1090 __tablename__
= 'core__notifications'
1091 id = Column(Integer
, primary_key
=True)
1092 type = Column(Unicode
)
1094 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1096 user_id
= Column(Integer
, ForeignKey('core__users.id'), nullable
=False,
1098 seen
= Column(Boolean
, default
=lambda: False, index
=True)
1099 user
= relationship(
1101 backref
=backref('notifications', cascade
='all, delete-orphan'))
1104 'polymorphic_identity': 'notification',
1105 'polymorphic_on': type
1109 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1111 klass
=self
.__class
__.__name
__,
1113 subject
=getattr(self
, 'subject', None),
1114 seen
='unseen' if not self
.seen
else 'seen')
1116 def __unicode__(self
):
1117 return u
'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1119 klass
=self
.__class
__.__name
__,
1121 subject
=getattr(self
, 'subject', None),
1122 seen
='unseen' if not self
.seen
else 'seen')
1125 class CommentNotification(Notification
):
1126 __tablename__
= 'core__comment_notifications'
1127 id = Column(Integer
, ForeignKey(Notification
.id), primary_key
=True)
1129 subject_id
= Column(Integer
, ForeignKey(MediaComment
.id))
1130 subject
= relationship(
1132 backref
=backref('comment_notifications', cascade
='all, delete-orphan'))
1135 'polymorphic_identity': 'comment_notification'
1139 class ProcessingNotification(Notification
):
1140 __tablename__
= 'core__processing_notifications'
1142 id = Column(Integer
, ForeignKey(Notification
.id), primary_key
=True)
1144 subject_id
= Column(Integer
, ForeignKey(MediaEntry
.id))
1145 subject
= relationship(
1147 backref
=backref('processing_notifications',
1148 cascade
='all, delete-orphan'))
1151 'polymorphic_identity': 'processing_notification'
1154 # the with_polymorphic call has been moved to the bottom above MODELS
1155 # this is because it causes conflicts with relationship calls.
1157 class ReportBase(Base
):
1159 This is the basic report object which the other reports are based off of.
1161 :keyword reporter_id Holds the id of the user who created
1162 the report, as an Integer column.
1163 :keyword report_content Hold the explanation left by the repor-
1164 -ter to indicate why they filed the
1165 report in the first place, as a
1167 :keyword reported_user_id Holds the id of the user who created
1168 the content which was reported, as
1170 :keyword created Holds a datetime column of when the re-
1172 :keyword discriminator This column distinguishes between the
1173 different types of reports.
1174 :keyword resolver_id Holds the id of the moderator/admin who
1175 resolved the report.
1176 :keyword resolved Holds the DateTime object which descri-
1177 -bes when this report was resolved
1178 :keyword result Holds the UnicodeText column of the
1179 resolver's reasons for resolving
1180 the report this way. Some of this
1183 __tablename__
= 'core__reports'
1184 id = Column(Integer
, primary_key
=True)
1185 reporter_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
1186 reporter
= relationship(
1188 backref
=backref("reports_filed_by",
1190 cascade
="all, delete-orphan"),
1191 primaryjoin
="User.id==ReportBase.reporter_id")
1192 report_content
= Column(UnicodeText
)
1193 reported_user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
1194 reported_user
= relationship(
1196 backref
=backref("reports_filed_on",
1198 cascade
="all, delete-orphan"),
1199 primaryjoin
="User.id==ReportBase.reported_user_id")
1200 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1201 discriminator
= Column('type', Unicode(50))
1202 resolver_id
= Column(Integer
, ForeignKey(User
.id))
1203 resolver
= relationship(
1205 backref
=backref("reports_resolved_by",
1207 cascade
="all, delete-orphan"),
1208 primaryjoin
="User.id==ReportBase.resolver_id")
1210 resolved
= Column(DateTime
)
1211 result
= Column(UnicodeText
)
1212 __mapper_args__
= {'polymorphic_on': discriminator
}
1214 def is_comment_report(self
):
1215 return self
.discriminator
=='comment_report'
1217 def is_media_entry_report(self
):
1218 return self
.discriminator
=='media_report'
1220 def is_archived_report(self
):
1221 return self
.resolved
is not None
1223 def archive(self
,resolver_id
, resolved
, result
):
1224 self
.resolver_id
= resolver_id
1225 self
.resolved
= resolved
1226 self
.result
= result
1229 class CommentReport(ReportBase
):
1231 Reports that have been filed on comments.
1232 :keyword comment_id Holds the integer value of the reported
1235 __tablename__
= 'core__reports_on_comments'
1236 __mapper_args__
= {'polymorphic_identity': 'comment_report'}
1238 id = Column('id',Integer
, ForeignKey('core__reports.id'),
1240 comment_id
= Column(Integer
, ForeignKey(MediaComment
.id), nullable
=True)
1241 comment
= relationship(
1242 MediaComment
, backref
=backref("reports_filed_on",
1246 class MediaReport(ReportBase
):
1248 Reports that have been filed on media entries
1249 :keyword media_entry_id Holds the integer value of the reported
1252 __tablename__
= 'core__reports_on_media'
1253 __mapper_args__
= {'polymorphic_identity': 'media_report'}
1255 id = Column('id',Integer
, ForeignKey('core__reports.id'),
1257 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=True)
1258 media_entry
= relationship(
1260 backref
=backref("reports_filed_on",
1263 class UserBan(Base
):
1265 Holds the information on a specific user's ban-state. As long as one of
1266 these is attached to a user, they are banned from accessing mediagoblin.
1267 When they try to log in, they are greeted with a page that tells them
1268 the reason why they are banned and when (if ever) the ban will be
1271 :keyword user_id Holds the id of the user this object is
1272 attached to. This is a one-to-one
1274 :keyword expiration_date Holds the date that the ban will be lifted.
1275 If this is null, the ban is permanent
1276 unless a moderator manually lifts it.
1277 :keyword reason Holds the reason why the user was banned.
1279 __tablename__
= 'core__user_bans'
1281 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False,
1283 expiration_date
= Column(Date
)
1284 reason
= Column(UnicodeText
, nullable
=False)
1287 class Privilege(Base
):
1289 The Privilege table holds all of the different privileges a user can hold.
1290 If a user 'has' a privilege, the User object is in a relationship with the
1293 :keyword privilege_name Holds a unicode object that is the recognizable
1294 name of this privilege. This is the column
1295 used for identifying whether or not a user
1296 has a necessary privilege or not.
1299 __tablename__
= 'core__privileges'
1301 id = Column(Integer
, nullable
=False, primary_key
=True)
1302 privilege_name
= Column(Unicode
, nullable
=False, unique
=True)
1303 all_users
= relationship(
1305 backref
='all_privileges',
1306 secondary
="core__privileges_users")
1308 def __init__(self
, privilege_name
):
1310 Currently consructors are required for tables that are initialized thru
1311 the FOUNDATIONS system. This is because they need to be able to be con-
1312 -structed by a list object holding their arg*s
1314 self
.privilege_name
= privilege_name
1317 return "<Privilege %s>" % (self
.privilege_name
)
1320 class PrivilegeUserAssociation(Base
):
1322 This table holds the many-to-many relationship between User and Privilege
1325 __tablename__
= 'core__privileges_users'
1330 ForeignKey(User
.id),
1335 ForeignKey(Privilege
.id),
1338 class Generator(Base
):
1339 """ Information about what created an activity """
1340 __tablename__
= "core__generators"
1342 id = Column(Integer
, primary_key
=True)
1343 name
= Column(Unicode
, nullable
=False)
1344 published
= Column(DateTime
, default
=datetime
.datetime
.utcnow
)
1345 updated
= Column(DateTime
, default
=datetime
.datetime
.utcnow
)
1346 object_type
= Column(Unicode
, nullable
=False)
1349 return "<{klass} {name}>".format(
1350 klass
=self
.__class
__.__name
__,
1354 def serialize(self
, request
):
1355 href
= request
.urlgen(
1356 "mediagoblin.api.object",
1357 object_type
=self
.object_type
,
1361 published
= UTC
.localize(self
.published
)
1362 updated
= UTC
.localize(self
.updated
)
1365 "displayName": self
.name
,
1366 "published": published
.isoformat(),
1367 "updated": updated
.isoformat(),
1368 "objectType": self
.object_type
,
1371 def unserialize(self
, data
):
1372 if "displayName" in data
:
1373 self
.name
= data
["displayName"]
1376 class ActivityIntermediator(Base
):
1378 This is used so that objects/targets can have a foreign key back to this
1379 object and activities can a foreign key to this object. This objects to be
1380 used multiple times for the activity object or target and also allows for
1381 different types of objects to be used as an Activity.
1383 __tablename__
= "core__activity_intermediators"
1385 id = Column(Integer
, primary_key
=True)
1386 type = Column(Unicode
, nullable
=False)
1390 "media": MediaEntry
,
1391 "comment": MediaComment
,
1392 "collection": Collection
,
1395 def _find_model(self
, obj
):
1396 """ Finds the model for a given object """
1397 for key
, model
in self
.TYPES
.items():
1398 if isinstance(obj
, model
):
1404 """ This sets itself as the activity """
1405 key
, model
= self
._find
_model
(obj
)
1407 raise ValueError("Invalid type of object given")
1411 # We need to populate the self.id so we need to save but, we don't
1412 # want to save this AI in the database (yet) so commit=False.
1413 self
.save(commit
=False)
1414 obj
.activity
= self
.id
1418 """ Finds the object for an activity """
1419 if self
.type is None:
1422 model
= self
.TYPES
[self
.type]
1423 return model
.query
.filter_by(activity
=self
.id).first()
1426 def validate_type(self
, key
, value
):
1427 """ Validate that the type set is a valid type """
1428 assert value
in self
.TYPES
1431 class Activity(Base
, ActivityMixin
):
1433 This holds all the metadata about an activity such as uploading an image,
1434 posting a comment, etc.
1436 __tablename__
= "core__activities"
1438 id = Column(Integer
, primary_key
=True)
1439 actor
= Column(Integer
,
1440 ForeignKey("core__users.id"),
1442 published
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1443 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1444 verb
= Column(Unicode
, nullable
=False)
1445 content
= Column(Unicode
, nullable
=True)
1446 title
= Column(Unicode
, nullable
=True)
1447 generator
= Column(Integer
,
1448 ForeignKey("core__generators.id"),
1450 object = Column(GenericForeignKey(),
1452 target
= Column(GenericForeignKey(),
1455 get_actor
= relationship(User
,
1456 backref
=backref("activities",
1457 cascade
="all, delete-orphan"))
1458 get_generator
= relationship(Generator
)
1461 if self
.content
is None:
1462 return "<{klass} verb:{verb}>".format(
1463 klass
=self
.__class
__.__name
__,
1467 return "<{klass} {content}>".format(
1468 klass
=self
.__class
__.__name
__,
1469 content
=self
.content
1472 def save(self
, set_updated
=True, *args
, **kwargs
):
1474 self
.updated
= datetime
.datetime
.now()
1475 super(Activity
, self
).save(*args
, **kwargs
)
1479 [ProcessingNotification
, CommentNotification
])
1482 User
, MediaEntry
, Tag
, MediaTag
, MediaComment
, Collection
, CollectionItem
,
1483 MediaFile
, FileKeynames
, MediaAttachmentFile
, ProcessingMetaData
,
1484 Notification
, CommentNotification
, ProcessingNotification
, Client
,
1485 CommentSubscription
, ReportBase
, CommentReport
, MediaReport
, UserBan
,
1486 Privilege
, PrivilegeUserAssociation
,
1487 RequestToken
, AccessToken
, NonceTimestamp
,
1488 Activity
, ActivityIntermediator
, Generator
,
1489 Location
, GenericModelReference
]
1492 Foundations are the default rows that are created immediately after the tables
1493 are initialized. Each entry to this dictionary should be in the format of:
1494 ModelConstructorObject:List of Dictionaries
1495 (Each Dictionary represents a row on the Table to be created, containing each
1496 of the columns' names as a key string, and each of the columns' values as a
1499 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1500 user_foundations = [{'name':u'Joanna', 'age':24},
1501 {'name':u'Andrea', 'age':41}]
1503 FOUNDATIONS = {User:user_foundations}
1505 privilege_foundations
= [{'privilege_name':u
'admin'},
1506 {'privilege_name':u
'moderator'},
1507 {'privilege_name':u
'uploader'},
1508 {'privilege_name':u
'reporter'},
1509 {'privilege_name':u
'commenter'},
1510 {'privilege_name':u
'active'}]
1511 FOUNDATIONS
= {Privilege
:privilege_foundations
}
1513 ######################################################
1514 # Special, migrations-tracking table
1516 # Not listed in MODELS because this is special and not
1517 # really migrated, but used for migrations (for now)
1518 ######################################################
1520 class MigrationData(Base
):
1521 __tablename__
= "core__migrations"
1523 name
= Column(Unicode
, primary_key
=True)
1524 version
= Column(Integer
, nullable
=False, default
=0)
1526 ######################################################
1529 def show_table_init(engine_uri
):
1530 if engine_uri
is None:
1531 engine_uri
= 'sqlite:///:memory:'
1532 from sqlalchemy
import create_engine
1533 engine
= create_engine(engine_uri
, echo
=True)
1535 Base
.metadata
.create_all(engine
)
1538 if __name__
== '__main__':
1539 from sys
import argv
1545 show_table_init(uri
)