trac#687: Add unit tests for `redirect` and `redirect_obj`.
[larjonas-mediagoblin.git] / mediagoblin / db / models.py
blob67659552f9eeb90559f9cf59166c5ab2775c3f87
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/>.
17 """
18 TODO: indexes on foreignkeys, where useful.
19 """
21 from __future__ import print_function
23 import logging
24 import datetime
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, \
30 class_mapper
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,
38 MutationDict)
39 from mediagoblin.db.base import Base, DictReadAttrProxy, FakeCursor
40 from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
41 CollectionMixin, CollectionItemMixin, ActivityMixin, TextCommentMixin, \
42 CommentingMixin
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
47 import six
48 from pytz import UTC
50 _log = logging.getLogger(__name__)
52 class GenericModelReference(Base):
53 """
54 Represents a relationship to any model that is defined with a integer pk
55 """
56 __tablename__ = "core__generic_model_reference"
58 id = Column(Integer, primary_key=True)
59 obj_pk = Column(Integer, nullable=False)
61 # This will be the tablename of the model
62 model_type = Column(Unicode, nullable=False)
64 # Constrain it so obj_pk and model_type have to be unique
65 # They should be this order as the index is generated, "model_type" will be
66 # the major order as it's put first.
67 __table_args__ = (
68 UniqueConstraint("model_type", "obj_pk"),
69 {})
71 def get_object(self):
72 # This can happen if it's yet to be saved
73 if self.model_type is None or self.obj_pk is None:
74 return None
76 model = self._get_model_from_type(self.model_type)
77 return model.query.filter_by(id=self.obj_pk).first()
79 def set_object(self, obj):
80 model = obj.__class__
82 # Check we've been given a object
83 if not issubclass(model, Base):
84 raise ValueError("Only models can be set as using the GMR")
86 # Check that the model has an explicit __tablename__ declaration
87 if getattr(model, "__tablename__", None) is None:
88 raise ValueError("Models must have __tablename__ attribute")
90 # Check that it's not a composite primary key
91 primary_keys = [key.name for key in class_mapper(model).primary_key]
92 if len(primary_keys) > 1:
93 raise ValueError("Models can not have composite primary keys")
95 # Check that the field on the model is a an integer field
96 pk_column = getattr(model, primary_keys[0])
97 if not isinstance(pk_column.type, Integer):
98 raise ValueError("Only models with integer pks can be set")
100 if getattr(obj, pk_column.key) is None:
101 obj.save(commit=False)
103 self.obj_pk = getattr(obj, pk_column.key)
104 self.model_type = obj.__tablename__
106 def _get_model_from_type(self, model_type):
107 """ Gets a model from a tablename (model type) """
108 if getattr(type(self), "_TYPE_MAP", None) is None:
109 # We want to build on the class (not the instance) a map of all the
110 # models by the table name (type) for easy lookup, this is done on
111 # the class so it can be shared between all instances
113 # to prevent circular imports do import here
114 registry = dict(Base._decl_class_registry).values()
115 self._TYPE_MAP = dict(
116 ((m.__tablename__, m) for m in registry if hasattr(m, "__tablename__"))
118 setattr(type(self), "_TYPE_MAP", self._TYPE_MAP)
120 return self.__class__._TYPE_MAP[model_type]
122 @classmethod
123 def find_for_obj(cls, obj):
124 """ Finds a GMR for an object or returns None """
125 # Is there one for this already.
126 model = type(obj)
127 pk = getattr(obj, "id")
129 gmr = cls.query.filter_by(
130 obj_pk=pk,
131 model_type=model.__tablename__
134 return gmr.first()
136 @classmethod
137 def find_or_new(cls, obj):
138 """ Finds an existing GMR or creates a new one for the object """
139 gmr = cls.find_for_obj(obj)
141 # If there isn't one already create one
142 if gmr is None:
143 gmr = cls(
144 obj_pk=obj.id,
145 model_type=type(obj).__tablename__
148 return gmr
150 class Location(Base):
151 """ Represents a physical location """
152 __tablename__ = "core__locations"
154 id = Column(Integer, primary_key=True)
155 name = Column(Unicode)
157 # GPS coordinates
158 position = Column(MutationDict.as_mutable(JSONEncoded))
159 address = Column(MutationDict.as_mutable(JSONEncoded))
161 @classmethod
162 def create(cls, data, obj):
163 location = cls()
164 location.unserialize(data)
165 location.save()
166 obj.location = location.id
167 return location
169 def serialize(self, request):
170 location = {"objectType": "place"}
172 if self.name is not None:
173 location["displayName"] = self.name
175 if self.position:
176 location["position"] = self.position
178 if self.address:
179 location["address"] = self.address
181 return location
183 def unserialize(self, data):
184 if "displayName" in data:
185 self.name = data["displayName"]
187 self.position = {}
188 self.address = {}
190 # nicer way to do this?
191 if "position" in data:
192 # TODO: deal with ISO 9709 formatted string as position
193 if "altitude" in data["position"]:
194 self.position["altitude"] = data["position"]["altitude"]
196 if "direction" in data["position"]:
197 self.position["direction"] = data["position"]["direction"]
199 if "longitude" in data["position"]:
200 self.position["longitude"] = data["position"]["longitude"]
202 if "latitude" in data["position"]:
203 self.position["latitude"] = data["position"]["latitude"]
205 if "address" in data:
206 if "formatted" in data["address"]:
207 self.address["formatted"] = data["address"]["formatted"]
209 if "streetAddress" in data["address"]:
210 self.address["streetAddress"] = data["address"]["streetAddress"]
212 if "locality" in data["address"]:
213 self.address["locality"] = data["address"]["locality"]
215 if "region" in data["address"]:
216 self.address["region"] = data["address"]["region"]
218 if "postalCode" in data["address"]:
219 self.address["postalCode"] = data["addresss"]["postalCode"]
221 if "country" in data["address"]:
222 self.address["country"] = data["address"]["country"]
224 class User(Base, UserMixin):
226 Base user that is common amongst LocalUser and RemoteUser.
228 This holds all the fields which are common between both the Local and Remote
229 user models.
231 NB: ForeignKeys should reference this User model and NOT the LocalUser or
232 RemoteUser models.
234 __tablename__ = "core__users"
236 id = Column(Integer, primary_key=True)
237 url = Column(Unicode)
238 bio = Column(UnicodeText)
239 name = Column(Unicode)
241 # This is required for the polymorphic inheritance
242 type = Column(Unicode)
244 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
245 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
247 location = Column(Integer, ForeignKey("core__locations.id"))
249 # Lazy getters
250 get_location = relationship("Location", lazy="joined")
252 __mapper_args__ = {
253 'polymorphic_identity': 'user',
254 'polymorphic_on': type,
257 deletion_mode = Base.SOFT_DELETE
259 def soft_delete(self, *args, **kwargs):
260 # Find all the Collections and delete those
261 for collection in Collection.query.filter_by(actor=self.id):
262 collection.delete(**kwargs)
264 # Find all the comments and delete those too
265 for comment in TextComment.query.filter_by(actor=self.id):
266 comment.delete(**kwargs)
268 # Find all the activities and delete those too
269 for activity in Activity.query.filter_by(actor=self.id):
270 activity.delete(**kwargs)
272 super(User, self).soft_delete(*args, **kwargs)
275 def delete(self, *args, **kwargs):
276 """Deletes a User and all related entries/comments/files/..."""
277 # Collections get deleted by relationships.
279 media_entries = MediaEntry.query.filter(MediaEntry.actor == 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 username = self.username
292 super(User, self).delete(*args, **kwargs)
293 _log.info('Deleted user "{0}" account'.format(username))
295 def has_privilege(self, privilege, allow_admin=True):
297 This method checks to make sure a user has all the correct privileges
298 to access a piece of content.
300 :param privilege A unicode object which represent the different
301 privileges which may give the user access to
302 content.
304 :param allow_admin If this is set to True the then if the user is
305 an admin, then this will always return True
306 even if the user hasn't been given the
307 privilege. (defaults to True)
309 priv = Privilege.query.filter_by(privilege_name=privilege).one()
310 if priv in self.all_privileges:
311 return True
312 elif allow_admin and self.has_privilege(u'admin', allow_admin=False):
313 return True
315 return False
317 def is_banned(self):
319 Checks if this user is banned.
321 :returns True if self is banned
322 :returns False if self is not
324 return UserBan.query.get(self.id) is not None
326 def serialize(self, request):
327 published = UTC.localize(self.created)
328 updated = UTC.localize(self.updated)
329 user = {
330 "published": published.isoformat(),
331 "updated": updated.isoformat(),
332 "objectType": self.object_type,
333 "pump_io": {
334 "shared": False,
335 "followed": False,
339 if self.bio:
340 user.update({"summary": self.bio})
341 if self.url:
342 user.update({"url": self.url})
343 if self.location:
344 user.update({"location": self.get_location.serialize(request)})
346 return user
348 def unserialize(self, data):
349 if "summary" in data:
350 self.bio = data["summary"]
352 if "location" in data:
353 Location.create(data, self)
355 class LocalUser(User):
356 """ This represents a user registered on this instance """
357 __tablename__ = "core__local_users"
359 id = Column(Integer, ForeignKey("core__users.id"), primary_key=True)
360 username = Column(Unicode, nullable=False, unique=True)
361 # Note: no db uniqueness constraint on email because it's not
362 # reliable (many email systems case insensitive despite against
363 # the RFC) and because it would be a mess to implement at this
364 # point.
365 email = Column(Unicode, nullable=False)
366 pw_hash = Column(Unicode)
368 # Intented to be nullable=False, but migrations would not work for it
369 # set to nullable=True implicitly.
370 wants_comment_notification = Column(Boolean, default=True)
371 wants_notifications = Column(Boolean, default=True)
372 license_preference = Column(Unicode)
373 uploaded = Column(Integer, default=0)
374 upload_limit = Column(Integer)
376 __mapper_args__ = {
377 "polymorphic_identity": "user_local",
380 ## TODO
381 # plugin data would be in a separate model
383 def __repr__(self):
384 return '<{0} #{1} {2} {3} "{4}">'.format(
385 self.__class__.__name__,
386 self.id,
387 'verified' if self.has_privilege(u'active') else 'non-verified',
388 'admin' if self.has_privilege(u'admin') else 'user',
389 self.username)
391 def serialize(self, request):
392 user = {
393 "id": "acct:{0}@{1}".format(self.username, request.host),
394 "preferredUsername": self.username,
395 "displayName": "{0}@{1}".format(self.username, request.host),
396 "links": {
397 "self": {
398 "href": request.urlgen(
399 "mediagoblin.api.user.profile",
400 username=self.username,
401 qualified=True
404 "activity-inbox": {
405 "href": request.urlgen(
406 "mediagoblin.api.inbox",
407 username=self.username,
408 qualified=True
411 "activity-outbox": {
412 "href": request.urlgen(
413 "mediagoblin.api.feed",
414 username=self.username,
415 qualified=True
421 user.update(super(LocalUser, self).serialize(request))
422 return user
424 class RemoteUser(User):
425 """ User that is on another (remote) instance """
426 __tablename__ = "core__remote_users"
428 id = Column(Integer, ForeignKey("core__users.id"), primary_key=True)
429 webfinger = Column(Unicode, unique=True)
431 __mapper_args__ = {
432 'polymorphic_identity': 'user_remote'
435 def __repr__(self):
436 return "<{0} #{1} {2}>".format(
437 self.__class__.__name__,
438 self.id,
439 self.webfinger
443 class Client(Base):
445 Model representing a client - Used for API Auth
447 __tablename__ = "core__clients"
449 id = Column(Unicode, nullable=True, primary_key=True)
450 secret = Column(Unicode, nullable=False)
451 expirey = Column(DateTime, nullable=True)
452 application_type = Column(Unicode, nullable=False)
453 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
454 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
456 # optional stuff
457 redirect_uri = Column(JSONEncoded, nullable=True)
458 logo_url = Column(Unicode, nullable=True)
459 application_name = Column(Unicode, nullable=True)
460 contacts = Column(JSONEncoded, nullable=True)
462 def __repr__(self):
463 if self.application_name:
464 return "<Client {0} - {1}>".format(self.application_name, self.id)
465 else:
466 return "<Client {0}>".format(self.id)
468 class RequestToken(Base):
470 Model for representing the request tokens
472 __tablename__ = "core__request_tokens"
474 token = Column(Unicode, primary_key=True)
475 secret = Column(Unicode, nullable=False)
476 client = Column(Unicode, ForeignKey(Client.id))
477 actor = Column(Integer, ForeignKey(User.id), nullable=True)
478 used = Column(Boolean, default=False)
479 authenticated = Column(Boolean, default=False)
480 verifier = Column(Unicode, nullable=True)
481 callback = Column(Unicode, nullable=False, default=u"oob")
482 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
483 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
485 get_client = relationship(Client)
487 class AccessToken(Base):
489 Model for representing the access tokens
491 __tablename__ = "core__access_tokens"
493 token = Column(Unicode, nullable=False, primary_key=True)
494 secret = Column(Unicode, nullable=False)
495 actor = Column(Integer, ForeignKey(User.id))
496 request_token = Column(Unicode, ForeignKey(RequestToken.token))
497 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
498 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
500 get_requesttoken = relationship(RequestToken)
503 class NonceTimestamp(Base):
505 A place the timestamp and nonce can be stored - this is for OAuth1
507 __tablename__ = "core__nonce_timestamps"
509 nonce = Column(Unicode, nullable=False, primary_key=True)
510 timestamp = Column(DateTime, nullable=False, primary_key=True)
512 class MediaEntry(Base, MediaEntryMixin, CommentingMixin):
514 TODO: Consider fetching the media_files using join
516 __tablename__ = "core__media_entries"
518 id = Column(Integer, primary_key=True)
519 public_id = Column(Unicode, unique=True, nullable=True)
520 remote = Column(Boolean, default=False)
522 actor = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
523 title = Column(Unicode, nullable=False)
524 slug = Column(Unicode)
525 description = Column(UnicodeText) # ??
526 media_type = Column(Unicode, nullable=False)
527 state = Column(Unicode, default=u'unprocessed', nullable=False)
528 # or use sqlalchemy.types.Enum?
529 license = Column(Unicode)
530 file_size = Column(Integer, default=0)
531 location = Column(Integer, ForeignKey("core__locations.id"))
532 get_location = relationship("Location", lazy="joined")
534 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow,
535 index=True)
536 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
538 fail_error = Column(Unicode)
539 fail_metadata = Column(JSONEncoded)
541 transcoding_progress = Column(SmallInteger)
543 queued_media_file = Column(PathTupleWithSlashes)
545 queued_task_id = Column(Unicode)
547 __table_args__ = (
548 UniqueConstraint('actor', 'slug'),
551 deletion_mode = Base.SOFT_DELETE
553 get_actor = relationship(User)
555 media_files_helper = relationship("MediaFile",
556 collection_class=attribute_mapped_collection("name"),
557 cascade="all, delete-orphan"
559 media_files = association_proxy('media_files_helper', 'file_path',
560 creator=lambda k, v: MediaFile(name=k, file_path=v)
563 attachment_files_helper = relationship("MediaAttachmentFile",
564 cascade="all, delete-orphan",
565 order_by="MediaAttachmentFile.created"
567 attachment_files = association_proxy("attachment_files_helper", "dict_view",
568 creator=lambda v: MediaAttachmentFile(
569 name=v["name"], filepath=v["filepath"])
572 tags_helper = relationship("MediaTag",
573 cascade="all, delete-orphan" # should be automatically deleted
575 tags = association_proxy("tags_helper", "dict_view",
576 creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
579 media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
580 default=MutationDict())
582 ## TODO
583 # fail_error
585 @property
586 def collections(self):
587 """ Get any collections that this MediaEntry is in """
588 return list(Collection.query.join(Collection.collection_items).join(
589 CollectionItem.object_helper
590 ).filter(
591 and_(
592 GenericModelReference.model_type == self.__tablename__,
593 GenericModelReference.obj_pk == self.id
597 def get_comments(self, ascending=False):
598 query = Comment.query.join(Comment.target_helper).filter(and_(
599 GenericModelReference.obj_pk == self.id,
600 GenericModelReference.model_type == self.__tablename__
603 if ascending:
604 query = query.order_by(Comment.added.asc())
605 else:
606 qury = query.order_by(Comment.added.desc())
608 return FakeCursor(query, lambda c:c.comment())
610 def url_to_prev(self, urlgen):
611 """get the next 'newer' entry by this user"""
612 media = MediaEntry.query.filter(
613 (MediaEntry.actor == self.actor)
614 & (MediaEntry.state == u'processed')
615 & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()
617 if media is not None:
618 return media.url_for_self(urlgen)
620 def url_to_next(self, urlgen):
621 """get the next 'older' entry by this user"""
622 media = MediaEntry.query.filter(
623 (MediaEntry.actor == self.actor)
624 & (MediaEntry.state == u'processed')
625 & (MediaEntry.id < self.id)).order_by(desc(MediaEntry.id)).first()
627 if media is not None:
628 return media.url_for_self(urlgen)
630 def get_file_metadata(self, file_key, metadata_key=None):
632 Return the file_metadata dict of a MediaFile. If metadata_key is given,
633 return the value of the key.
635 media_file = MediaFile.query.filter_by(media_entry=self.id,
636 name=six.text_type(file_key)).first()
638 if media_file:
639 if metadata_key:
640 return media_file.file_metadata.get(metadata_key, None)
642 return media_file.file_metadata
644 def set_file_metadata(self, file_key, **kwargs):
646 Update the file_metadata of a MediaFile.
648 media_file = MediaFile.query.filter_by(media_entry=self.id,
649 name=six.text_type(file_key)).first()
651 file_metadata = media_file.file_metadata or {}
653 for key, value in six.iteritems(kwargs):
654 file_metadata[key] = value
656 media_file.file_metadata = file_metadata
657 media_file.save()
659 @property
660 def media_data(self):
661 return getattr(self, self.media_data_ref)
663 def media_data_init(self, **kwargs):
665 Initialize or update the contents of a media entry's media_data row
667 media_data = self.media_data
669 if media_data is None:
670 # Get the correct table:
671 table = import_component(self.media_type + '.models:DATA_MODEL')
672 # No media data, so actually add a new one
673 media_data = table(**kwargs)
674 # Get the relationship set up.
675 media_data.get_media_entry = self
676 else:
677 # Update old media data
678 for field, value in six.iteritems(kwargs):
679 setattr(media_data, field, value)
681 @memoized_property
682 def media_data_ref(self):
683 return import_component(self.media_type + '.models:BACKREF_NAME')
685 def __repr__(self):
686 if six.PY2:
687 # obj.__repr__() should return a str on Python 2
688 safe_title = self.title.encode('utf-8', 'replace')
689 else:
690 safe_title = self.title
692 return '<{classname} {id}: {title}>'.format(
693 classname=self.__class__.__name__,
694 id=self.id,
695 title=safe_title)
697 def soft_delete(self, *args, **kwargs):
698 # Find all of the media comments for this and delete them
699 for comment in self.get_comments():
700 comment.delete(*args, **kwargs)
702 super(MediaEntry, self).soft_delete(*args, **kwargs)
704 def delete(self, del_orphan_tags=True, **kwargs):
705 """Delete MediaEntry and all related files/attachments/comments
707 This will *not* automatically delete unused collections, which
708 can remain empty...
710 :param del_orphan_tags: True/false if we delete unused Tags too
711 :param commit: True/False if this should end the db transaction"""
712 # User's CollectionItems are automatically deleted via "cascade".
713 # Comments on this Media are deleted by cascade, hopefully.
715 # Delete all related files/attachments
716 try:
717 delete_media_files(self)
718 except OSError as error:
719 # Returns list of files we failed to delete
720 _log.error('No such files from the user "{1}" to delete: '
721 '{0}'.format(str(error), self.get_actor))
722 _log.info('Deleted Media entry id "{0}"'.format(self.id))
723 # Related MediaTag's are automatically cleaned, but we might
724 # want to clean out unused Tag's too.
725 if del_orphan_tags:
726 # TODO: Import here due to cyclic imports!!!
727 # This cries for refactoring
728 from mediagoblin.db.util import clean_orphan_tags
729 clean_orphan_tags(commit=False)
730 # pass through commit=False/True in kwargs
731 super(MediaEntry, self).delete(**kwargs)
733 def serialize(self, request, show_comments=True):
734 """ Unserialize MediaEntry to object """
735 author = self.get_actor
736 published = UTC.localize(self.created)
737 updated = UTC.localize(self.updated)
738 public_id = self.get_public_id(request.urlgen)
739 context = {
740 "id": public_id,
741 "author": author.serialize(request),
742 "objectType": self.object_type,
743 "url": self.url_for_self(request.urlgen, qualified=True),
744 "image": {
745 "url": request.host_url + self.thumb_url[1:],
747 "fullImage":{
748 "url": request.host_url + self.original_url[1:],
750 "published": published.isoformat(),
751 "updated": updated.isoformat(),
752 "pump_io": {
753 "shared": False,
755 "links": {
756 "self": {
757 "href": public_id,
763 if self.title:
764 context["displayName"] = self.title
766 if self.description:
767 context["content"] = self.description
769 if self.license:
770 context["license"] = self.license
772 if self.location:
773 context["location"] = self.get_location.serialize(request)
775 if show_comments:
776 comments = [
777 comment.serialize(request) for comment in self.get_comments()]
778 total = len(comments)
779 context["replies"] = {
780 "totalItems": total,
781 "items": comments,
782 "url": request.urlgen(
783 "mediagoblin.api.object.comments",
784 object_type=self.object_type,
785 id=self.id,
786 qualified=True
790 # Add image height and width if possible. We didn't use to store this
791 # data and we're not able (and maybe not willing) to re-process all
792 # images so it's possible this might not exist.
793 if self.get_file_metadata("thumb", "height"):
794 height = self.get_file_metadata("thumb", "height")
795 context["image"]["height"] = height
796 if self.get_file_metadata("thumb", "width"):
797 width = self.get_file_metadata("thumb", "width")
798 context["image"]["width"] = width
799 if self.get_file_metadata("original", "height"):
800 height = self.get_file_metadata("original", "height")
801 context["fullImage"]["height"] = height
802 if self.get_file_metadata("original", "height"):
803 width = self.get_file_metadata("original", "width")
804 context["fullImage"]["width"] = width
806 return context
808 def unserialize(self, data):
809 """ Takes API objects and unserializes on existing MediaEntry """
810 if "displayName" in data:
811 self.title = data["displayName"]
813 if "content" in data:
814 self.description = data["content"]
816 if "license" in data:
817 self.license = data["license"]
819 if "location" in data:
820 License.create(data["location"], self)
822 return True
824 class FileKeynames(Base):
826 keywords for various places.
827 currently the MediaFile keys
829 __tablename__ = "core__file_keynames"
830 id = Column(Integer, primary_key=True)
831 name = Column(Unicode, unique=True)
833 def __repr__(self):
834 return "<FileKeyname %r: %r>" % (self.id, self.name)
836 @classmethod
837 def find_or_new(cls, name):
838 t = cls.query.filter_by(name=name).first()
839 if t is not None:
840 return t
841 return cls(name=name)
844 class MediaFile(Base):
846 TODO: Highly consider moving "name" into a new table.
847 TODO: Consider preloading said table in software
849 __tablename__ = "core__mediafiles"
851 media_entry = Column(
852 Integer, ForeignKey(MediaEntry.id),
853 nullable=False)
854 name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
855 file_path = Column(PathTupleWithSlashes)
856 file_metadata = Column(MutationDict.as_mutable(JSONEncoded))
858 __table_args__ = (
859 PrimaryKeyConstraint('media_entry', 'name_id'),
862 def __repr__(self):
863 return "<MediaFile %s: %r>" % (self.name, self.file_path)
865 name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
866 name = association_proxy('name_helper', 'name',
867 creator=FileKeynames.find_or_new
871 class MediaAttachmentFile(Base):
872 __tablename__ = "core__attachment_files"
874 id = Column(Integer, primary_key=True)
875 media_entry = Column(
876 Integer, ForeignKey(MediaEntry.id),
877 nullable=False)
878 name = Column(Unicode, nullable=False)
879 filepath = Column(PathTupleWithSlashes)
880 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
882 @property
883 def dict_view(self):
884 """A dict like view on this object"""
885 return DictReadAttrProxy(self)
888 class Tag(Base):
889 __tablename__ = "core__tags"
891 id = Column(Integer, primary_key=True)
892 slug = Column(Unicode, nullable=False, unique=True)
894 def __repr__(self):
895 return "<Tag %r: %r>" % (self.id, self.slug)
897 @classmethod
898 def find_or_new(cls, slug):
899 t = cls.query.filter_by(slug=slug).first()
900 if t is not None:
901 return t
902 return cls(slug=slug)
905 class MediaTag(Base):
906 __tablename__ = "core__media_tags"
908 id = Column(Integer, primary_key=True)
909 media_entry = Column(
910 Integer, ForeignKey(MediaEntry.id),
911 nullable=False, index=True)
912 tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
913 name = Column(Unicode)
914 # created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
916 __table_args__ = (
917 UniqueConstraint('tag', 'media_entry'),
920 tag_helper = relationship(Tag)
921 slug = association_proxy('tag_helper', 'slug',
922 creator=Tag.find_or_new
925 def __init__(self, name=None, slug=None):
926 Base.__init__(self)
927 if name is not None:
928 self.name = name
929 if slug is not None:
930 self.tag_helper = Tag.find_or_new(slug)
932 @property
933 def dict_view(self):
934 """A dict like view on this object"""
935 return DictReadAttrProxy(self)
937 class Comment(Base):
939 Link table between a response and another object that can have replies.
941 This acts as a link table between an object and the comments on it, it's
942 done like this so that you can look up all the comments without knowing
943 whhich comments are on an object before hand. Any object can be a comment
944 and more or less any object can accept comments too.
946 Important: This is NOT the old MediaComment table.
948 __tablename__ = "core__comment_links"
950 id = Column(Integer, primary_key=True)
952 # The GMR to the object the comment is on.
953 target_id = Column(
954 Integer,
955 ForeignKey(GenericModelReference.id),
956 nullable=False
958 target_helper = relationship(
959 GenericModelReference,
960 foreign_keys=[target_id]
962 target = association_proxy("target_helper", "get_object",
963 creator=GenericModelReference.find_or_new)
965 # The comment object
966 comment_id = Column(
967 Integer,
968 ForeignKey(GenericModelReference.id),
969 nullable=False
971 comment_helper = relationship(
972 GenericModelReference,
973 foreign_keys=[comment_id]
975 comment = association_proxy("comment_helper", "get_object",
976 creator=GenericModelReference.find_or_new)
978 # When it was added
979 added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
982 class TextComment(Base, TextCommentMixin, CommentingMixin):
984 A basic text comment, this is a usually short amount of text and nothing else
986 # This is a legacy from when Comments where just on MediaEntry objects.
987 __tablename__ = "core__media_comments"
989 id = Column(Integer, primary_key=True)
990 public_id = Column(Unicode, unique=True)
991 actor = Column(Integer, ForeignKey(User.id), nullable=False)
992 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
993 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
994 content = Column(UnicodeText, nullable=False)
995 location = Column(Integer, ForeignKey("core__locations.id"))
996 get_location = relationship("Location", lazy="joined")
998 # Cascade: Comments are owned by their creator. So do the full thing.
999 # lazy=dynamic: People might post a *lot* of comments,
1000 # so make the "posted_comments" a query-like thing.
1001 get_actor = relationship(User,
1002 backref=backref("posted_comments",
1003 lazy="dynamic",
1004 cascade="all, delete-orphan"))
1005 deletion_mode = Base.SOFT_DELETE
1007 def serialize(self, request):
1008 """ Unserialize to python dictionary for API """
1009 target = self.get_reply_to()
1010 # If this is target just.. give them nothing?
1011 if target is None:
1012 target = {}
1013 else:
1014 target = target.serialize(request, show_comments=False)
1017 author = self.get_actor
1018 published = UTC.localize(self.created)
1019 context = {
1020 "id": self.get_public_id(request.urlgen),
1021 "objectType": self.object_type,
1022 "content": self.content,
1023 "inReplyTo": target,
1024 "author": author.serialize(request),
1025 "published": published.isoformat(),
1026 "updated": published.isoformat(),
1029 if self.location:
1030 context["location"] = self.get_location.seralize(request)
1032 return context
1034 def unserialize(self, data, request):
1035 """ Takes API objects and unserializes on existing comment """
1036 if "content" in data:
1037 self.content = data["content"]
1039 if "location" in data:
1040 Location.create(data["location"], self)
1043 # Handle changing the reply ID
1044 if "inReplyTo" in data:
1045 # Validate that the ID is correct
1046 try:
1047 id = extract_url_arguments(
1048 url=data["inReplyTo"]["id"],
1049 urlmap=request.app.url_map
1050 )["id"]
1051 except ValueError:
1052 raise False
1054 public_id = request.urlgen(
1055 "mediagoblin.api.object",
1056 id=id,
1057 object_type=data["inReplyTo"]["objectType"],
1058 qualified=True
1061 media = MediaEntry.query.filter_by(public_id=public_id).first()
1062 if media is None:
1063 return False
1065 # We need an ID for this model.
1066 self.save(commit=False)
1068 # Create the link
1069 link = Comment()
1070 link.target = media
1071 link.comment = self
1072 link.save()
1074 return True
1076 class Collection(Base, CollectionMixin, CommentingMixin):
1077 """A representation of a collection of objects.
1079 This holds a group/collection of objects that could be a user defined album
1080 or their inbox, outbox, followers, etc. These are always ordered and accessable
1081 via the API and web.
1083 The collection has a number of types which determine what kind of collection
1084 it is, for example the users inbox will be of `Collection.INBOX_TYPE` that will
1085 be stored on the `Collection.type` field. It's important to set the correct type.
1087 On deletion, contained CollectionItems get automatically reaped via
1088 SQL cascade"""
1089 __tablename__ = "core__collections"
1091 id = Column(Integer, primary_key=True)
1092 public_id = Column(Unicode, unique=True)
1093 title = Column(Unicode, nullable=False)
1094 slug = Column(Unicode)
1095 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow,
1096 index=True)
1097 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1098 description = Column(UnicodeText)
1099 actor = Column(Integer, ForeignKey(User.id), nullable=False)
1100 num_items = Column(Integer, default=0)
1102 # There are lots of different special types of collections in the pump.io API
1103 # for example: followers, following, inbox, outbox, etc. See type constants
1104 # below the fields on this model.
1105 type = Column(Unicode, nullable=False)
1107 # Location
1108 location = Column(Integer, ForeignKey("core__locations.id"))
1109 get_location = relationship("Location", lazy="joined")
1111 # Cascade: Collections are owned by their creator. So do the full thing.
1112 get_actor = relationship(User,
1113 backref=backref("collections",
1114 cascade="all, delete-orphan"))
1115 __table_args__ = (
1116 UniqueConstraint("actor", "slug"),
1119 deletion_mode = Base.SOFT_DELETE
1121 # These are the types, It's strongly suggested if new ones are invented they
1122 # are prefixed to ensure they're unique from other types. Any types used in
1123 # the main mediagoblin should be prefixed "core-"
1124 INBOX_TYPE = "core-inbox"
1125 OUTBOX_TYPE = "core-outbox"
1126 FOLLOWER_TYPE = "core-followers"
1127 FOLLOWING_TYPE = "core-following"
1128 COMMENT_TYPE = "core-comments"
1129 USER_DEFINED_TYPE = "core-user-defined"
1131 def get_collection_items(self, ascending=False):
1132 #TODO, is this still needed with self.collection_items being available?
1133 order_col = CollectionItem.position
1134 if not ascending:
1135 order_col = desc(order_col)
1136 return CollectionItem.query.filter_by(
1137 collection=self.id).order_by(order_col)
1139 def __repr__(self):
1140 safe_title = self.title.encode('ascii', 'replace')
1141 return '<{classname} #{id}: {title} by {actor}>'.format(
1142 id=self.id,
1143 classname=self.__class__.__name__,
1144 actor=self.actor,
1145 title=safe_title)
1147 def serialize(self, request):
1148 # Get all serialized output in a list
1149 items = [i.serialize(request) for i in self.get_collection_items()]
1150 return {
1151 "totalItems": self.items,
1152 "url": self.url_for_self(request.urlgen, qualified=True),
1153 "items": items,
1157 class CollectionItem(Base, CollectionItemMixin):
1158 __tablename__ = "core__collection_items"
1160 id = Column(Integer, primary_key=True)
1162 collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
1163 note = Column(UnicodeText, nullable=True)
1164 added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1165 position = Column(Integer)
1166 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
1167 in_collection = relationship(Collection,
1168 backref=backref(
1169 "collection_items",
1170 cascade="all, delete-orphan"))
1172 # Link to the object (could be anything.
1173 object_id = Column(
1174 Integer,
1175 ForeignKey(GenericModelReference.id),
1176 nullable=False,
1177 index=True
1179 object_helper = relationship(
1180 GenericModelReference,
1181 foreign_keys=[object_id]
1183 get_object = association_proxy(
1184 "object_helper",
1185 "get_object",
1186 creator=GenericModelReference.find_or_new
1189 __table_args__ = (
1190 UniqueConstraint('collection', 'object_id'),
1193 @property
1194 def dict_view(self):
1195 """A dict like view on this object"""
1196 return DictReadAttrProxy(self)
1198 def __repr__(self):
1199 return '<{classname} #{id}: Object {obj} in {collection}>'.format(
1200 id=self.id,
1201 classname=self.__class__.__name__,
1202 collection=self.collection,
1203 obj=self.get_object()
1206 def serialize(self, request):
1207 return self.get_object().serialize(request)
1210 class ProcessingMetaData(Base):
1211 __tablename__ = 'core__processing_metadata'
1213 id = Column(Integer, primary_key=True)
1214 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
1215 index=True)
1216 media_entry = relationship(MediaEntry,
1217 backref=backref('processing_metadata',
1218 cascade='all, delete-orphan'))
1219 callback_url = Column(Unicode)
1221 @property
1222 def dict_view(self):
1223 """A dict like view on this object"""
1224 return DictReadAttrProxy(self)
1227 class CommentSubscription(Base):
1228 __tablename__ = 'core__comment_subscriptions'
1229 id = Column(Integer, primary_key=True)
1231 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1233 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
1234 media_entry = relationship(MediaEntry,
1235 backref=backref('comment_subscriptions',
1236 cascade='all, delete-orphan'))
1238 user_id = Column(Integer, ForeignKey(User.id), nullable=False)
1239 user = relationship(User,
1240 backref=backref('comment_subscriptions',
1241 cascade='all, delete-orphan'))
1243 notify = Column(Boolean, nullable=False, default=True)
1244 send_email = Column(Boolean, nullable=False, default=True)
1246 def __repr__(self):
1247 return ('<{classname} #{id}: {user} {media} notify: '
1248 '{notify} email: {email}>').format(
1249 id=self.id,
1250 classname=self.__class__.__name__,
1251 user=self.user,
1252 media=self.media_entry,
1253 notify=self.notify,
1254 email=self.send_email)
1257 class Notification(Base):
1258 __tablename__ = 'core__notifications'
1259 id = Column(Integer, primary_key=True)
1261 object_id = Column(Integer, ForeignKey(GenericModelReference.id))
1262 object_helper = relationship(GenericModelReference)
1263 obj = association_proxy("object_helper", "get_object",
1264 creator=GenericModelReference.find_or_new)
1266 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1267 user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False,
1268 index=True)
1269 seen = Column(Boolean, default=lambda: False, index=True)
1270 user = relationship(
1271 User,
1272 backref=backref('notifications', cascade='all, delete-orphan'))
1274 def __repr__(self):
1275 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1276 id=self.id,
1277 klass=self.__class__.__name__,
1278 user=self.user,
1279 subject=getattr(self, 'subject', None),
1280 seen='unseen' if not self.seen else 'seen')
1282 def __unicode__(self):
1283 return u'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1284 id=self.id,
1285 klass=self.__class__.__name__,
1286 user=self.user,
1287 subject=getattr(self, 'subject', None),
1288 seen='unseen' if not self.seen else 'seen')
1290 class Report(Base):
1292 Represents a report that someone might file against Media, Comments, etc.
1294 :keyword reporter_id Holds the id of the user who created
1295 the report, as an Integer column.
1296 :keyword report_content Hold the explanation left by the repor-
1297 -ter to indicate why they filed the
1298 report in the first place, as a
1299 Unicode column.
1300 :keyword reported_user_id Holds the id of the user who created
1301 the content which was reported, as
1302 an Integer column.
1303 :keyword created Holds a datetime column of when the re-
1304 -port was filed.
1305 :keyword resolver_id Holds the id of the moderator/admin who
1306 resolved the report.
1307 :keyword resolved Holds the DateTime object which descri-
1308 -bes when this report was resolved
1309 :keyword result Holds the UnicodeText column of the
1310 resolver's reasons for resolving
1311 the report this way. Some of this
1312 is auto-generated
1313 :keyword object_id Holds the ID of the GenericModelReference
1314 which points to the reported object.
1316 __tablename__ = 'core__reports'
1318 id = Column(Integer, primary_key=True)
1319 reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
1320 reporter = relationship(
1321 User,
1322 backref=backref("reports_filed_by",
1323 lazy="dynamic",
1324 cascade="all, delete-orphan"),
1325 primaryjoin="User.id==Report.reporter_id")
1326 report_content = Column(UnicodeText)
1327 reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
1328 reported_user = relationship(
1329 User,
1330 backref=backref("reports_filed_on",
1331 lazy="dynamic",
1332 cascade="all, delete-orphan"),
1333 primaryjoin="User.id==Report.reported_user_id")
1334 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1335 resolver_id = Column(Integer, ForeignKey(User.id))
1336 resolver = relationship(
1337 User,
1338 backref=backref("reports_resolved_by",
1339 lazy="dynamic",
1340 cascade="all, delete-orphan"),
1341 primaryjoin="User.id==Report.resolver_id")
1343 resolved = Column(DateTime)
1344 result = Column(UnicodeText)
1346 object_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=False)
1347 object_helper = relationship(GenericModelReference)
1348 obj = association_proxy("object_helper", "get_object",
1349 creator=GenericModelReference.find_or_new)
1351 def is_archived_report(self):
1352 return self.resolved is not None
1354 def is_comment_report(self):
1355 if self.object_id is None:
1356 return False
1357 return isinstance(self.obj(), TextComment)
1359 def is_media_entry_report(self):
1360 if self.object_id is None:
1361 return False
1362 return isinstance(self.obj(), MediaEntry)
1364 def archive(self,resolver_id, resolved, result):
1365 self.resolver_id = resolver_id
1366 self.resolved = resolved
1367 self.result = result
1369 class UserBan(Base):
1371 Holds the information on a specific user's ban-state. As long as one of
1372 these is attached to a user, they are banned from accessing mediagoblin.
1373 When they try to log in, they are greeted with a page that tells them
1374 the reason why they are banned and when (if ever) the ban will be
1375 lifted
1377 :keyword user_id Holds the id of the user this object is
1378 attached to. This is a one-to-one
1379 relationship.
1380 :keyword expiration_date Holds the date that the ban will be lifted.
1381 If this is null, the ban is permanent
1382 unless a moderator manually lifts it.
1383 :keyword reason Holds the reason why the user was banned.
1385 __tablename__ = 'core__user_bans'
1387 user_id = Column(Integer, ForeignKey(User.id), nullable=False,
1388 primary_key=True)
1389 expiration_date = Column(Date)
1390 reason = Column(UnicodeText, nullable=False)
1393 class Privilege(Base):
1395 The Privilege table holds all of the different privileges a user can hold.
1396 If a user 'has' a privilege, the User object is in a relationship with the
1397 privilege object.
1399 :keyword privilege_name Holds a unicode object that is the recognizable
1400 name of this privilege. This is the column
1401 used for identifying whether or not a user
1402 has a necessary privilege or not.
1405 __tablename__ = 'core__privileges'
1407 id = Column(Integer, nullable=False, primary_key=True)
1408 privilege_name = Column(Unicode, nullable=False, unique=True)
1409 all_users = relationship(
1410 User,
1411 backref='all_privileges',
1412 secondary="core__privileges_users")
1414 def __init__(self, privilege_name):
1416 Currently consructors are required for tables that are initialized thru
1417 the FOUNDATIONS system. This is because they need to be able to be con-
1418 -structed by a list object holding their arg*s
1420 self.privilege_name = privilege_name
1422 def __repr__(self):
1423 return "<Privilege %s>" % (self.privilege_name)
1426 class PrivilegeUserAssociation(Base):
1428 This table holds the many-to-many relationship between User and Privilege
1431 __tablename__ = 'core__privileges_users'
1433 user = Column(
1434 "user",
1435 Integer,
1436 ForeignKey(User.id),
1437 primary_key=True)
1438 privilege = Column(
1439 "privilege",
1440 Integer,
1441 ForeignKey(Privilege.id),
1442 primary_key=True)
1444 class Generator(Base):
1445 """ Information about what created an activity """
1446 __tablename__ = "core__generators"
1448 id = Column(Integer, primary_key=True)
1449 name = Column(Unicode, nullable=False)
1450 published = Column(DateTime, default=datetime.datetime.utcnow)
1451 updated = Column(DateTime, default=datetime.datetime.utcnow)
1452 object_type = Column(Unicode, nullable=False)
1454 deletion_mode = Base.SOFT_DELETE
1456 def __repr__(self):
1457 return "<{klass} {name}>".format(
1458 klass=self.__class__.__name__,
1459 name=self.name
1462 def serialize(self, request):
1463 href = request.urlgen(
1464 "mediagoblin.api.object",
1465 object_type=self.object_type,
1466 id=self.id,
1467 qualified=True
1469 published = UTC.localize(self.published)
1470 updated = UTC.localize(self.updated)
1471 return {
1472 "id": href,
1473 "displayName": self.name,
1474 "published": published.isoformat(),
1475 "updated": updated.isoformat(),
1476 "objectType": self.object_type,
1479 def unserialize(self, data):
1480 if "displayName" in data:
1481 self.name = data["displayName"]
1483 class Activity(Base, ActivityMixin):
1485 This holds all the metadata about an activity such as uploading an image,
1486 posting a comment, etc.
1488 __tablename__ = "core__activities"
1490 id = Column(Integer, primary_key=True)
1491 public_id = Column(Unicode, unique=True)
1492 actor = Column(Integer,
1493 ForeignKey("core__users.id"),
1494 nullable=False)
1495 published = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1496 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1498 verb = Column(Unicode, nullable=False)
1499 content = Column(Unicode, nullable=True)
1500 title = Column(Unicode, nullable=True)
1501 generator = Column(Integer,
1502 ForeignKey("core__generators.id"),
1503 nullable=True)
1505 # Create the generic foreign keys for the object
1506 object_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=False)
1507 object_helper = relationship(GenericModelReference, foreign_keys=[object_id])
1508 object = association_proxy("object_helper", "get_object",
1509 creator=GenericModelReference.find_or_new)
1511 # Create the generic foreign Key for the target
1512 target_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=True)
1513 target_helper = relationship(GenericModelReference, foreign_keys=[target_id])
1514 target = association_proxy("target_helper", "get_object",
1515 creator=GenericModelReference.find_or_new)
1517 get_actor = relationship(User,
1518 backref=backref("activities",
1519 cascade="all, delete-orphan"))
1520 get_generator = relationship(Generator)
1522 deletion_mode = Base.SOFT_DELETE
1524 def __repr__(self):
1525 if self.content is None:
1526 return "<{klass} verb:{verb}>".format(
1527 klass=self.__class__.__name__,
1528 verb=self.verb
1530 else:
1531 return "<{klass} {content}>".format(
1532 klass=self.__class__.__name__,
1533 content=self.content
1536 def save(self, set_updated=True, *args, **kwargs):
1537 if set_updated:
1538 self.updated = datetime.datetime.now()
1539 super(Activity, self).save(*args, **kwargs)
1541 class Graveyard(Base):
1542 """ Where models come to die """
1543 __tablename__ = "core__graveyard"
1545 id = Column(Integer, primary_key=True)
1546 public_id = Column(Unicode, nullable=True, unique=True)
1548 deleted = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1549 object_type = Column(Unicode, nullable=False)
1551 # This could either be a deleted actor or a real actor, this must be
1552 # nullable as it we shouldn't have it set for deleted actor
1553 actor_id = Column(Integer, ForeignKey(GenericModelReference.id))
1554 actor_helper = relationship(GenericModelReference)
1555 actor = association_proxy("actor_helper", "get_object",
1556 creator=GenericModelReference.find_or_new)
1558 def __repr__(self):
1559 return "<{klass} deleted {obj_type}>".format(
1560 klass=type(self).__name__,
1561 obj_type=self.object_type
1564 def serialize(self, request):
1565 return {
1566 "id": self.public_id,
1567 "objectType": self.object_type,
1568 "actor": self.actor(),
1569 "published": self.deleted,
1570 "updated": self.deleted,
1571 "deleted": self.deleted
1574 MODELS = [
1575 LocalUser, RemoteUser, User, MediaEntry, Tag, MediaTag, Comment, TextComment,
1576 Collection, CollectionItem, MediaFile, FileKeynames, MediaAttachmentFile,
1577 ProcessingMetaData, Notification, Client, CommentSubscription, Report,
1578 UserBan, Privilege, PrivilegeUserAssociation, RequestToken, AccessToken,
1579 NonceTimestamp, Activity, Generator, Location, GenericModelReference, Graveyard]
1582 Foundations are the default rows that are created immediately after the tables
1583 are initialized. Each entry to this dictionary should be in the format of:
1584 ModelConstructorObject:List of Dictionaries
1585 (Each Dictionary represents a row on the Table to be created, containing each
1586 of the columns' names as a key string, and each of the columns' values as a
1587 value)
1589 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1590 user_foundations = [{'name':u'Joanna', 'age':24},
1591 {'name':u'Andrea', 'age':41}]
1593 FOUNDATIONS = {User:user_foundations}
1595 privilege_foundations = [{'privilege_name':u'admin'},
1596 {'privilege_name':u'moderator'},
1597 {'privilege_name':u'uploader'},
1598 {'privilege_name':u'reporter'},
1599 {'privilege_name':u'commenter'},
1600 {'privilege_name':u'active'}]
1601 FOUNDATIONS = {Privilege:privilege_foundations}
1603 ######################################################
1604 # Special, migrations-tracking table
1606 # Not listed in MODELS because this is special and not
1607 # really migrated, but used for migrations (for now)
1608 ######################################################
1610 class MigrationData(Base):
1611 __tablename__ = "core__migrations"
1613 name = Column(Unicode, primary_key=True)
1614 version = Column(Integer, nullable=False, default=0)
1616 ######################################################
1619 def show_table_init(engine_uri):
1620 if engine_uri is None:
1621 engine_uri = 'sqlite:///:memory:'
1622 from sqlalchemy import create_engine
1623 engine = create_engine(engine_uri, echo=True)
1625 Base.metadata.create_all(engine)
1628 if __name__ == '__main__':
1629 from sys import argv
1630 print(repr(argv))
1631 if len(argv) == 2:
1632 uri = argv[1]
1633 else:
1634 uri = None
1635 show_table_init(uri)