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
, Float
29 from sqlalchemy
.orm
import relationship
, backref
, with_polymorphic
, validates
, \
31 from sqlalchemy
.orm
.collections
import attribute_mapped_collection
32 from sqlalchemy
.sql
import and_
33 from sqlalchemy
.sql
.expression
import desc
34 from sqlalchemy
.ext
.associationproxy
import association_proxy
35 from sqlalchemy
.util
import memoized_property
37 from mediagoblin
.db
.extratypes
import (PathTupleWithSlashes
, JSONEncoded
,
39 from mediagoblin
.db
.base
import Base
, DictReadAttrProxy
, FakeCursor
40 from mediagoblin
.db
.mixin
import UserMixin
, MediaEntryMixin
, \
41 CollectionMixin
, CollectionItemMixin
, ActivityMixin
, TextCommentMixin
, \
43 from mediagoblin
.tools
.files
import delete_media_files
44 from mediagoblin
.tools
.common
import import_component
45 from mediagoblin
.tools
.routing
import extract_url_arguments
46 from mediagoblin
.tools
.text
import convert_to_tag_list_of_dicts
49 from six
.moves
.urllib
.parse
import urljoin
52 _log
= logging
.getLogger(__name__
)
54 class GenericModelReference(Base
):
56 Represents a relationship to any model that is defined with a integer pk
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)
66 # Constrain it so obj_pk and model_type have to be unique
67 # They should be this order as the index is generated, "model_type" will be
68 # the major order as it's put first.
70 UniqueConstraint("model_type", "obj_pk"),
74 # This can happen if it's yet to be saved
75 if self
.model_type
is None or self
.obj_pk
is None:
78 model
= self
._get
_model
_from
_type
(self
.model_type
)
79 return model
.query
.filter_by(id=self
.obj_pk
).first()
81 def set_object(self
, obj
):
84 # Check we've been given a object
85 if not issubclass(model
, Base
):
86 raise ValueError("Only models can be set as using the GMR")
88 # Check that the model has an explicit __tablename__ declaration
89 if getattr(model
, "__tablename__", None) is None:
90 raise ValueError("Models must have __tablename__ attribute")
92 # Check that it's not a composite primary key
93 primary_keys
= [key
.name
for key
in class_mapper(model
).primary_key
]
94 if len(primary_keys
) > 1:
95 raise ValueError("Models can not have composite primary keys")
97 # Check that the field on the model is a an integer field
98 pk_column
= getattr(model
, primary_keys
[0])
99 if not isinstance(pk_column
.type, Integer
):
100 raise ValueError("Only models with integer pks can be set")
102 if getattr(obj
, pk_column
.key
) is None:
103 obj
.save(commit
=False)
105 self
.obj_pk
= getattr(obj
, pk_column
.key
)
106 self
.model_type
= obj
.__tablename
__
108 def _get_model_from_type(self
, model_type
):
109 """ Gets a model from a tablename (model type) """
110 if getattr(type(self
), "_TYPE_MAP", None) is None:
111 # We want to build on the class (not the instance) a map of all the
112 # models by the table name (type) for easy lookup, this is done on
113 # the class so it can be shared between all instances
115 # to prevent circular imports do import here
116 registry
= dict(Base
._decl
_class
_registry
).values()
117 self
._TYPE
_MAP
= dict(
118 ((m
.__tablename
__, m
) for m
in registry
if hasattr(m
, "__tablename__"))
120 setattr(type(self
), "_TYPE_MAP", self
._TYPE
_MAP
)
122 return self
.__class
__._TYPE
_MAP
[model_type
]
125 def find_for_obj(cls
, obj
):
126 """ Finds a GMR for an object or returns None """
127 # Is there one for this already.
129 pk
= getattr(obj
, "id")
131 gmr
= cls
.query
.filter_by(
133 model_type
=model
.__tablename
__
139 def find_or_new(cls
, obj
):
140 """ Finds an existing GMR or creates a new one for the object """
141 gmr
= cls
.find_for_obj(obj
)
143 # If there isn't one already create one
147 model_type
=type(obj
).__tablename
__
152 class Location(Base
):
153 """ Represents a physical location """
154 __tablename__
= "core__locations"
156 id = Column(Integer
, primary_key
=True)
157 name
= Column(Unicode
)
160 position
= Column(MutationDict
.as_mutable(JSONEncoded
))
161 address
= Column(MutationDict
.as_mutable(JSONEncoded
))
164 def create(cls
, data
, obj
):
166 location
.unserialize(data
)
168 obj
.location
= location
.id
171 def serialize(self
, request
):
172 location
= {"objectType": "place"}
174 if self
.name
is not None:
175 location
["displayName"] = self
.name
178 location
["position"] = self
.position
181 location
["address"] = self
.address
185 def unserialize(self
, data
):
186 if "displayName" in data
:
187 self
.name
= data
["displayName"]
192 # nicer way to do this?
193 if "position" in data
:
194 # TODO: deal with ISO 9709 formatted string as position
195 if "altitude" in data
["position"]:
196 self
.position
["altitude"] = data
["position"]["altitude"]
198 if "direction" in data
["position"]:
199 self
.position
["direction"] = data
["position"]["direction"]
201 if "longitude" in data
["position"]:
202 self
.position
["longitude"] = data
["position"]["longitude"]
204 if "latitude" in data
["position"]:
205 self
.position
["latitude"] = data
["position"]["latitude"]
207 if "address" in data
:
208 if "formatted" in data
["address"]:
209 self
.address
["formatted"] = data
["address"]["formatted"]
211 if "streetAddress" in data
["address"]:
212 self
.address
["streetAddress"] = data
["address"]["streetAddress"]
214 if "locality" in data
["address"]:
215 self
.address
["locality"] = data
["address"]["locality"]
217 if "region" in data
["address"]:
218 self
.address
["region"] = data
["address"]["region"]
220 if "postalCode" in data
["address"]:
221 self
.address
["postalCode"] = data
["addresss"]["postalCode"]
223 if "country" in data
["address"]:
224 self
.address
["country"] = data
["address"]["country"]
226 class User(Base
, UserMixin
):
228 Base user that is common amongst LocalUser and RemoteUser.
230 This holds all the fields which are common between both the Local and Remote
233 NB: ForeignKeys should reference this User model and NOT the LocalUser or
236 __tablename__
= "core__users"
238 id = Column(Integer
, primary_key
=True)
239 url
= Column(Unicode
)
240 bio
= Column(UnicodeText
)
241 name
= Column(Unicode
)
243 # This is required for the polymorphic inheritance
244 type = Column(Unicode
)
246 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
247 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
249 location
= Column(Integer
, ForeignKey("core__locations.id"))
252 get_location
= relationship("Location", lazy
="joined")
255 'polymorphic_identity': 'user',
256 'polymorphic_on': type,
259 deletion_mode
= Base
.SOFT_DELETE
261 def soft_delete(self
, *args
, **kwargs
):
262 # Find all the Collections and delete those
263 for collection
in Collection
.query
.filter_by(actor
=self
.id):
264 collection
.delete(**kwargs
)
266 # Find all the comments and delete those too
267 for comment
in TextComment
.query
.filter_by(actor
=self
.id):
268 comment
.delete(**kwargs
)
270 # Find all the activities and delete those too
271 for activity
in Activity
.query
.filter_by(actor
=self
.id):
272 activity
.delete(**kwargs
)
274 super(User
, self
).soft_delete(*args
, **kwargs
)
277 def delete(self
, *args
, **kwargs
):
278 """Deletes a User and all related entries/comments/files/..."""
279 # Collections get deleted by relationships.
281 media_entries
= MediaEntry
.query
.filter(MediaEntry
.actor
== self
.id)
282 for media
in media_entries
:
283 # TODO: Make sure that "MediaEntry.delete()" also deletes
284 # all related files/Comments
285 media
.delete(del_orphan_tags
=False, commit
=False)
287 # Delete now unused tags
288 # TODO: import here due to cyclic imports!!! This cries for refactoring
289 from mediagoblin
.db
.util
import clean_orphan_tags
290 clean_orphan_tags(commit
=False)
292 # Delete user, pass through commit=False/True in kwargs
293 username
= self
.username
294 super(User
, self
).delete(*args
, **kwargs
)
295 _log
.info('Deleted user "{0}" account'.format(username
))
297 def has_privilege(self
, privilege
, allow_admin
=True):
299 This method checks to make sure a user has all the correct privileges
300 to access a piece of content.
302 :param privilege A unicode object which represent the different
303 privileges which may give the user access to
306 :param allow_admin If this is set to True the then if the user is
307 an admin, then this will always return True
308 even if the user hasn't been given the
309 privilege. (defaults to True)
311 priv
= Privilege
.query
.filter_by(privilege_name
=privilege
).one()
312 if priv
in self
.all_privileges
:
314 elif allow_admin
and self
.has_privilege(u
'admin', allow_admin
=False):
321 Checks if this user is banned.
323 :returns True if self is banned
324 :returns False if self is not
326 return UserBan
.query
.get(self
.id) is not None
328 def serialize(self
, request
):
329 published
= UTC
.localize(self
.created
)
330 updated
= UTC
.localize(self
.updated
)
332 "published": published
.isoformat(),
333 "updated": updated
.isoformat(),
334 "objectType": self
.object_type
,
342 user
.update({"summary": self
.bio
})
344 user
.update({"url": self
.url
})
346 user
.update({"location": self
.get_location
.serialize(request
)})
350 def unserialize(self
, data
):
351 if "summary" in data
:
352 self
.bio
= data
["summary"]
354 if "location" in data
:
355 Location
.create(data
, self
)
357 class LocalUser(User
):
358 """ This represents a user registered on this instance """
359 __tablename__
= "core__local_users"
361 id = Column(Integer
, ForeignKey("core__users.id"), primary_key
=True)
362 username
= Column(Unicode
, nullable
=False, unique
=True)
363 # Note: no db uniqueness constraint on email because it's not
364 # reliable (many email systems case insensitive despite against
365 # the RFC) and because it would be a mess to implement at this
367 email
= Column(Unicode
, nullable
=False)
368 pw_hash
= Column(Unicode
)
370 # Intented to be nullable=False, but migrations would not work for it
371 # set to nullable=True implicitly.
372 wants_comment_notification
= Column(Boolean
, default
=True)
373 wants_notifications
= Column(Boolean
, default
=True)
374 license_preference
= Column(Unicode
)
375 uploaded
= Column(Integer
, default
=0)
376 upload_limit
= Column(Integer
)
379 "polymorphic_identity": "user_local",
383 # plugin data would be in a separate model
386 return '<{0} #{1} {2} {3} "{4}">'.format(
387 self
.__class
__.__name
__,
389 'verified' if self
.has_privilege(u
'active') else 'non-verified',
390 'admin' if self
.has_privilege(u
'admin') else 'user',
393 def get_public_id(self
, host
):
394 return "acct:{0}@{1}".format(self
.username
, host
)
396 def serialize(self
, request
):
398 "id": self
.get_public_id(request
.host
),
399 "preferredUsername": self
.username
,
400 "displayName": self
.get_public_id(request
.host
).split(":", 1)[1],
403 "href": request
.urlgen(
404 "mediagoblin.api.user.profile",
405 username
=self
.username
,
410 "href": request
.urlgen(
411 "mediagoblin.api.inbox",
412 username
=self
.username
,
417 "href": request
.urlgen(
418 "mediagoblin.api.feed",
419 username
=self
.username
,
426 user
.update(super(LocalUser
, self
).serialize(request
))
429 class RemoteUser(User
):
430 """ User that is on another (remote) instance """
431 __tablename__
= "core__remote_users"
433 id = Column(Integer
, ForeignKey("core__users.id"), primary_key
=True)
434 webfinger
= Column(Unicode
, unique
=True)
437 'polymorphic_identity': 'user_remote'
441 return "<{0} #{1} {2}>".format(
442 self
.__class
__.__name
__,
450 Model representing a client - Used for API Auth
452 __tablename__
= "core__clients"
454 id = Column(Unicode
, nullable
=True, primary_key
=True)
455 secret
= Column(Unicode
, nullable
=False)
456 expirey
= Column(DateTime
, nullable
=True)
457 application_type
= Column(Unicode
, nullable
=False)
458 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
459 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
462 redirect_uri
= Column(JSONEncoded
, nullable
=True)
463 logo_url
= Column(Unicode
, nullable
=True)
464 application_name
= Column(Unicode
, nullable
=True)
465 contacts
= Column(JSONEncoded
, nullable
=True)
468 if self
.application_name
:
469 return "<Client {0} - {1}>".format(self
.application_name
, self
.id)
471 return "<Client {0}>".format(self
.id)
473 class RequestToken(Base
):
475 Model for representing the request tokens
477 __tablename__
= "core__request_tokens"
479 token
= Column(Unicode
, primary_key
=True)
480 secret
= Column(Unicode
, nullable
=False)
481 client
= Column(Unicode
, ForeignKey(Client
.id))
482 actor
= Column(Integer
, ForeignKey(User
.id), nullable
=True)
483 used
= Column(Boolean
, default
=False)
484 authenticated
= Column(Boolean
, default
=False)
485 verifier
= Column(Unicode
, nullable
=True)
486 callback
= Column(Unicode
, nullable
=False, default
=u
"oob")
487 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
488 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
490 get_client
= relationship(Client
)
492 class AccessToken(Base
):
494 Model for representing the access tokens
496 __tablename__
= "core__access_tokens"
498 token
= Column(Unicode
, nullable
=False, primary_key
=True)
499 secret
= Column(Unicode
, nullable
=False)
500 actor
= Column(Integer
, ForeignKey(User
.id))
501 request_token
= Column(Unicode
, ForeignKey(RequestToken
.token
))
502 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
503 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
505 get_requesttoken
= relationship(RequestToken
)
508 class NonceTimestamp(Base
):
510 A place the timestamp and nonce can be stored - this is for OAuth1
512 __tablename__
= "core__nonce_timestamps"
514 nonce
= Column(Unicode
, nullable
=False, primary_key
=True)
515 timestamp
= Column(DateTime
, nullable
=False, primary_key
=True)
517 class MediaEntry(Base
, MediaEntryMixin
, CommentingMixin
):
519 TODO: Consider fetching the media_files using join
521 __tablename__
= "core__media_entries"
523 id = Column(Integer
, primary_key
=True)
524 public_id
= Column(Unicode
, unique
=True, nullable
=True)
525 remote
= Column(Boolean
, default
=False)
527 actor
= Column(Integer
, ForeignKey(User
.id), nullable
=False, index
=True)
528 title
= Column(Unicode
, nullable
=False)
529 slug
= Column(Unicode
)
530 description
= Column(UnicodeText
) # ??
531 media_type
= Column(Unicode
, nullable
=False)
532 state
= Column(Unicode
, default
=u
'unprocessed', nullable
=False)
533 # or use sqlalchemy.types.Enum?
534 license
= Column(Unicode
)
535 file_size
= Column(Integer
, default
=0)
536 location
= Column(Integer
, ForeignKey("core__locations.id"))
537 get_location
= relationship("Location", lazy
="joined")
539 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
,
541 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
543 fail_error
= Column(Unicode
)
544 fail_metadata
= Column(JSONEncoded
)
546 transcoding_progress
= Column(Float
, default
=0)
547 main_transcoding_progress
= Column(Float
, default
=0)
549 queued_media_file
= Column(PathTupleWithSlashes
)
551 queued_task_id
= Column(Unicode
)
554 UniqueConstraint('actor', 'slug'),
557 deletion_mode
= Base
.SOFT_DELETE
559 get_actor
= relationship(User
)
561 media_files_helper
= relationship("MediaFile",
562 collection_class
=attribute_mapped_collection("name"),
563 cascade
="all, delete-orphan"
565 media_files
= association_proxy('media_files_helper', 'file_path',
566 creator
=lambda k
, v
: MediaFile(name
=k
, file_path
=v
)
569 attachment_files_helper
= relationship("MediaAttachmentFile",
570 cascade
="all, delete-orphan",
571 order_by
="MediaAttachmentFile.created"
573 attachment_files
= association_proxy("attachment_files_helper", "dict_view",
574 creator
=lambda v
: MediaAttachmentFile(
575 name
=v
["name"], filepath
=v
["filepath"])
578 subtitle_files_helper
= relationship("MediaSubtitleFile",
579 cascade
="all, delete-orphan",
580 order_by
="MediaSubtitleFile.created"
582 subtitle_files
= association_proxy("subtitle_files_helper", "dict_view",
583 creator
=lambda v
: MediaSubtitleFile(
584 name
=v
["name"], filepath
=v
["filepath"])
587 tags_helper
= relationship("MediaTag",
588 cascade
="all, delete-orphan" # should be automatically deleted
590 tags
= association_proxy("tags_helper", "dict_view",
591 creator
=lambda v
: MediaTag(name
=v
["name"], slug
=v
["slug"])
594 media_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
),
595 default
=MutationDict())
601 def get_uploader(self
):
603 return self
.get_actor
611 def collections(self
):
612 """ Get any collections that this MediaEntry is in """
613 return list(Collection
.query
.join(Collection
.collection_items
).join(
614 CollectionItem
.object_helper
617 GenericModelReference
.model_type
== self
.__tablename
__,
618 GenericModelReference
.obj_pk
== self
.id
622 def get_comments(self
, ascending
=False):
623 query
= Comment
.query
.join(Comment
.target_helper
).filter(and_(
624 GenericModelReference
.obj_pk
== self
.id,
625 GenericModelReference
.model_type
== self
.__tablename
__
629 query
= query
.order_by(Comment
.added
.asc())
631 query
= query
.order_by(Comment
.added
.desc())
635 def url_to_prev(self
, urlgen
):
636 """get the next 'newer' entry by this user"""
637 media
= MediaEntry
.query
.filter(
638 (MediaEntry
.actor
== self
.actor
)
639 & (MediaEntry
.state
== u
'processed')
640 & (MediaEntry
.id > self
.id)).order_by(MediaEntry
.id).first()
642 if media
is not None:
643 return media
.url_for_self(urlgen
)
645 def url_to_next(self
, urlgen
):
646 """get the next 'older' entry by this user"""
647 media
= MediaEntry
.query
.filter(
648 (MediaEntry
.actor
== self
.actor
)
649 & (MediaEntry
.state
== u
'processed')
650 & (MediaEntry
.id < self
.id)).order_by(desc(MediaEntry
.id)).first()
652 if media
is not None:
653 return media
.url_for_self(urlgen
)
655 def get_file_metadata(self
, file_key
, metadata_key
=None):
657 Return the file_metadata dict of a MediaFile. If metadata_key is given,
658 return the value of the key.
660 media_file
= MediaFile
.query
.filter_by(media_entry
=self
.id,
661 name
=six
.text_type(file_key
)).first()
665 return media_file
.file_metadata
.get(metadata_key
, None)
667 return media_file
.file_metadata
669 def set_file_metadata(self
, file_key
, **kwargs
):
671 Update the file_metadata of a MediaFile.
673 media_file
= MediaFile
.query
.filter_by(media_entry
=self
.id,
674 name
=six
.text_type(file_key
)).first()
676 file_metadata
= media_file
.file_metadata
or {}
678 for key
, value
in six
.iteritems(kwargs
):
679 file_metadata
[key
] = value
681 media_file
.file_metadata
= file_metadata
685 def media_data(self
):
686 return getattr(self
, self
.media_data_ref
)
688 def media_data_init(self
, **kwargs
):
690 Initialize or update the contents of a media entry's media_data row
692 media_data
= self
.media_data
694 if media_data
is None:
695 # Get the correct table:
696 table
= import_component(self
.media_type
+ '.models:DATA_MODEL')
697 # No media data, so actually add a new one
698 media_data
= table(**kwargs
)
699 # Get the relationship set up.
700 media_data
.get_media_entry
= self
702 # Update old media data
703 for field
, value
in six
.iteritems(kwargs
):
704 setattr(media_data
, field
, value
)
707 def media_data_ref(self
):
708 return import_component(self
.media_type
+ '.models:BACKREF_NAME')
712 # obj.__repr__() should return a str on Python 2
713 safe_title
= self
.title
.encode('utf-8', 'replace')
715 safe_title
= self
.title
717 return '<{classname} {id}: {title}>'.format(
718 classname
=self
.__class
__.__name
__,
722 def soft_delete(self
, *args
, **kwargs
):
723 # Find all of the media comments for this and delete them
724 for comment
in self
.get_comments():
725 comment
.delete(*args
, **kwargs
)
727 super(MediaEntry
, self
).soft_delete(*args
, **kwargs
)
729 def delete(self
, del_orphan_tags
=True, **kwargs
):
730 """Delete MediaEntry and all related files/attachments/comments
732 This will *not* automatically delete unused collections, which
735 :param del_orphan_tags: True/false if we delete unused Tags too
736 :param commit: True/False if this should end the db transaction"""
737 # User's CollectionItems are automatically deleted via "cascade".
738 # Comments on this Media are deleted by cascade, hopefully.
740 # Delete all related files/attachments
742 delete_media_files(self
)
743 except OSError as error
:
744 # Returns list of files we failed to delete
745 _log
.error('No such files from the user "{1}" to delete: '
746 '{0}'.format(str(error
), self
.get_actor
))
747 _log
.info('Deleted Media entry id "{0}"'.format(self
.id))
748 # Related MediaTag's are automatically cleaned, but we might
749 # want to clean out unused Tag's too.
751 # TODO: Import here due to cyclic imports!!!
752 # This cries for refactoring
753 from mediagoblin
.db
.util
import clean_orphan_tags
754 clean_orphan_tags(commit
=False)
755 # pass through commit=False/True in kwargs
756 super(MediaEntry
, self
).delete(**kwargs
)
758 def serialize(self
, request
, show_comments
=True):
759 """ Unserialize MediaEntry to object """
760 author
= self
.get_actor
761 published
= UTC
.localize(self
.created
)
762 updated
= UTC
.localize(self
.updated
)
763 public_id
= self
.get_public_id(request
.urlgen
)
766 "author": author
.serialize(request
),
767 "objectType": self
.object_type
,
768 "url": self
.url_for_self(request
.urlgen
, qualified
=True),
770 "url": urljoin(request
.host_url
, self
.thumb_url
),
773 "url": urljoin(request
.host_url
, self
.original_url
),
775 "published": published
.isoformat(),
776 "updated": updated
.isoformat(),
788 context
["displayName"] = self
.title
791 context
["content"] = self
.description
794 context
["license"] = self
.license
797 context
["location"] = self
.get_location
.serialize(request
)
799 # Always show tags, even if empty list
801 context
["tags"] = [tag
['name'] for tag
in self
.tags
]
807 l
.comment().serialize(request
) for l
in self
.get_comments()]
808 total
= len(comments
)
809 context
["replies"] = {
812 "url": request
.urlgen(
813 "mediagoblin.api.object.comments",
814 object_type
=self
.object_type
,
820 # Add image height and width if possible. We didn't use to store this
821 # data and we're not able (and maybe not willing) to re-process all
822 # images so it's possible this might not exist.
823 if self
.get_file_metadata("thumb", "height"):
824 height
= self
.get_file_metadata("thumb", "height")
825 context
["image"]["height"] = height
826 if self
.get_file_metadata("thumb", "width"):
827 width
= self
.get_file_metadata("thumb", "width")
828 context
["image"]["width"] = width
829 if self
.get_file_metadata("original", "height"):
830 height
= self
.get_file_metadata("original", "height")
831 context
["fullImage"]["height"] = height
832 if self
.get_file_metadata("original", "height"):
833 width
= self
.get_file_metadata("original", "width")
834 context
["fullImage"]["width"] = width
838 def unserialize(self
, data
):
839 """ Takes API objects and unserializes on existing MediaEntry """
840 if "displayName" in data
:
841 self
.title
= data
["displayName"]
843 if "content" in data
:
844 self
.description
= data
["content"]
846 if "license" in data
:
847 self
.license
= data
["license"]
849 if "location" in data
:
850 License
.create(data
["location"], self
)
853 self
.tags
= convert_to_tag_list_of_dicts(', '.join(data
["tags"]))
857 class FileKeynames(Base
):
859 keywords for various places.
860 currently the MediaFile keys
862 __tablename__
= "core__file_keynames"
863 id = Column(Integer
, primary_key
=True)
864 name
= Column(Unicode
, unique
=True)
867 return "<FileKeyname %r: %r>" % (self
.id, self
.name
)
870 def find_or_new(cls
, name
):
871 t
= cls
.query
.filter_by(name
=name
).first()
874 return cls(name
=name
)
877 class MediaFile(Base
):
879 TODO: Highly consider moving "name" into a new table.
880 TODO: Consider preloading said table in software
882 __tablename__
= "core__mediafiles"
884 media_entry
= Column(
885 Integer
, ForeignKey(MediaEntry
.id),
887 name_id
= Column(SmallInteger
, ForeignKey(FileKeynames
.id), nullable
=False)
888 file_path
= Column(PathTupleWithSlashes
)
889 file_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
))
892 PrimaryKeyConstraint('media_entry', 'name_id'),
896 return "<MediaFile %s: %r>" % (self
.name
, self
.file_path
)
898 name_helper
= relationship(FileKeynames
, lazy
="joined", innerjoin
=True)
899 name
= association_proxy('name_helper', 'name',
900 creator
=FileKeynames
.find_or_new
904 class MediaAttachmentFile(Base
):
905 __tablename__
= "core__attachment_files"
907 id = Column(Integer
, primary_key
=True)
908 media_entry
= Column(
909 Integer
, ForeignKey(MediaEntry
.id),
911 name
= Column(Unicode
, nullable
=False)
912 filepath
= Column(PathTupleWithSlashes
)
913 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
917 """A dict like view on this object"""
918 return DictReadAttrProxy(self
)
920 class MediaSubtitleFile(Base
):
921 __tablename__
= "core__subtitle_files"
923 id = Column(Integer
, primary_key
=True)
924 media_entry
= Column(
925 Integer
, ForeignKey(MediaEntry
.id),
927 name
= Column(Unicode
, nullable
=False)
928 filepath
= Column(PathTupleWithSlashes
)
929 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
933 """A dict like view on this object"""
934 return DictReadAttrProxy(self
)
938 __tablename__
= "core__tags"
940 id = Column(Integer
, primary_key
=True)
941 slug
= Column(Unicode
, nullable
=False, unique
=True)
944 return "<Tag %r: %r>" % (self
.id, self
.slug
)
947 def find_or_new(cls
, slug
):
948 t
= cls
.query
.filter_by(slug
=slug
).first()
951 return cls(slug
=slug
)
954 class MediaTag(Base
):
955 __tablename__
= "core__media_tags"
957 id = Column(Integer
, primary_key
=True)
958 media_entry
= Column(
959 Integer
, ForeignKey(MediaEntry
.id),
960 nullable
=False, index
=True)
961 tag
= Column(Integer
, ForeignKey(Tag
.id), nullable
=False, index
=True)
962 name
= Column(Unicode
)
963 # created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
966 UniqueConstraint('tag', 'media_entry'),
969 tag_helper
= relationship(Tag
)
970 slug
= association_proxy('tag_helper', 'slug',
971 creator
=Tag
.find_or_new
974 def __init__(self
, name
=None, slug
=None):
979 self
.tag_helper
= Tag
.find_or_new(slug
)
983 """A dict like view on this object"""
984 return DictReadAttrProxy(self
)
988 Link table between a response and another object that can have replies.
990 This acts as a link table between an object and the comments on it, it's
991 done like this so that you can look up all the comments without knowing
992 whhich comments are on an object before hand. Any object can be a comment
993 and more or less any object can accept comments too.
995 Important: This is NOT the old MediaComment table.
997 __tablename__
= "core__comment_links"
999 id = Column(Integer
, primary_key
=True)
1001 # The GMR to the object the comment is on.
1004 ForeignKey(GenericModelReference
.id),
1007 target_helper
= relationship(
1008 GenericModelReference
,
1009 foreign_keys
=[target_id
]
1011 target
= association_proxy("target_helper", "get_object",
1012 creator
=GenericModelReference
.find_or_new
)
1014 # The comment object
1015 comment_id
= Column(
1017 ForeignKey(GenericModelReference
.id),
1020 comment_helper
= relationship(
1021 GenericModelReference
,
1022 foreign_keys
=[comment_id
]
1024 comment
= association_proxy("comment_helper", "get_object",
1025 creator
=GenericModelReference
.find_or_new
)
1028 added
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1031 def get_author(self
):
1033 return self
.comment().get_actor
# noqa
1035 def __getattr__(self
, attr
):
1036 if attr
.startswith('_'):
1037 # if attr starts with '_', then it's probably some internal
1038 # sqlalchemy variable. Since __getattr__ is called when
1039 # non-existing attributes are being accessed, we should not try to
1040 # fetch it from self.comment()
1041 raise AttributeError
1043 _log
.debug('Old attr is being accessed: {0}'.format(attr
))
1044 return getattr(self
.comment(), attr
) # noqa
1045 except Exception as e
:
1049 class TextComment(Base
, TextCommentMixin
, CommentingMixin
):
1051 A basic text comment, this is a usually short amount of text and nothing else
1053 # This is a legacy from when Comments where just on MediaEntry objects.
1054 __tablename__
= "core__media_comments"
1056 id = Column(Integer
, primary_key
=True)
1057 public_id
= Column(Unicode
, unique
=True)
1058 actor
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
1059 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1060 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1061 content
= Column(UnicodeText
, nullable
=False)
1062 location
= Column(Integer
, ForeignKey("core__locations.id"))
1063 get_location
= relationship("Location", lazy
="joined")
1065 # Cascade: Comments are owned by their creator. So do the full thing.
1066 # lazy=dynamic: People might post a *lot* of comments,
1067 # so make the "posted_comments" a query-like thing.
1068 get_actor
= relationship(User
,
1069 backref
=backref("posted_comments",
1071 cascade
="all, delete-orphan"))
1072 deletion_mode
= Base
.SOFT_DELETE
1074 def serialize(self
, request
):
1075 """ Unserialize to python dictionary for API """
1076 target
= self
.get_reply_to()
1077 # If this is target just.. give them nothing?
1081 target
= target
.serialize(request
, show_comments
=False)
1084 author
= self
.get_actor
1085 published
= UTC
.localize(self
.created
)
1087 "id": self
.get_public_id(request
.urlgen
),
1088 "objectType": self
.object_type
,
1089 "content": self
.content
,
1090 "inReplyTo": target
,
1091 "author": author
.serialize(request
),
1092 "published": published
.isoformat(),
1093 "updated": published
.isoformat(),
1097 context
["location"] = self
.get_location
.seralize(request
)
1101 def unserialize(self
, data
, request
):
1102 """ Takes API objects and unserializes on existing comment """
1103 if "content" in data
:
1104 self
.content
= data
["content"]
1106 if "location" in data
:
1107 Location
.create(data
["location"], self
)
1110 # Handle changing the reply ID
1111 if "inReplyTo" in data
:
1112 # Validate that the ID is correct
1114 id = extract_url_arguments(
1115 url
=data
["inReplyTo"]["id"],
1116 urlmap
=request
.app
.url_map
1121 public_id
= request
.urlgen(
1122 "mediagoblin.api.object",
1124 object_type
=data
["inReplyTo"]["objectType"],
1128 media
= MediaEntry
.query
.filter_by(public_id
=public_id
).first()
1132 # We need an ID for this model.
1133 self
.save(commit
=False)
1143 class Collection(Base
, CollectionMixin
, CommentingMixin
):
1144 """A representation of a collection of objects.
1146 This holds a group/collection of objects that could be a user defined album
1147 or their inbox, outbox, followers, etc. These are always ordered and accessable
1148 via the API and web.
1150 The collection has a number of types which determine what kind of collection
1151 it is, for example the users inbox will be of `Collection.INBOX_TYPE` that will
1152 be stored on the `Collection.type` field. It's important to set the correct type.
1154 On deletion, contained CollectionItems get automatically reaped via
1156 __tablename__
= "core__collections"
1158 id = Column(Integer
, primary_key
=True)
1159 public_id
= Column(Unicode
, unique
=True)
1160 title
= Column(Unicode
, nullable
=False)
1161 slug
= Column(Unicode
)
1162 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
,
1164 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1165 description
= Column(UnicodeText
)
1166 actor
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
1167 num_items
= Column(Integer
, default
=0)
1169 # There are lots of different special types of collections in the pump.io API
1170 # for example: followers, following, inbox, outbox, etc. See type constants
1171 # below the fields on this model.
1172 type = Column(Unicode
, nullable
=False)
1175 location
= Column(Integer
, ForeignKey("core__locations.id"))
1176 get_location
= relationship("Location", lazy
="joined")
1178 # Cascade: Collections are owned by their creator. So do the full thing.
1179 get_actor
= relationship(User
,
1180 backref
=backref("collections",
1181 cascade
="all, delete-orphan"))
1183 UniqueConstraint("actor", "slug"),
1186 deletion_mode
= Base
.SOFT_DELETE
1188 # These are the types, It's strongly suggested if new ones are invented they
1189 # are prefixed to ensure they're unique from other types. Any types used in
1190 # the main mediagoblin should be prefixed "core-"
1191 INBOX_TYPE
= "core-inbox"
1192 OUTBOX_TYPE
= "core-outbox"
1193 FOLLOWER_TYPE
= "core-followers"
1194 FOLLOWING_TYPE
= "core-following"
1195 COMMENT_TYPE
= "core-comments"
1196 USER_DEFINED_TYPE
= "core-user-defined"
1198 def get_collection_items(self
, ascending
=False):
1199 #TODO, is this still needed with self.collection_items being available?
1200 order_col
= CollectionItem
.position
1202 order_col
= desc(order_col
)
1203 return CollectionItem
.query
.filter_by(
1204 collection
=self
.id).order_by(order_col
)
1207 safe_title
= self
.title
.encode('ascii', 'replace')
1208 return '<{classname} #{id}: {title} by {actor}>'.format(
1210 classname
=self
.__class
__.__name
__,
1214 def serialize(self
, request
):
1215 # Get all serialized output in a list
1216 items
= [i
.serialize(request
) for i
in self
.get_collection_items()]
1218 "totalItems": self
.num_items
,
1219 "url": self
.url_for_self(request
.urlgen
, qualified
=True),
1224 class CollectionItem(Base
, CollectionItemMixin
):
1225 __tablename__
= "core__collection_items"
1227 id = Column(Integer
, primary_key
=True)
1229 collection
= Column(Integer
, ForeignKey(Collection
.id), nullable
=False)
1230 note
= Column(UnicodeText
, nullable
=True)
1231 added
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1232 position
= Column(Integer
)
1233 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
1234 in_collection
= relationship(Collection
,
1237 cascade
="all, delete-orphan"))
1239 # Link to the object (could be anything.
1242 ForeignKey(GenericModelReference
.id),
1246 object_helper
= relationship(
1247 GenericModelReference
,
1248 foreign_keys
=[object_id
]
1250 get_object
= association_proxy(
1253 creator
=GenericModelReference
.find_or_new
1257 UniqueConstraint('collection', 'object_id'),
1261 def dict_view(self
):
1262 """A dict like view on this object"""
1263 return DictReadAttrProxy(self
)
1266 return '<{classname} #{id}: Object {obj} in {collection}>'.format(
1268 classname
=self
.__class
__.__name
__,
1269 collection
=self
.collection
,
1270 obj
=self
.get_object()
1273 def serialize(self
, request
):
1274 return self
.get_object().serialize(request
)
1277 class ProcessingMetaData(Base
):
1278 __tablename__
= 'core__processing_metadata'
1280 id = Column(Integer
, primary_key
=True)
1281 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False,
1283 media_entry
= relationship(MediaEntry
,
1284 backref
=backref('processing_metadata',
1285 cascade
='all, delete-orphan'))
1286 callback_url
= Column(Unicode
)
1289 def dict_view(self
):
1290 """A dict like view on this object"""
1291 return DictReadAttrProxy(self
)
1294 class CommentSubscription(Base
):
1295 __tablename__
= 'core__comment_subscriptions'
1296 id = Column(Integer
, primary_key
=True)
1298 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1300 media_entry_id
= Column(Integer
, ForeignKey(MediaEntry
.id), nullable
=False)
1301 media_entry
= relationship(MediaEntry
,
1302 backref
=backref('comment_subscriptions',
1303 cascade
='all, delete-orphan'))
1305 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
1306 user
= relationship(User
,
1307 backref
=backref('comment_subscriptions',
1308 cascade
='all, delete-orphan'))
1310 notify
= Column(Boolean
, nullable
=False, default
=True)
1311 send_email
= Column(Boolean
, nullable
=False, default
=True)
1314 return ('<{classname} #{id}: {user} {media} notify: '
1315 '{notify} email: {email}>').format(
1317 classname
=self
.__class
__.__name
__,
1319 media
=self
.media_entry
,
1321 email
=self
.send_email
)
1324 class Notification(Base
):
1325 __tablename__
= 'core__notifications'
1326 id = Column(Integer
, primary_key
=True)
1328 object_id
= Column(Integer
, ForeignKey(GenericModelReference
.id))
1329 object_helper
= relationship(GenericModelReference
)
1330 obj
= association_proxy("object_helper", "get_object",
1331 creator
=GenericModelReference
.find_or_new
)
1333 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1334 user_id
= Column(Integer
, ForeignKey('core__users.id'), nullable
=False,
1336 seen
= Column(Boolean
, default
=lambda: False, index
=True)
1337 user
= relationship(
1339 backref
=backref('notifications', cascade
='all, delete-orphan'))
1342 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1344 klass
=self
.__class
__.__name
__,
1346 subject
=getattr(self
, 'subject', None),
1347 seen
='unseen' if not self
.seen
else 'seen')
1349 def __unicode__(self
):
1350 return u
'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1352 klass
=self
.__class
__.__name
__,
1354 subject
=getattr(self
, 'subject', None),
1355 seen
='unseen' if not self
.seen
else 'seen')
1359 Represents a report that someone might file against Media, Comments, etc.
1361 :keyword reporter_id Holds the id of the user who created
1362 the report, as an Integer column.
1363 :keyword report_content Hold the explanation left by the repor-
1364 -ter to indicate why they filed the
1365 report in the first place, as a
1367 :keyword reported_user_id Holds the id of the user who created
1368 the content which was reported, as
1370 :keyword created Holds a datetime column of when the re-
1372 :keyword resolver_id Holds the id of the moderator/admin who
1373 resolved the report.
1374 :keyword resolved Holds the DateTime object which descri-
1375 -bes when this report was resolved
1376 :keyword result Holds the UnicodeText column of the
1377 resolver's reasons for resolving
1378 the report this way. Some of this
1380 :keyword object_id Holds the ID of the GenericModelReference
1381 which points to the reported object.
1383 __tablename__
= 'core__reports'
1385 id = Column(Integer
, primary_key
=True)
1386 reporter_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
1387 reporter
= relationship(
1389 backref
=backref("reports_filed_by",
1391 cascade
="all, delete-orphan"),
1392 primaryjoin
="User.id==Report.reporter_id")
1393 report_content
= Column(UnicodeText
)
1394 reported_user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False)
1395 reported_user
= relationship(
1397 backref
=backref("reports_filed_on",
1399 cascade
="all, delete-orphan"),
1400 primaryjoin
="User.id==Report.reported_user_id")
1401 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1402 resolver_id
= Column(Integer
, ForeignKey(User
.id))
1403 resolver
= relationship(
1405 backref
=backref("reports_resolved_by",
1407 cascade
="all, delete-orphan"),
1408 primaryjoin
="User.id==Report.resolver_id")
1410 resolved
= Column(DateTime
)
1411 result
= Column(UnicodeText
)
1413 object_id
= Column(Integer
, ForeignKey(GenericModelReference
.id), nullable
=True)
1414 object_helper
= relationship(GenericModelReference
)
1415 obj
= association_proxy("object_helper", "get_object",
1416 creator
=GenericModelReference
.find_or_new
)
1418 def is_archived_report(self
):
1419 return self
.resolved
is not None
1421 def is_comment_report(self
):
1422 if self
.object_id
is None:
1424 return isinstance(self
.obj(), TextComment
)
1426 def is_media_entry_report(self
):
1427 if self
.object_id
is None:
1429 return isinstance(self
.obj(), MediaEntry
)
1431 def archive(self
,resolver_id
, resolved
, result
):
1432 self
.resolver_id
= resolver_id
1433 self
.resolved
= resolved
1434 self
.result
= result
1436 class UserBan(Base
):
1438 Holds the information on a specific user's ban-state. As long as one of
1439 these is attached to a user, they are banned from accessing mediagoblin.
1440 When they try to log in, they are greeted with a page that tells them
1441 the reason why they are banned and when (if ever) the ban will be
1444 :keyword user_id Holds the id of the user this object is
1445 attached to. This is a one-to-one
1447 :keyword expiration_date Holds the date that the ban will be lifted.
1448 If this is null, the ban is permanent
1449 unless a moderator manually lifts it.
1450 :keyword reason Holds the reason why the user was banned.
1452 __tablename__
= 'core__user_bans'
1454 user_id
= Column(Integer
, ForeignKey(User
.id), nullable
=False,
1456 expiration_date
= Column(Date
)
1457 reason
= Column(UnicodeText
, nullable
=False)
1460 class Privilege(Base
):
1462 The Privilege table holds all of the different privileges a user can hold.
1463 If a user 'has' a privilege, the User object is in a relationship with the
1466 :keyword privilege_name Holds a unicode object that is the recognizable
1467 name of this privilege. This is the column
1468 used for identifying whether or not a user
1469 has a necessary privilege or not.
1472 __tablename__
= 'core__privileges'
1474 id = Column(Integer
, nullable
=False, primary_key
=True)
1475 privilege_name
= Column(Unicode
, nullable
=False, unique
=True)
1476 all_users
= relationship(
1478 backref
='all_privileges',
1479 secondary
="core__privileges_users")
1481 def __init__(self
, privilege_name
):
1483 Currently consructors are required for tables that are initialized thru
1484 the FOUNDATIONS system. This is because they need to be able to be con-
1485 -structed by a list object holding their arg*s
1487 self
.privilege_name
= privilege_name
1490 return "<Privilege %s>" % (self
.privilege_name
)
1493 class PrivilegeUserAssociation(Base
):
1495 This table holds the many-to-many relationship between User and Privilege
1498 __tablename__
= 'core__privileges_users'
1503 ForeignKey(User
.id),
1508 ForeignKey(Privilege
.id),
1511 class Generator(Base
):
1512 """ Information about what created an activity """
1513 __tablename__
= "core__generators"
1515 id = Column(Integer
, primary_key
=True)
1516 name
= Column(Unicode
, nullable
=False)
1517 published
= Column(DateTime
, default
=datetime
.datetime
.utcnow
)
1518 updated
= Column(DateTime
, default
=datetime
.datetime
.utcnow
)
1519 object_type
= Column(Unicode
, nullable
=False)
1521 deletion_mode
= Base
.SOFT_DELETE
1524 return "<{klass} {name}>".format(
1525 klass
=self
.__class
__.__name
__,
1529 def serialize(self
, request
):
1530 href
= request
.urlgen(
1531 "mediagoblin.api.object",
1532 object_type
=self
.object_type
,
1536 published
= UTC
.localize(self
.published
)
1537 updated
= UTC
.localize(self
.updated
)
1540 "displayName": self
.name
,
1541 "published": published
.isoformat(),
1542 "updated": updated
.isoformat(),
1543 "objectType": self
.object_type
,
1546 def unserialize(self
, data
):
1547 if "displayName" in data
:
1548 self
.name
= data
["displayName"]
1550 class Activity(Base
, ActivityMixin
):
1552 This holds all the metadata about an activity such as uploading an image,
1553 posting a comment, etc.
1555 __tablename__
= "core__activities"
1557 id = Column(Integer
, primary_key
=True)
1558 public_id
= Column(Unicode
, unique
=True)
1559 actor
= Column(Integer
,
1560 ForeignKey("core__users.id"),
1562 published
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1563 updated
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1565 verb
= Column(Unicode
, nullable
=False)
1566 content
= Column(Unicode
, nullable
=True)
1567 title
= Column(Unicode
, nullable
=True)
1568 generator
= Column(Integer
,
1569 ForeignKey("core__generators.id"),
1572 # Create the generic foreign keys for the object
1573 object_id
= Column(Integer
, ForeignKey(GenericModelReference
.id), nullable
=False)
1574 object_helper
= relationship(GenericModelReference
, foreign_keys
=[object_id
])
1575 object = association_proxy("object_helper", "get_object",
1576 creator
=GenericModelReference
.find_or_new
)
1578 # Create the generic foreign Key for the target
1579 target_id
= Column(Integer
, ForeignKey(GenericModelReference
.id), nullable
=True)
1580 target_helper
= relationship(GenericModelReference
, foreign_keys
=[target_id
])
1581 target
= association_proxy("target_helper", "get_object",
1582 creator
=GenericModelReference
.find_or_new
)
1584 get_actor
= relationship(User
,
1585 backref
=backref("activities",
1586 cascade
="all, delete-orphan"))
1587 get_generator
= relationship(Generator
)
1589 deletion_mode
= Base
.SOFT_DELETE
1592 if self
.content
is None:
1593 return "<{klass} verb:{verb}>".format(
1594 klass
=self
.__class
__.__name
__,
1598 return "<{klass} {content}>".format(
1599 klass
=self
.__class
__.__name
__,
1600 content
=self
.content
1603 def save(self
, set_updated
=True, *args
, **kwargs
):
1605 self
.updated
= datetime
.datetime
.now()
1606 super(Activity
, self
).save(*args
, **kwargs
)
1608 class Graveyard(Base
):
1609 """ Where models come to die """
1610 __tablename__
= "core__graveyard"
1612 id = Column(Integer
, primary_key
=True)
1613 public_id
= Column(Unicode
, nullable
=True, unique
=True)
1615 deleted
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
1616 object_type
= Column(Unicode
, nullable
=False)
1618 # This could either be a deleted actor or a real actor, this must be
1619 # nullable as it we shouldn't have it set for deleted actor
1620 actor_id
= Column(Integer
, ForeignKey(GenericModelReference
.id))
1621 actor_helper
= relationship(GenericModelReference
)
1622 actor
= association_proxy("actor_helper", "get_object",
1623 creator
=GenericModelReference
.find_or_new
)
1626 return "<{klass} deleted {obj_type}>".format(
1627 klass
=type(self
).__name
__,
1628 obj_type
=self
.object_type
1631 def serialize(self
, request
):
1632 deleted
= UTC
.localize(self
.deleted
).isoformat()
1634 "id": self
.public_id
,
1635 "objectType": self
.object_type
,
1636 "published": deleted
,
1641 if self
.actor_id
is not None:
1642 context
["actor"] = self
.actor().serialize(request
)
1646 LocalUser
, RemoteUser
, User
, MediaEntry
, Tag
, MediaTag
, Comment
, TextComment
,
1647 Collection
, CollectionItem
, MediaFile
, FileKeynames
, MediaAttachmentFile
, MediaSubtitleFile
,
1648 ProcessingMetaData
, Notification
, Client
, CommentSubscription
, Report
,
1649 UserBan
, Privilege
, PrivilegeUserAssociation
, RequestToken
, AccessToken
,
1650 NonceTimestamp
, Activity
, Generator
, Location
, GenericModelReference
, Graveyard
]
1653 Foundations are the default rows that are created immediately after the tables
1654 are initialized. Each entry to this dictionary should be in the format of:
1655 ModelConstructorObject:List of Dictionaries
1656 (Each Dictionary represents a row on the Table to be created, containing each
1657 of the columns' names as a key string, and each of the columns' values as a
1660 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1661 user_foundations = [{'name':u'Joanna', 'age':24},
1662 {'name':u'Andrea', 'age':41}]
1664 FOUNDATIONS = {User:user_foundations}
1666 privilege_foundations
= [{'privilege_name':u
'admin'},
1667 {'privilege_name':u
'moderator'},
1668 {'privilege_name':u
'uploader'},
1669 {'privilege_name':u
'reporter'},
1670 {'privilege_name':u
'commenter'},
1671 {'privilege_name':u
'active'}]
1672 FOUNDATIONS
= {Privilege
:privilege_foundations
}
1674 ######################################################
1675 # Special, migrations-tracking table
1677 # Not listed in MODELS because this is special and not
1678 # really migrated, but used for migrations (for now)
1679 ######################################################
1681 class MigrationData(Base
):
1682 __tablename__
= "core__migrations"
1684 name
= Column(Unicode
, primary_key
=True)
1685 version
= Column(Integer
, nullable
=False, default
=0)
1687 ######################################################
1690 def show_table_init(engine_uri
):
1691 if engine_uri
is None:
1692 engine_uri
= 'sqlite:///:memory:'
1693 from sqlalchemy
import create_engine
1694 engine
= create_engine(engine_uri
, echo
=True)
1696 Base
.metadata
.create_all(engine
)
1699 if __name__
== '__main__':
1700 from sys
import argv
1706 show_table_init(uri
)