1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 TODO: indexes on foreignkeys, where useful.
21 from __future__
import print_function
26 from sqlalchemy
import Column
, Integer
, Unicode
, UnicodeText
, DateTime
, \
27 Boolean
, ForeignKey
, UniqueConstraint
, PrimaryKeyConstraint
, \
28 SmallInteger
, Date
, types
29 from sqlalchemy
.orm
import relationship
, backref
, with_polymorphic
, validates
, \
31 from sqlalchemy
.orm
.collections
import attribute_mapped_collection
32 from sqlalchemy
.sql
import and_
33 from sqlalchemy
.sql
.expression
import desc
34 from sqlalchemy
.ext
.associationproxy
import association_proxy
35 from sqlalchemy
.util
import memoized_property
37 from mediagoblin
.db
.extratypes
import (PathTupleWithSlashes
, JSONEncoded
,
39 from mediagoblin
.db
.base
import Base
, DictReadAttrProxy
, FakeCursor
40 from mediagoblin
.db
.mixin
import UserMixin
, MediaEntryMixin
, \
41 CollectionMixin
, CollectionItemMixin
, ActivityMixin
, TextCommentMixin
, \
43 from mediagoblin
.tools
.files
import delete_media_files
44 from mediagoblin
.tools
.common
import import_component
45 from mediagoblin
.tools
.routing
import extract_url_arguments
50 _log
= logging
.getLogger(__name__
)
52 class GenericModelReference(Base
):
54 Represents a relationship to any model that is defined with a integer pk
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.
68 UniqueConstraint("model_type", "obj_pk"),
72 # This can happen if it's yet to be saved
73 if self
.model_type
is None or self
.obj_pk
is 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
):
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
]
123 def find_for_obj(cls
, obj
):
124 """ Finds a GMR for an object or returns None """
125 # Is there one for this already.
127 pk
= getattr(obj
, "id")
129 gmr
= cls
.query
.filter_by(
131 model_type
=model
.__tablename
__
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
145 model_type
=type(obj
).__tablename
__
150 class Location(Base
):
151 """ Represents a physical location """
152 __tablename__
= "core__locations"
154 id = Column(Integer
, primary_key
=True)
155 name
= Column(Unicode
)
158 position
= Column(MutationDict
.as_mutable(JSONEncoded
))
159 address
= Column(MutationDict
.as_mutable(JSONEncoded
))
162 def create(cls
, data
, obj
):
164 location
.unserialize(data
)
166 obj
.location
= location
.id
169 def serialize(self
, request
):
170 location
= {"objectType": "place"}
172 if self
.name
is not None:
173 location
["displayName"] = self
.name
176 location
["position"] = self
.position
179 location
["address"] = self
.address
183 def unserialize(self
, data
):
184 if "displayName" in data
:
185 self
.name
= data
["displayName"]
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
231 NB: ForeignKeys should reference this User model and NOT the LocalUser or
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"))
250 get_location
= relationship("Location", lazy
="joined")
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
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
:
312 elif allow_admin
and self
.has_privilege(u
'admin', allow_admin
=False):
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
)
330 "published": published
.isoformat(),
331 "updated": updated
.isoformat(),
332 "objectType": self
.object_type
,
340 user
.update({"summary": self
.bio
})
342 user
.update({"url": self
.url
})
344 user
.update({"location": self
.get_location
.serialize(request
)})
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
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
)
377 "polymorphic_identity": "user_local",
381 # plugin data would be in a separate model
384 return '<{0} #{1} {2} {3} "{4}">'.format(
385 self
.__class
__.__name
__,
387 'verified' if self
.has_privilege(u
'active') else 'non-verified',
388 'admin' if self
.has_privilege(u
'admin') else 'user',
391 def serialize(self
, request
):
393 "id": "acct:{0}@{1}".format(self
.username
, request
.host
),
394 "preferredUsername": self
.username
,
395 "displayName": "{0}@{1}".format(self
.username
, request
.host
),
398 "href": request
.urlgen(
399 "mediagoblin.api.user.profile",
400 username
=self
.username
,
405 "href": request
.urlgen(
406 "mediagoblin.api.inbox",
407 username
=self
.username
,
412 "href": request
.urlgen(
413 "mediagoblin.api.feed",
414 username
=self
.username
,
421 user
.update(super(LocalUser
, self
).serialize(request
))
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)
432 'polymorphic_identity': 'user_remote'
436 return "<{0} #{1} {2}>".format(
437 self
.__class
__.__name
__,
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
)
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)
463 if self
.application_name
:
464 return "<Client {0} - {1}>".format(self
.application_name
, self
.id)
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
,
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
)
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())
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
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
__
604 query
= query
.order_by(Comment
.added
.asc())
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()
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
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
677 # Update old media data
678 for field
, value
in six
.iteritems(kwargs
):
679 setattr(media_data
, field
, value
)
682 def media_data_ref(self
):
683 return import_component(self
.media_type
+ '.models:BACKREF_NAME')
687 # obj.__repr__() should return a str on Python 2
688 safe_title
= self
.title
.encode('utf-8', 'replace')
690 safe_title
= self
.title
692 return '<{classname} {id}: {title}>'.format(
693 classname
=self
.__class
__.__name
__,
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
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
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.
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
)
741 "author": author
.serialize(request
),
742 "objectType": self
.object_type
,
743 "url": self
.url_for_self(request
.urlgen
, qualified
=True),
745 "url": request
.host_url
+ self
.thumb_url
[1:],
748 "url": request
.host_url
+ self
.original_url
[1:],
750 "published": published
.isoformat(),
751 "updated": updated
.isoformat(),
764 context
["displayName"] = self
.title
767 context
["content"] = self
.description
770 context
["license"] = self
.license
773 context
["location"] = self
.get_location
.serialize(request
)
777 comment
.serialize(request
) for comment
in self
.get_comments()]
778 total
= len(comments
)
779 context
["replies"] = {
782 "url": request
.urlgen(
783 "mediagoblin.api.object.comments",
784 object_type
=self
.object_type
,
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
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
)
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)
834 return "<FileKeyname %r: %r>" % (self
.id, self
.name
)
837 def find_or_new(cls
, name
):
838 t
= cls
.query
.filter_by(name
=name
).first()
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),
854 name_id
= Column(SmallInteger
, ForeignKey(FileKeynames
.id), nullable
=False)
855 file_path
= Column(PathTupleWithSlashes
)
856 file_metadata
= Column(MutationDict
.as_mutable(JSONEncoded
))
859 PrimaryKeyConstraint('media_entry', 'name_id'),
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),
878 name
= Column(Unicode
, nullable
=False)
879 filepath
= Column(PathTupleWithSlashes
)
880 created
= Column(DateTime
, nullable
=False, default
=datetime
.datetime
.utcnow
)
884 """A dict like view on this object"""
885 return DictReadAttrProxy(self
)
889 __tablename__
= "core__tags"
891 id = Column(Integer
, primary_key
=True)
892 slug
= Column(Unicode
, nullable
=False, unique
=True)
895 return "<Tag %r: %r>" % (self
.id, self
.slug
)
898 def find_or_new(cls
, slug
):
899 t
= cls
.query
.filter_by(slug
=slug
).first()
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)
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):
930 self
.tag_helper
= Tag
.find_or_new(slug
)
934 """A dict like view on this object"""
935 return DictReadAttrProxy(self
)
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.
955 ForeignKey(GenericModelReference
.id),
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
)
968 ForeignKey(GenericModelReference
.id),
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
)
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",
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?
1014 target
= target
.serialize(request
, show_comments
=False)
1017 author
= self
.get_actor
1018 published
= UTC
.localize(self
.created
)
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(),
1030 context
["location"] = self
.get_location
.seralize(request
)
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
1047 id = extract_url_arguments(
1048 url
=data
["inReplyTo"]["id"],
1049 urlmap
=request
.app
.url_map
1054 public_id
= request
.urlgen(
1055 "mediagoblin.api.object",
1057 object_type
=data
["inReplyTo"]["objectType"],
1061 media
= MediaEntry
.query
.filter_by(public_id
=public_id
).first()
1065 # We need an ID for this model.
1066 self
.save(commit
=False)
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
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
,
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)
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"))
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
1135 order_col
= desc(order_col
)
1136 return CollectionItem
.query
.filter_by(
1137 collection
=self
.id).order_by(order_col
)
1140 safe_title
= self
.title
.encode('ascii', 'replace')
1141 return '<{classname} #{id}: {title} by {actor}>'.format(
1143 classname
=self
.__class
__.__name
__,
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()]
1151 "totalItems": self
.items
,
1152 "url": self
.url_for_self(request
.urlgen
, qualified
=True),
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
,
1170 cascade
="all, delete-orphan"))
1172 # Link to the object (could be anything.
1175 ForeignKey(GenericModelReference
.id),
1179 object_helper
= relationship(
1180 GenericModelReference
,
1181 foreign_keys
=[object_id
]
1183 get_object
= association_proxy(
1186 creator
=GenericModelReference
.find_or_new
1190 UniqueConstraint('collection', 'object_id'),
1194 def dict_view(self
):
1195 """A dict like view on this object"""
1196 return DictReadAttrProxy(self
)
1199 return '<{classname} #{id}: Object {obj} in {collection}>'.format(
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,
1216 media_entry
= relationship(MediaEntry
,
1217 backref
=backref('processing_metadata',
1218 cascade
='all, delete-orphan'))
1219 callback_url
= Column(Unicode
)
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)
1247 return ('<{classname} #{id}: {user} {media} notify: '
1248 '{notify} email: {email}>').format(
1250 classname
=self
.__class
__.__name
__,
1252 media
=self
.media_entry
,
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,
1269 seen
= Column(Boolean
, default
=lambda: False, index
=True)
1270 user
= relationship(
1272 backref
=backref('notifications', cascade
='all, delete-orphan'))
1275 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1277 klass
=self
.__class
__.__name
__,
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(
1285 klass
=self
.__class
__.__name
__,
1287 subject
=getattr(self
, 'subject', None),
1288 seen
='unseen' if not self
.seen
else 'seen')
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
1300 :keyword reported_user_id Holds the id of the user who created
1301 the content which was reported, as
1303 :keyword created Holds a datetime column of when the re-
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
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(
1322 backref
=backref("reports_filed_by",
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(
1330 backref
=backref("reports_filed_on",
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(
1338 backref
=backref("reports_resolved_by",
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:
1357 return isinstance(self
.obj(), TextComment
)
1359 def is_media_entry_report(self
):
1360 if self
.object_id
is None:
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
1377 :keyword user_id Holds the id of the user this object is
1378 attached to. This is a one-to-one
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,
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
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(
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
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'
1436 ForeignKey(User
.id),
1441 ForeignKey(Privilege
.id),
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
1457 return "<{klass} {name}>".format(
1458 klass
=self
.__class
__.__name
__,
1462 def serialize(self
, request
):
1463 href
= request
.urlgen(
1464 "mediagoblin.api.object",
1465 object_type
=self
.object_type
,
1469 published
= UTC
.localize(self
.published
)
1470 updated
= UTC
.localize(self
.updated
)
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"),
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"),
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
1525 if self
.content
is None:
1526 return "<{klass} verb:{verb}>".format(
1527 klass
=self
.__class
__.__name
__,
1531 return "<{klass} {content}>".format(
1532 klass
=self
.__class
__.__name
__,
1533 content
=self
.content
1536 def save(self
, set_updated
=True, *args
, **kwargs
):
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
)
1559 return "<{klass} deleted {obj_type}>".format(
1560 klass
=type(self
).__name
__,
1561 obj_type
=self
.object_type
1564 def serialize(self
, request
):
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
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
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
1635 show_table_init(uri
)