Fix serialization after model changes
[larjonas-mediagoblin.git] / mediagoblin / db / models.py
blobf1449667a0a2f7b6d4c88e2a714fbfd55f683374
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.expression import desc
33 from sqlalchemy.ext.associationproxy import association_proxy
34 from sqlalchemy.util import memoized_property
36 from mediagoblin.db.extratypes import (PathTupleWithSlashes, JSONEncoded,
37 MutationDict)
38 from mediagoblin.db.base import Base, DictReadAttrProxy
39 from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
40 MediaCommentMixin, CollectionMixin, CollectionItemMixin, \
41 ActivityMixin
42 from mediagoblin.tools.files import delete_media_files
43 from mediagoblin.tools.common import import_component
44 from mediagoblin.tools.routing import extract_url_arguments
46 import six
47 from pytz import UTC
49 _log = logging.getLogger(__name__)
51 class GenericModelReference(Base):
52 """
53 Represents a relationship to any model that is defined with a integer pk
54 """
55 __tablename__ = "core__generic_model_reference"
57 id = Column(Integer, primary_key=True)
58 obj_pk = Column(Integer, nullable=False)
60 # This will be the tablename of the model
61 model_type = Column(Unicode, nullable=False)
63 # Constrain it so obj_pk and model_type have to be unique
64 # They should be this order as the index is generated, "model_type" will be
65 # the major order as it's put first.
66 __table_args__ = (
67 UniqueConstraint("model_type", "obj_pk"),
68 {})
70 def get_object(self):
71 # This can happen if it's yet to be saved
72 if self.model_type is None or self.obj_pk is None:
73 return None
75 model = self._get_model_from_type(self.model_type)
76 return model.query.filter_by(id=self.obj_pk).first()
78 def set_object(self, obj):
79 model = obj.__class__
81 # Check we've been given a object
82 if not issubclass(model, Base):
83 raise ValueError("Only models can be set as using the GMR")
85 # Check that the model has an explicit __tablename__ declaration
86 if getattr(model, "__tablename__", None) is None:
87 raise ValueError("Models must have __tablename__ attribute")
89 # Check that it's not a composite primary key
90 primary_keys = [key.name for key in class_mapper(model).primary_key]
91 if len(primary_keys) > 1:
92 raise ValueError("Models can not have composite primary keys")
94 # Check that the field on the model is a an integer field
95 pk_column = getattr(model, primary_keys[0])
96 if not isinstance(pk_column.type, Integer):
97 raise ValueError("Only models with integer pks can be set")
99 if getattr(obj, pk_column.key) is None:
100 obj.save(commit=False)
102 self.obj_pk = getattr(obj, pk_column.key)
103 self.model_type = obj.__tablename__
105 def _get_model_from_type(self, model_type):
106 """ Gets a model from a tablename (model type) """
107 if getattr(type(self), "_TYPE_MAP", None) is None:
108 # We want to build on the class (not the instance) a map of all the
109 # models by the table name (type) for easy lookup, this is done on
110 # the class so it can be shared between all instances
112 # to prevent circular imports do import here
113 registry = dict(Base._decl_class_registry).values()
114 self._TYPE_MAP = dict(
115 ((m.__tablename__, m) for m in registry if hasattr(m, "__tablename__"))
117 setattr(type(self), "_TYPE_MAP", self._TYPE_MAP)
119 return self.__class__._TYPE_MAP[model_type]
121 @classmethod
122 def find_for_obj(cls, obj):
123 """ Finds a GMR for an object or returns None """
124 # Is there one for this already.
125 model = type(obj)
126 pk = getattr(obj, "id")
128 gmr = cls.query.filter_by(
129 obj_pk=pk,
130 model_type=model.__tablename__
133 return gmr.first()
135 @classmethod
136 def find_or_new(cls, obj):
137 """ Finds an existing GMR or creates a new one for the object """
138 gmr = cls.find_for_obj(obj)
140 # If there isn't one already create one
141 if gmr is None:
142 gmr = cls(
143 obj_pk=obj.id,
144 model_type=type(obj).__tablename__
147 return gmr
149 class Location(Base):
150 """ Represents a physical location """
151 __tablename__ = "core__locations"
153 id = Column(Integer, primary_key=True)
154 name = Column(Unicode)
156 # GPS coordinates
157 position = Column(MutationDict.as_mutable(JSONEncoded))
158 address = Column(MutationDict.as_mutable(JSONEncoded))
160 @classmethod
161 def create(cls, data, obj):
162 location = cls()
163 location.unserialize(data)
164 location.save()
165 obj.location = location.id
166 return location
168 def serialize(self, request):
169 location = {"objectType": "place"}
171 if self.name is not None:
172 location["displayName"] = self.name
174 if self.position:
175 location["position"] = self.position
177 if self.address:
178 location["address"] = self.address
180 return location
182 def unserialize(self, data):
183 if "displayName" in data:
184 self.name = data["displayName"]
186 self.position = {}
187 self.address = {}
189 # nicer way to do this?
190 if "position" in data:
191 # TODO: deal with ISO 9709 formatted string as position
192 if "altitude" in data["position"]:
193 self.position["altitude"] = data["position"]["altitude"]
195 if "direction" in data["position"]:
196 self.position["direction"] = data["position"]["direction"]
198 if "longitude" in data["position"]:
199 self.position["longitude"] = data["position"]["longitude"]
201 if "latitude" in data["position"]:
202 self.position["latitude"] = data["position"]["latitude"]
204 if "address" in data:
205 if "formatted" in data["address"]:
206 self.address["formatted"] = data["address"]["formatted"]
208 if "streetAddress" in data["address"]:
209 self.address["streetAddress"] = data["address"]["streetAddress"]
211 if "locality" in data["address"]:
212 self.address["locality"] = data["address"]["locality"]
214 if "region" in data["address"]:
215 self.address["region"] = data["address"]["region"]
217 if "postalCode" in data["address"]:
218 self.address["postalCode"] = data["addresss"]["postalCode"]
220 if "country" in data["address"]:
221 self.address["country"] = data["address"]["country"]
223 class User(Base, UserMixin):
225 Base user that is common amongst LocalUser and RemoteUser.
227 This holds all the fields which are common between both the Local and Remote
228 user models.
230 NB: ForeignKeys should reference this User model and NOT the LocalUser or
231 RemoteUser models.
233 __tablename__ = "core__users"
235 id = Column(Integer, primary_key=True)
236 url = Column(Unicode)
237 bio = Column(UnicodeText)
238 name = Column(Unicode)
240 # This is required for the polymorphic inheritance
241 type = Column(Unicode)
243 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
244 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
246 location = Column(Integer, ForeignKey("core__locations.id"))
248 # Lazy getters
249 get_location = relationship("Location", lazy="joined")
251 __mapper_args__ = {
252 'polymorphic_identity': 'user',
253 'polymorphic_on': type
256 def has_privilege(self, privilege, allow_admin=True):
258 This method checks to make sure a user has all the correct privileges
259 to access a piece of content.
261 :param privilege A unicode object which represent the different
262 privileges which may give the user access to
263 content.
265 :param allow_admin If this is set to True the then if the user is
266 an admin, then this will always return True
267 even if the user hasn't been given the
268 privilege. (defaults to True)
270 priv = Privilege.query.filter_by(privilege_name=privilege).one()
271 if priv in self.all_privileges:
272 return True
273 elif allow_admin and self.has_privilege(u'admin', allow_admin=False):
274 return True
276 return False
278 def is_banned(self):
280 Checks if this user is banned.
282 :returns True if self is banned
283 :returns False if self is not
285 return UserBan.query.get(self.id) is not None
287 def serialize(self, request):
288 published = UTC.localize(self.created)
289 updated = UTC.localize(self.updated)
290 user = {
291 "published": published.isoformat(),
292 "updated": updated.isoformat(),
293 "objectType": self.object_type,
294 "pump_io": {
295 "shared": False,
296 "followed": False,
300 if self.bio:
301 user.update({"summary": self.bio})
302 if self.url:
303 user.update({"url": self.url})
304 if self.location:
305 user.update({"location": self.get_location.serialize(request)})
307 return user
309 def unserialize(self, data):
310 if "summary" in data:
311 self.bio = data["summary"]
313 if "location" in data:
314 Location.create(data, self)
316 class LocalUser(User):
317 """ This represents a user registered on this instance """
318 __tablename__ = "core__local_users"
320 id = Column(Integer, ForeignKey("core__users.id"), primary_key=True)
321 username = Column(Unicode, nullable=False, unique=True)
322 # Note: no db uniqueness constraint on email because it's not
323 # reliable (many email systems case insensitive despite against
324 # the RFC) and because it would be a mess to implement at this
325 # point.
326 email = Column(Unicode, nullable=False)
327 pw_hash = Column(Unicode)
329 # Intented to be nullable=False, but migrations would not work for it
330 # set to nullable=True implicitly.
331 wants_comment_notification = Column(Boolean, default=True)
332 wants_notifications = Column(Boolean, default=True)
333 license_preference = Column(Unicode)
334 uploaded = Column(Integer, default=0)
335 upload_limit = Column(Integer)
337 __mapper_args__ = {
338 'polymorphic_identity': 'user_local'
341 ## TODO
342 # plugin data would be in a separate model
344 def __repr__(self):
345 return '<{0} #{1} {2} {3} "{4}">'.format(
346 self.__class__.__name__,
347 self.id,
348 'verified' if self.has_privilege(u'active') else 'non-verified',
349 'admin' if self.has_privilege(u'admin') else 'user',
350 self.username)
352 def delete(self, **kwargs):
353 """Deletes a User and all related entries/comments/files/..."""
354 # Collections get deleted by relationships.
356 media_entries = MediaEntry.query.filter(MediaEntry.uploader == self.id)
357 for media in media_entries:
358 # TODO: Make sure that "MediaEntry.delete()" also deletes
359 # all related files/Comments
360 media.delete(del_orphan_tags=False, commit=False)
362 # Delete now unused tags
363 # TODO: import here due to cyclic imports!!! This cries for refactoring
364 from mediagoblin.db.util import clean_orphan_tags
365 clean_orphan_tags(commit=False)
367 # Delete user, pass through commit=False/True in kwargs
368 super(User, self).delete(**kwargs)
369 _log.info('Deleted user "{0}" account'.format(self.username))
371 def serialize(self, request):
372 user = {
373 "id": "acct:{0}@{1}".format(self.username, request.host),
374 "preferredUsername": self.username,
375 "displayName": "{0}@{1}".format(self.username, request.host),
376 "links": {
377 "self": {
378 "href": request.urlgen(
379 "mediagoblin.api.user.profile",
380 username=self.username,
381 qualified=True
384 "activity-inbox": {
385 "href": request.urlgen(
386 "mediagoblin.api.inbox",
387 username=self.username,
388 qualified=True
391 "activity-outbox": {
392 "href": request.urlgen(
393 "mediagoblin.api.feed",
394 username=self.username,
395 qualified=True
401 user.update(super(LocalUser, self).serialize(request))
402 return user
404 class RemoteUser(User):
405 """ User that is on another (remote) instance """
406 __tablename__ = "core__remote_users"
408 id = Column(Integer, ForeignKey("core__users.id"), primary_key=True)
409 webfinger = Column(Unicode, unique=True)
411 __mapper_args__ = {
412 'polymorphic_identity': 'user_remote'
415 def __repr__(self):
416 return "<{0} #{1} {2}>".format(
417 self.__class__.__name__,
418 self.id,
419 self.webfinger
423 class Client(Base):
425 Model representing a client - Used for API Auth
427 __tablename__ = "core__clients"
429 id = Column(Unicode, nullable=True, primary_key=True)
430 secret = Column(Unicode, nullable=False)
431 expirey = Column(DateTime, nullable=True)
432 application_type = Column(Unicode, nullable=False)
433 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
434 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
436 # optional stuff
437 redirect_uri = Column(JSONEncoded, nullable=True)
438 logo_url = Column(Unicode, nullable=True)
439 application_name = Column(Unicode, nullable=True)
440 contacts = Column(JSONEncoded, nullable=True)
442 def __repr__(self):
443 if self.application_name:
444 return "<Client {0} - {1}>".format(self.application_name, self.id)
445 else:
446 return "<Client {0}>".format(self.id)
448 class RequestToken(Base):
450 Model for representing the request tokens
452 __tablename__ = "core__request_tokens"
454 token = Column(Unicode, primary_key=True)
455 secret = Column(Unicode, nullable=False)
456 client = Column(Unicode, ForeignKey(Client.id))
457 user = Column(Integer, ForeignKey(User.id), nullable=True)
458 used = Column(Boolean, default=False)
459 authenticated = Column(Boolean, default=False)
460 verifier = Column(Unicode, nullable=True)
461 callback = Column(Unicode, nullable=False, default=u"oob")
462 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
463 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
465 get_client = relationship(Client)
467 class AccessToken(Base):
469 Model for representing the access tokens
471 __tablename__ = "core__access_tokens"
473 token = Column(Unicode, nullable=False, primary_key=True)
474 secret = Column(Unicode, nullable=False)
475 user = Column(Integer, ForeignKey(User.id))
476 request_token = Column(Unicode, ForeignKey(RequestToken.token))
477 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
478 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
480 get_requesttoken = relationship(RequestToken)
483 class NonceTimestamp(Base):
485 A place the timestamp and nonce can be stored - this is for OAuth1
487 __tablename__ = "core__nonce_timestamps"
489 nonce = Column(Unicode, nullable=False, primary_key=True)
490 timestamp = Column(DateTime, nullable=False, primary_key=True)
492 class MediaEntry(Base, MediaEntryMixin):
494 TODO: Consider fetching the media_files using join
496 __tablename__ = "core__media_entries"
498 id = Column(Integer, primary_key=True)
499 public_id = Column(Unicode, unique=True, nullable=True)
500 remote = Column(Boolean, default=False)
502 uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
503 title = Column(Unicode, nullable=False)
504 slug = Column(Unicode)
505 description = Column(UnicodeText) # ??
506 media_type = Column(Unicode, nullable=False)
507 state = Column(Unicode, default=u'unprocessed', nullable=False)
508 # or use sqlalchemy.types.Enum?
509 license = Column(Unicode)
510 file_size = Column(Integer, default=0)
511 location = Column(Integer, ForeignKey("core__locations.id"))
512 get_location = relationship("Location", lazy="joined")
514 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow,
515 index=True)
516 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
518 fail_error = Column(Unicode)
519 fail_metadata = Column(JSONEncoded)
521 transcoding_progress = Column(SmallInteger)
523 queued_media_file = Column(PathTupleWithSlashes)
525 queued_task_id = Column(Unicode)
527 __table_args__ = (
528 UniqueConstraint('uploader', 'slug'),
531 get_uploader = relationship(User)
533 media_files_helper = relationship("MediaFile",
534 collection_class=attribute_mapped_collection("name"),
535 cascade="all, delete-orphan"
537 media_files = association_proxy('media_files_helper', 'file_path',
538 creator=lambda k, v: MediaFile(name=k, file_path=v)
541 attachment_files_helper = relationship("MediaAttachmentFile",
542 cascade="all, delete-orphan",
543 order_by="MediaAttachmentFile.created"
545 attachment_files = association_proxy("attachment_files_helper", "dict_view",
546 creator=lambda v: MediaAttachmentFile(
547 name=v["name"], filepath=v["filepath"])
550 tags_helper = relationship("MediaTag",
551 cascade="all, delete-orphan" # should be automatically deleted
553 tags = association_proxy("tags_helper", "dict_view",
554 creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
557 collections_helper = relationship("CollectionItem",
558 cascade="all, delete-orphan"
560 collections = association_proxy("collections_helper", "in_collection")
561 media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
562 default=MutationDict())
564 ## TODO
565 # fail_error
567 def get_comments(self, ascending=False):
568 order_col = MediaComment.created
569 if not ascending:
570 order_col = desc(order_col)
571 return self.all_comments.order_by(order_col)
573 def url_to_prev(self, urlgen):
574 """get the next 'newer' entry by this user"""
575 media = MediaEntry.query.filter(
576 (MediaEntry.uploader == self.uploader)
577 & (MediaEntry.state == u'processed')
578 & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()
580 if media is not None:
581 return media.url_for_self(urlgen)
583 def url_to_next(self, urlgen):
584 """get the next 'older' entry by this user"""
585 media = MediaEntry.query.filter(
586 (MediaEntry.uploader == self.uploader)
587 & (MediaEntry.state == u'processed')
588 & (MediaEntry.id < self.id)).order_by(desc(MediaEntry.id)).first()
590 if media is not None:
591 return media.url_for_self(urlgen)
593 def get_file_metadata(self, file_key, metadata_key=None):
595 Return the file_metadata dict of a MediaFile. If metadata_key is given,
596 return the value of the key.
598 media_file = MediaFile.query.filter_by(media_entry=self.id,
599 name=six.text_type(file_key)).first()
601 if media_file:
602 if metadata_key:
603 return media_file.file_metadata.get(metadata_key, None)
605 return media_file.file_metadata
607 def set_file_metadata(self, file_key, **kwargs):
609 Update the file_metadata of a MediaFile.
611 media_file = MediaFile.query.filter_by(media_entry=self.id,
612 name=six.text_type(file_key)).first()
614 file_metadata = media_file.file_metadata or {}
616 for key, value in six.iteritems(kwargs):
617 file_metadata[key] = value
619 media_file.file_metadata = file_metadata
620 media_file.save()
622 @property
623 def media_data(self):
624 return getattr(self, self.media_data_ref)
626 def media_data_init(self, **kwargs):
628 Initialize or update the contents of a media entry's media_data row
630 media_data = self.media_data
632 if media_data is None:
633 # Get the correct table:
634 table = import_component(self.media_type + '.models:DATA_MODEL')
635 # No media data, so actually add a new one
636 media_data = table(**kwargs)
637 # Get the relationship set up.
638 media_data.get_media_entry = self
639 else:
640 # Update old media data
641 for field, value in six.iteritems(kwargs):
642 setattr(media_data, field, value)
644 @memoized_property
645 def media_data_ref(self):
646 return import_component(self.media_type + '.models:BACKREF_NAME')
648 def __repr__(self):
649 if six.PY2:
650 # obj.__repr__() should return a str on Python 2
651 safe_title = self.title.encode('utf-8', 'replace')
652 else:
653 safe_title = self.title
655 return '<{classname} {id}: {title}>'.format(
656 classname=self.__class__.__name__,
657 id=self.id,
658 title=safe_title)
660 def delete(self, del_orphan_tags=True, **kwargs):
661 """Delete MediaEntry and all related files/attachments/comments
663 This will *not* automatically delete unused collections, which
664 can remain empty...
666 :param del_orphan_tags: True/false if we delete unused Tags too
667 :param commit: True/False if this should end the db transaction"""
668 # User's CollectionItems are automatically deleted via "cascade".
669 # Comments on this Media are deleted by cascade, hopefully.
671 # Delete all related files/attachments
672 try:
673 delete_media_files(self)
674 except OSError as error:
675 # Returns list of files we failed to delete
676 _log.error('No such files from the user "{1}" to delete: '
677 '{0}'.format(str(error), self.get_uploader))
678 _log.info('Deleted Media entry id "{0}"'.format(self.id))
679 # Related MediaTag's are automatically cleaned, but we might
680 # want to clean out unused Tag's too.
681 if del_orphan_tags:
682 # TODO: Import here due to cyclic imports!!!
683 # This cries for refactoring
684 from mediagoblin.db.util import clean_orphan_tags
685 clean_orphan_tags(commit=False)
686 # pass through commit=False/True in kwargs
687 super(MediaEntry, self).delete(**kwargs)
689 def serialize(self, request, show_comments=True):
690 """ Unserialize MediaEntry to object """
691 author = self.get_uploader
692 published = UTC.localize(self.created)
693 updated = UTC.localize(self.updated)
694 public_id = self.get_public_id(request)
695 context = {
696 "id": public_id,
697 "author": author.serialize(request),
698 "objectType": self.object_type,
699 "url": self.url_for_self(request.urlgen, qualified=True),
700 "image": {
701 "url": request.host_url + self.thumb_url[1:],
703 "fullImage":{
704 "url": request.host_url + self.original_url[1:],
706 "published": published.isoformat(),
707 "updated": updated.isoformat(),
708 "pump_io": {
709 "shared": False,
711 "links": {
712 "self": {
713 "href": public_id,
719 if self.title:
720 context["displayName"] = self.title
722 if self.description:
723 context["content"] = self.description
725 if self.license:
726 context["license"] = self.license
728 if self.location:
729 context["location"] = self.get_location.serialize(request)
731 if show_comments:
732 comments = [
733 comment.serialize(request) for comment in self.get_comments()]
734 total = len(comments)
735 context["replies"] = {
736 "totalItems": total,
737 "items": comments,
738 "url": request.urlgen(
739 "mediagoblin.api.object.comments",
740 object_type=self.object_type,
741 id=self.id,
742 qualified=True
746 # Add image height and width if possible. We didn't use to store this
747 # data and we're not able (and maybe not willing) to re-process all
748 # images so it's possible this might not exist.
749 if self.get_file_metadata("thumb", "height"):
750 height = self.get_file_metadata("thumb", "height")
751 context["image"]["height"] = height
752 if self.get_file_metadata("thumb", "width"):
753 width = self.get_file_metadata("thumb", "width")
754 context["image"]["width"] = width
755 if self.get_file_metadata("original", "height"):
756 height = self.get_file_metadata("original", "height")
757 context["fullImage"]["height"] = height
758 if self.get_file_metadata("original", "height"):
759 width = self.get_file_metadata("original", "width")
760 context["fullImage"]["width"] = width
762 return context
764 def unserialize(self, data):
765 """ Takes API objects and unserializes on existing MediaEntry """
766 if "displayName" in data:
767 self.title = data["displayName"]
769 if "content" in data:
770 self.description = data["content"]
772 if "license" in data:
773 self.license = data["license"]
775 if "location" in data:
776 License.create(data["location"], self)
778 return True
780 class FileKeynames(Base):
782 keywords for various places.
783 currently the MediaFile keys
785 __tablename__ = "core__file_keynames"
786 id = Column(Integer, primary_key=True)
787 name = Column(Unicode, unique=True)
789 def __repr__(self):
790 return "<FileKeyname %r: %r>" % (self.id, self.name)
792 @classmethod
793 def find_or_new(cls, name):
794 t = cls.query.filter_by(name=name).first()
795 if t is not None:
796 return t
797 return cls(name=name)
800 class MediaFile(Base):
802 TODO: Highly consider moving "name" into a new table.
803 TODO: Consider preloading said table in software
805 __tablename__ = "core__mediafiles"
807 media_entry = Column(
808 Integer, ForeignKey(MediaEntry.id),
809 nullable=False)
810 name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
811 file_path = Column(PathTupleWithSlashes)
812 file_metadata = Column(MutationDict.as_mutable(JSONEncoded))
814 __table_args__ = (
815 PrimaryKeyConstraint('media_entry', 'name_id'),
818 def __repr__(self):
819 return "<MediaFile %s: %r>" % (self.name, self.file_path)
821 name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
822 name = association_proxy('name_helper', 'name',
823 creator=FileKeynames.find_or_new
827 class MediaAttachmentFile(Base):
828 __tablename__ = "core__attachment_files"
830 id = Column(Integer, primary_key=True)
831 media_entry = Column(
832 Integer, ForeignKey(MediaEntry.id),
833 nullable=False)
834 name = Column(Unicode, nullable=False)
835 filepath = Column(PathTupleWithSlashes)
836 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
838 @property
839 def dict_view(self):
840 """A dict like view on this object"""
841 return DictReadAttrProxy(self)
844 class Tag(Base):
845 __tablename__ = "core__tags"
847 id = Column(Integer, primary_key=True)
848 slug = Column(Unicode, nullable=False, unique=True)
850 def __repr__(self):
851 return "<Tag %r: %r>" % (self.id, self.slug)
853 @classmethod
854 def find_or_new(cls, slug):
855 t = cls.query.filter_by(slug=slug).first()
856 if t is not None:
857 return t
858 return cls(slug=slug)
861 class MediaTag(Base):
862 __tablename__ = "core__media_tags"
864 id = Column(Integer, primary_key=True)
865 media_entry = Column(
866 Integer, ForeignKey(MediaEntry.id),
867 nullable=False, index=True)
868 tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
869 name = Column(Unicode)
870 # created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
872 __table_args__ = (
873 UniqueConstraint('tag', 'media_entry'),
876 tag_helper = relationship(Tag)
877 slug = association_proxy('tag_helper', 'slug',
878 creator=Tag.find_or_new
881 def __init__(self, name=None, slug=None):
882 Base.__init__(self)
883 if name is not None:
884 self.name = name
885 if slug is not None:
886 self.tag_helper = Tag.find_or_new(slug)
888 @property
889 def dict_view(self):
890 """A dict like view on this object"""
891 return DictReadAttrProxy(self)
894 class MediaComment(Base, MediaCommentMixin):
895 __tablename__ = "core__media_comments"
897 id = Column(Integer, primary_key=True)
898 media_entry = Column(
899 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
900 author = Column(Integer, ForeignKey(User.id), nullable=False)
901 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
902 content = Column(UnicodeText, nullable=False)
903 location = Column(Integer, ForeignKey("core__locations.id"))
904 get_location = relationship("Location", lazy="joined")
906 # Cascade: Comments are owned by their creator. So do the full thing.
907 # lazy=dynamic: People might post a *lot* of comments,
908 # so make the "posted_comments" a query-like thing.
909 get_author = relationship(User,
910 backref=backref("posted_comments",
911 lazy="dynamic",
912 cascade="all, delete-orphan"))
913 get_entry = relationship(MediaEntry,
914 backref=backref("comments",
915 lazy="dynamic",
916 cascade="all, delete-orphan"))
918 # Cascade: Comments are somewhat owned by their MediaEntry.
919 # So do the full thing.
920 # lazy=dynamic: MediaEntries might have many comments,
921 # so make the "all_comments" a query-like thing.
922 get_media_entry = relationship(MediaEntry,
923 backref=backref("all_comments",
924 lazy="dynamic",
925 cascade="all, delete-orphan"))
927 def serialize(self, request):
928 """ Unserialize to python dictionary for API """
929 href = request.urlgen(
930 "mediagoblin.api.object",
931 object_type=self.object_type,
932 id=self.id,
933 qualified=True
935 media = MediaEntry.query.filter_by(id=self.media_entry).first()
936 author = self.get_author
937 published = UTC.localize(self.created)
938 context = {
939 "id": href,
940 "objectType": self.object_type,
941 "content": self.content,
942 "inReplyTo": media.serialize(request, show_comments=False),
943 "author": author.serialize(request),
944 "published": published.isoformat(),
945 "updated": published.isoformat(),
948 if self.location:
949 context["location"] = self.get_location.seralize(request)
951 return context
953 def unserialize(self, data, request):
954 """ Takes API objects and unserializes on existing comment """
955 # Handle changing the reply ID
956 if "inReplyTo" in data:
957 # Validate that the ID is correct
958 try:
959 media_id = int(extract_url_arguments(
960 url=data["inReplyTo"]["id"],
961 urlmap=request.app.url_map
962 )["id"])
963 except ValueError:
964 return False
966 media = MediaEntry.query.filter_by(id=media_id).first()
967 if media is None:
968 return False
970 self.media_entry = media.id
972 if "content" in data:
973 self.content = data["content"]
975 if "location" in data:
976 Location.create(data["location"], self)
978 return True
982 class Collection(Base, CollectionMixin):
983 """An 'album' or 'set' of media by a user.
985 On deletion, contained CollectionItems get automatically reaped via
986 SQL cascade"""
987 __tablename__ = "core__collections"
989 id = Column(Integer, primary_key=True)
990 title = Column(Unicode, nullable=False)
991 slug = Column(Unicode)
992 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow,
993 index=True)
994 description = Column(UnicodeText)
995 creator = Column(Integer, ForeignKey(User.id), nullable=False)
996 location = Column(Integer, ForeignKey("core__locations.id"))
997 get_location = relationship("Location", lazy="joined")
999 # TODO: No of items in Collection. Badly named, can we migrate to num_items?
1000 items = Column(Integer, default=0)
1002 # Cascade: Collections are owned by their creator. So do the full thing.
1003 get_creator = relationship(User,
1004 backref=backref("collections",
1005 cascade="all, delete-orphan"))
1007 __table_args__ = (
1008 UniqueConstraint('creator', 'slug'),
1011 def get_collection_items(self, ascending=False):
1012 #TODO, is this still needed with self.collection_items being available?
1013 order_col = CollectionItem.position
1014 if not ascending:
1015 order_col = desc(order_col)
1016 return CollectionItem.query.filter_by(
1017 collection=self.id).order_by(order_col)
1019 def __repr__(self):
1020 safe_title = self.title.encode('ascii', 'replace')
1021 return '<{classname} #{id}: {title} by {creator}>'.format(
1022 id=self.id,
1023 classname=self.__class__.__name__,
1024 creator=self.creator,
1025 title=safe_title)
1027 def serialize(self, request):
1028 # Get all serialized output in a list
1029 items = []
1030 for item in self.get_collection_items():
1031 items.append(item.serialize(request))
1033 return {
1034 "totalItems": self.items,
1035 "url": self.url_for_self(request.urlgen, qualified=True),
1036 "items": items,
1040 class CollectionItem(Base, CollectionItemMixin):
1041 __tablename__ = "core__collection_items"
1043 id = Column(Integer, primary_key=True)
1044 media_entry = Column(
1045 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
1046 collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
1047 note = Column(UnicodeText, nullable=True)
1048 added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1049 position = Column(Integer)
1051 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
1052 in_collection = relationship(Collection,
1053 backref=backref(
1054 "collection_items",
1055 cascade="all, delete-orphan"))
1057 get_media_entry = relationship(MediaEntry)
1059 __table_args__ = (
1060 UniqueConstraint('collection', 'media_entry'),
1063 @property
1064 def dict_view(self):
1065 """A dict like view on this object"""
1066 return DictReadAttrProxy(self)
1068 def __repr__(self):
1069 return '<{classname} #{id}: Entry {entry} in {collection}>'.format(
1070 id=self.id,
1071 classname=self.__class__.__name__,
1072 collection=self.collection,
1073 entry=self.media_entry)
1075 def serialize(self, request):
1076 return self.get_media_entry.serialize(request)
1079 class ProcessingMetaData(Base):
1080 __tablename__ = 'core__processing_metadata'
1082 id = Column(Integer, primary_key=True)
1083 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
1084 index=True)
1085 media_entry = relationship(MediaEntry,
1086 backref=backref('processing_metadata',
1087 cascade='all, delete-orphan'))
1088 callback_url = Column(Unicode)
1090 @property
1091 def dict_view(self):
1092 """A dict like view on this object"""
1093 return DictReadAttrProxy(self)
1096 class CommentSubscription(Base):
1097 __tablename__ = 'core__comment_subscriptions'
1098 id = Column(Integer, primary_key=True)
1100 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1102 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
1103 media_entry = relationship(MediaEntry,
1104 backref=backref('comment_subscriptions',
1105 cascade='all, delete-orphan'))
1107 user_id = Column(Integer, ForeignKey(User.id), nullable=False)
1108 user = relationship(User,
1109 backref=backref('comment_subscriptions',
1110 cascade='all, delete-orphan'))
1112 notify = Column(Boolean, nullable=False, default=True)
1113 send_email = Column(Boolean, nullable=False, default=True)
1115 def __repr__(self):
1116 return ('<{classname} #{id}: {user} {media} notify: '
1117 '{notify} email: {email}>').format(
1118 id=self.id,
1119 classname=self.__class__.__name__,
1120 user=self.user,
1121 media=self.media_entry,
1122 notify=self.notify,
1123 email=self.send_email)
1126 class Notification(Base):
1127 __tablename__ = 'core__notifications'
1128 id = Column(Integer, primary_key=True)
1129 type = Column(Unicode)
1131 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1133 user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False,
1134 index=True)
1135 seen = Column(Boolean, default=lambda: False, index=True)
1136 user = relationship(
1137 User,
1138 backref=backref('notifications', cascade='all, delete-orphan'))
1140 __mapper_args__ = {
1141 'polymorphic_identity': 'notification',
1142 'polymorphic_on': type
1145 def __repr__(self):
1146 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1147 id=self.id,
1148 klass=self.__class__.__name__,
1149 user=self.user,
1150 subject=getattr(self, 'subject', None),
1151 seen='unseen' if not self.seen else 'seen')
1153 def __unicode__(self):
1154 return u'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1155 id=self.id,
1156 klass=self.__class__.__name__,
1157 user=self.user,
1158 subject=getattr(self, 'subject', None),
1159 seen='unseen' if not self.seen else 'seen')
1162 class CommentNotification(Notification):
1163 __tablename__ = 'core__comment_notifications'
1164 id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
1166 subject_id = Column(Integer, ForeignKey(MediaComment.id))
1167 subject = relationship(
1168 MediaComment,
1169 backref=backref('comment_notifications', cascade='all, delete-orphan'))
1171 __mapper_args__ = {
1172 'polymorphic_identity': 'comment_notification'
1176 class ProcessingNotification(Notification):
1177 __tablename__ = 'core__processing_notifications'
1179 id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
1181 subject_id = Column(Integer, ForeignKey(MediaEntry.id))
1182 subject = relationship(
1183 MediaEntry,
1184 backref=backref('processing_notifications',
1185 cascade='all, delete-orphan'))
1187 __mapper_args__ = {
1188 'polymorphic_identity': 'processing_notification'
1191 # the with_polymorphic call has been moved to the bottom above MODELS
1192 # this is because it causes conflicts with relationship calls.
1194 class ReportBase(Base):
1196 This is the basic report object which the other reports are based off of.
1198 :keyword reporter_id Holds the id of the user who created
1199 the report, as an Integer column.
1200 :keyword report_content Hold the explanation left by the repor-
1201 -ter to indicate why they filed the
1202 report in the first place, as a
1203 Unicode column.
1204 :keyword reported_user_id Holds the id of the user who created
1205 the content which was reported, as
1206 an Integer column.
1207 :keyword created Holds a datetime column of when the re-
1208 -port was filed.
1209 :keyword discriminator This column distinguishes between the
1210 different types of reports.
1211 :keyword resolver_id Holds the id of the moderator/admin who
1212 resolved the report.
1213 :keyword resolved Holds the DateTime object which descri-
1214 -bes when this report was resolved
1215 :keyword result Holds the UnicodeText column of the
1216 resolver's reasons for resolving
1217 the report this way. Some of this
1218 is auto-generated
1220 __tablename__ = 'core__reports'
1221 id = Column(Integer, primary_key=True)
1222 reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
1223 reporter = relationship(
1224 User,
1225 backref=backref("reports_filed_by",
1226 lazy="dynamic",
1227 cascade="all, delete-orphan"),
1228 primaryjoin="User.id==ReportBase.reporter_id")
1229 report_content = Column(UnicodeText)
1230 reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
1231 reported_user = relationship(
1232 User,
1233 backref=backref("reports_filed_on",
1234 lazy="dynamic",
1235 cascade="all, delete-orphan"),
1236 primaryjoin="User.id==ReportBase.reported_user_id")
1237 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1238 discriminator = Column('type', Unicode(50))
1239 resolver_id = Column(Integer, ForeignKey(User.id))
1240 resolver = relationship(
1241 User,
1242 backref=backref("reports_resolved_by",
1243 lazy="dynamic",
1244 cascade="all, delete-orphan"),
1245 primaryjoin="User.id==ReportBase.resolver_id")
1247 resolved = Column(DateTime)
1248 result = Column(UnicodeText)
1249 __mapper_args__ = {'polymorphic_on': discriminator}
1251 def is_comment_report(self):
1252 return self.discriminator=='comment_report'
1254 def is_media_entry_report(self):
1255 return self.discriminator=='media_report'
1257 def is_archived_report(self):
1258 return self.resolved is not None
1260 def archive(self,resolver_id, resolved, result):
1261 self.resolver_id = resolver_id
1262 self.resolved = resolved
1263 self.result = result
1266 class CommentReport(ReportBase):
1268 Reports that have been filed on comments.
1269 :keyword comment_id Holds the integer value of the reported
1270 comment's ID
1272 __tablename__ = 'core__reports_on_comments'
1273 __mapper_args__ = {'polymorphic_identity': 'comment_report'}
1275 id = Column('id',Integer, ForeignKey('core__reports.id'),
1276 primary_key=True)
1277 comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=True)
1278 comment = relationship(
1279 MediaComment, backref=backref("reports_filed_on",
1280 lazy="dynamic"))
1282 class MediaReport(ReportBase):
1284 Reports that have been filed on media entries
1285 :keyword media_entry_id Holds the integer value of the reported
1286 media entry's ID
1288 __tablename__ = 'core__reports_on_media'
1289 __mapper_args__ = {'polymorphic_identity': 'media_report'}
1291 id = Column('id',Integer, ForeignKey('core__reports.id'),
1292 primary_key=True)
1293 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=True)
1294 media_entry = relationship(
1295 MediaEntry,
1296 backref=backref("reports_filed_on",
1297 lazy="dynamic"))
1299 class UserBan(Base):
1301 Holds the information on a specific user's ban-state. As long as one of
1302 these is attached to a user, they are banned from accessing mediagoblin.
1303 When they try to log in, they are greeted with a page that tells them
1304 the reason why they are banned and when (if ever) the ban will be
1305 lifted
1307 :keyword user_id Holds the id of the user this object is
1308 attached to. This is a one-to-one
1309 relationship.
1310 :keyword expiration_date Holds the date that the ban will be lifted.
1311 If this is null, the ban is permanent
1312 unless a moderator manually lifts it.
1313 :keyword reason Holds the reason why the user was banned.
1315 __tablename__ = 'core__user_bans'
1317 user_id = Column(Integer, ForeignKey(User.id), nullable=False,
1318 primary_key=True)
1319 expiration_date = Column(Date)
1320 reason = Column(UnicodeText, nullable=False)
1323 class Privilege(Base):
1325 The Privilege table holds all of the different privileges a user can hold.
1326 If a user 'has' a privilege, the User object is in a relationship with the
1327 privilege object.
1329 :keyword privilege_name Holds a unicode object that is the recognizable
1330 name of this privilege. This is the column
1331 used for identifying whether or not a user
1332 has a necessary privilege or not.
1335 __tablename__ = 'core__privileges'
1337 id = Column(Integer, nullable=False, primary_key=True)
1338 privilege_name = Column(Unicode, nullable=False, unique=True)
1339 all_users = relationship(
1340 User,
1341 backref='all_privileges',
1342 secondary="core__privileges_users")
1344 def __init__(self, privilege_name):
1346 Currently consructors are required for tables that are initialized thru
1347 the FOUNDATIONS system. This is because they need to be able to be con-
1348 -structed by a list object holding their arg*s
1350 self.privilege_name = privilege_name
1352 def __repr__(self):
1353 return "<Privilege %s>" % (self.privilege_name)
1356 class PrivilegeUserAssociation(Base):
1358 This table holds the many-to-many relationship between User and Privilege
1361 __tablename__ = 'core__privileges_users'
1363 user = Column(
1364 "user",
1365 Integer,
1366 ForeignKey(User.id),
1367 primary_key=True)
1368 privilege = Column(
1369 "privilege",
1370 Integer,
1371 ForeignKey(Privilege.id),
1372 primary_key=True)
1374 class Generator(Base):
1375 """ Information about what created an activity """
1376 __tablename__ = "core__generators"
1378 id = Column(Integer, primary_key=True)
1379 name = Column(Unicode, nullable=False)
1380 published = Column(DateTime, default=datetime.datetime.utcnow)
1381 updated = Column(DateTime, default=datetime.datetime.utcnow)
1382 object_type = Column(Unicode, nullable=False)
1384 def __repr__(self):
1385 return "<{klass} {name}>".format(
1386 klass=self.__class__.__name__,
1387 name=self.name
1390 def serialize(self, request):
1391 href = request.urlgen(
1392 "mediagoblin.api.object",
1393 object_type=self.object_type,
1394 id=self.id,
1395 qualified=True
1397 published = UTC.localize(self.published)
1398 updated = UTC.localize(self.updated)
1399 return {
1400 "id": href,
1401 "displayName": self.name,
1402 "published": published.isoformat(),
1403 "updated": updated.isoformat(),
1404 "objectType": self.object_type,
1407 def unserialize(self, data):
1408 if "displayName" in data:
1409 self.name = data["displayName"]
1411 class Activity(Base, ActivityMixin):
1413 This holds all the metadata about an activity such as uploading an image,
1414 posting a comment, etc.
1416 __tablename__ = "core__activities"
1418 id = Column(Integer, primary_key=True)
1419 actor = Column(Integer,
1420 ForeignKey("core__users.id"),
1421 nullable=False)
1422 published = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1423 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1424 verb = Column(Unicode, nullable=False)
1425 content = Column(Unicode, nullable=True)
1426 title = Column(Unicode, nullable=True)
1427 generator = Column(Integer,
1428 ForeignKey("core__generators.id"),
1429 nullable=True)
1431 # Create the generic foreign keys for the object
1432 object_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=False)
1433 object_helper = relationship(GenericModelReference, foreign_keys=[object_id])
1434 object = association_proxy("object_helper", "get_object",
1435 creator=GenericModelReference.find_or_new)
1437 # Create the generic foreign Key for the target
1438 target_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=True)
1439 target_helper = relationship(GenericModelReference, foreign_keys=[target_id])
1440 target = association_proxy("target_helper", "get_object",
1441 creator=GenericModelReference.find_or_new)
1443 get_actor = relationship(User,
1444 backref=backref("activities",
1445 cascade="all, delete-orphan"))
1446 get_generator = relationship(Generator)
1448 def __repr__(self):
1449 if self.content is None:
1450 return "<{klass} verb:{verb}>".format(
1451 klass=self.__class__.__name__,
1452 verb=self.verb
1454 else:
1455 return "<{klass} {content}>".format(
1456 klass=self.__class__.__name__,
1457 content=self.content
1460 def save(self, set_updated=True, *args, **kwargs):
1461 if set_updated:
1462 self.updated = datetime.datetime.now()
1463 super(Activity, self).save(*args, **kwargs)
1465 with_polymorphic(
1466 Notification,
1467 [ProcessingNotification, CommentNotification])
1469 MODELS = [
1470 LocalUser, RemoteUser, User, MediaEntry, Tag, MediaTag, MediaComment,
1471 Collection, CollectionItem, MediaFile, FileKeynames, MediaAttachmentFile,
1472 ProcessingMetaData, Notification, CommentNotification,
1473 ProcessingNotification, Client, CommentSubscription, ReportBase,
1474 CommentReport, MediaReport, UserBan, Privilege, PrivilegeUserAssociation,
1475 RequestToken, AccessToken, NonceTimestamp, Activity, Generator, Location,
1476 GenericModelReference]
1479 Foundations are the default rows that are created immediately after the tables
1480 are initialized. Each entry to this dictionary should be in the format of:
1481 ModelConstructorObject:List of Dictionaries
1482 (Each Dictionary represents a row on the Table to be created, containing each
1483 of the columns' names as a key string, and each of the columns' values as a
1484 value)
1486 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1487 user_foundations = [{'name':u'Joanna', 'age':24},
1488 {'name':u'Andrea', 'age':41}]
1490 FOUNDATIONS = {User:user_foundations}
1492 privilege_foundations = [{'privilege_name':u'admin'},
1493 {'privilege_name':u'moderator'},
1494 {'privilege_name':u'uploader'},
1495 {'privilege_name':u'reporter'},
1496 {'privilege_name':u'commenter'},
1497 {'privilege_name':u'active'}]
1498 FOUNDATIONS = {Privilege:privilege_foundations}
1500 ######################################################
1501 # Special, migrations-tracking table
1503 # Not listed in MODELS because this is special and not
1504 # really migrated, but used for migrations (for now)
1505 ######################################################
1507 class MigrationData(Base):
1508 __tablename__ = "core__migrations"
1510 name = Column(Unicode, primary_key=True)
1511 version = Column(Integer, nullable=False, default=0)
1513 ######################################################
1516 def show_table_init(engine_uri):
1517 if engine_uri is None:
1518 engine_uri = 'sqlite:///:memory:'
1519 from sqlalchemy import create_engine
1520 engine = create_engine(engine_uri, echo=True)
1522 Base.metadata.create_all(engine)
1525 if __name__ == '__main__':
1526 from sys import argv
1527 print(repr(argv))
1528 if len(argv) == 2:
1529 uri = argv[1]
1530 else:
1531 uri = None
1532 show_table_init(uri)