Migrate Activity to using the new GenericForeignKey
[mediagoblin.git] / mediagoblin / db / models.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
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.
8 #
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.
13 #
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/>.
16
17 """
18 TODO: indexes on foreignkeys, where useful.
19 """
20
21 from __future__ import print_function
22
23 import logging
24 import datetime
25
26 from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
27 Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
28 SmallInteger, Date
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
35
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
45
46 import six
47 from pytz import UTC
48
49 _log = logging.getLogger(__name__)
50
51 class GenericModelReference(Base):
52 """
53 Represents a relationship to any model that is defined with a integer pk
54
55 NB: This model should not be used directly but through the GenericForeignKey
56 field provided.
57 """
58 __tablename__ = "core__generic_model_reference"
59
60 id = Column(Integer, primary_key=True)
61 obj_pk = Column(Integer, nullable=False)
62
63 # This will be the tablename of the model
64 model_type = Column(Unicode, nullable=False)
65
66 def get_object(self):
67 # This can happen if it's yet to be saved
68 if self.model_type is None or self.obj_pk is None:
69 return None
70
71 model = self._get_model_from_type(self.model_type)
72 return model.query.filter_by(id=self.obj_pk)
73
74 def set_object(self, obj):
75 model = obj.__class__
76
77 # Check we've been given a object
78 if not issubclass(model, Base):
79 raise ValueError("Only models can be set as GenericForeignKeys")
80
81 # Check that the model has an explicit __tablename__ declaration
82 if getattr(model, "__tablename__", None) is None:
83 raise ValueError("Models must have __tablename__ attribute")
84
85 # Check that it's not a composite primary key
86 primary_keys = [key.name for key in class_mapper(model).primary_key]
87 if len(primary_keys) > 1:
88 raise ValueError("Models can not have composite primary keys")
89
90 # Check that the field on the model is a an integer field
91 pk_column = getattr(model, primary_keys[0])
92 if issubclass(Integer, pk_column):
93 raise ValueError("Only models with integer pks can be set")
94
95 # Ensure that everything has it's ID set
96 obj.save(commit=False)
97
98 self.obj_pk = obj.id
99 self.model_type = obj.__tablename__
100
101 def _get_model_from_type(self, model_type):
102 """ Gets a model from a tablename (model type) """
103 if getattr(self, "_TYPE_MAP", None) is None:
104 # We want to build on the class (not the instance) a map of all the
105 # models by the table name (type) for easy lookup, this is done on
106 # the class so it can be shared between all instances
107
108 # to prevent circular imports do import here
109 self._TYPE_MAP = dict(((m.__tablename__, m) for m in MODELS))
110 setattr(self.__class__._TYPE_MAP, self._TYPE_MAP)
111
112 return self._TYPE_MAP[model_type]
113
114
115 class GenericForeignKey(ForeignKey):
116
117 def __init__(self, *args, **kwargs):
118 super(GenericForeignKey, self).__init__(
119 GenericModelReference.id,
120 *args,
121 **kwargs
122 )
123
124 def __get__(self, *args, **kwargs):
125 """ Looks up GenericModelReference and model for field """
126 # Find the value of the foreign key.
127 ref = super(self, GenericForeignKey).__get__(*args, **kwargs)
128
129 # If this hasn't been set yet return None
130 if ref is None:
131 return None
132
133 # Look up the GenericModelReference for this.
134 gmr = GenericModelReference.query.filter_by(id=ref).first()
135
136 # If it's set to something invalid (i.e. no GMR exists return None)
137 if gmr is None:
138 return None
139
140 # Ask the GMR for the corresponding model
141 return gmr.get_object()
142
143
144 class Location(Base):
145 """ Represents a physical location """
146 __tablename__ = "core__locations"
147
148 id = Column(Integer, primary_key=True)
149 name = Column(Unicode)
150
151 # GPS coordinates
152 position = Column(MutationDict.as_mutable(JSONEncoded))
153 address = Column(MutationDict.as_mutable(JSONEncoded))
154
155 @classmethod
156 def create(cls, data, obj):
157 location = cls()
158 location.unserialize(data)
159 location.save()
160 obj.location = location.id
161 return location
162
163 def serialize(self, request):
164 location = {"objectType": "place"}
165
166 if self.name is not None:
167 location["displayName"] = self.name
168
169 if self.position:
170 location["position"] = self.position
171
172 if self.address:
173 location["address"] = self.address
174
175 return location
176
177 def unserialize(self, data):
178 if "displayName" in data:
179 self.name = data["displayName"]
180
181 self.position = {}
182 self.address = {}
183
184 # nicer way to do this?
185 if "position" in data:
186 # TODO: deal with ISO 9709 formatted string as position
187 if "altitude" in data["position"]:
188 self.position["altitude"] = data["position"]["altitude"]
189
190 if "direction" in data["position"]:
191 self.position["direction"] = data["position"]["direction"]
192
193 if "longitude" in data["position"]:
194 self.position["longitude"] = data["position"]["longitude"]
195
196 if "latitude" in data["position"]:
197 self.position["latitude"] = data["position"]["latitude"]
198
199 if "address" in data:
200 if "formatted" in data["address"]:
201 self.address["formatted"] = data["address"]["formatted"]
202
203 if "streetAddress" in data["address"]:
204 self.address["streetAddress"] = data["address"]["streetAddress"]
205
206 if "locality" in data["address"]:
207 self.address["locality"] = data["address"]["locality"]
208
209 if "region" in data["address"]:
210 self.address["region"] = data["address"]["region"]
211
212 if "postalCode" in data["address"]:
213 self.address["postalCode"] = data["addresss"]["postalCode"]
214
215 if "country" in data["address"]:
216 self.address["country"] = data["address"]["country"]
217
218 class User(Base, UserMixin):
219 """
220 TODO: We should consider moving some rarely used fields
221 into some sort of "shadow" table.
222 """
223 __tablename__ = "core__users"
224
225 id = Column(Integer, primary_key=True)
226 username = Column(Unicode, nullable=False, unique=True)
227 # Note: no db uniqueness constraint on email because it's not
228 # reliable (many email systems case insensitive despite against
229 # the RFC) and because it would be a mess to implement at this
230 # point.
231 email = Column(Unicode, nullable=False)
232 pw_hash = Column(Unicode)
233 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
234 # Intented to be nullable=False, but migrations would not work for it
235 # set to nullable=True implicitly.
236 wants_comment_notification = Column(Boolean, default=True)
237 wants_notifications = Column(Boolean, default=True)
238 license_preference = Column(Unicode)
239 url = Column(Unicode)
240 bio = Column(UnicodeText) # ??
241 uploaded = Column(Integer, default=0)
242 upload_limit = Column(Integer)
243 location = Column(Integer, ForeignKey("core__locations.id"))
244 get_location = relationship("Location", lazy="joined")
245
246 activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
247
248 ## TODO
249 # plugin data would be in a separate model
250
251 def __repr__(self):
252 return '<{0} #{1} {2} {3} "{4}">'.format(
253 self.__class__.__name__,
254 self.id,
255 'verified' if self.has_privilege(u'active') else 'non-verified',
256 'admin' if self.has_privilege(u'admin') else 'user',
257 self.username)
258
259 def delete(self, **kwargs):
260 """Deletes a User and all related entries/comments/files/..."""
261 # Collections get deleted by relationships.
262
263 media_entries = MediaEntry.query.filter(MediaEntry.uploader == self.id)
264 for media in media_entries:
265 # TODO: Make sure that "MediaEntry.delete()" also deletes
266 # all related files/Comments
267 media.delete(del_orphan_tags=False, commit=False)
268
269 # Delete now unused tags
270 # TODO: import here due to cyclic imports!!! This cries for refactoring
271 from mediagoblin.db.util import clean_orphan_tags
272 clean_orphan_tags(commit=False)
273
274 # Delete user, pass through commit=False/True in kwargs
275 super(User, self).delete(**kwargs)
276 _log.info('Deleted user "{0}" account'.format(self.username))
277
278 def has_privilege(self, privilege, allow_admin=True):
279 """
280 This method checks to make sure a user has all the correct privileges
281 to access a piece of content.
282
283 :param privilege A unicode object which represent the different
284 privileges which may give the user access to
285 content.
286
287 :param allow_admin If this is set to True the then if the user is
288 an admin, then this will always return True
289 even if the user hasn't been given the
290 privilege. (defaults to True)
291 """
292 priv = Privilege.query.filter_by(privilege_name=privilege).one()
293 if priv in self.all_privileges:
294 return True
295 elif allow_admin and self.has_privilege(u'admin', allow_admin=False):
296 return True
297
298 return False
299
300 def is_banned(self):
301 """
302 Checks if this user is banned.
303
304 :returns True if self is banned
305 :returns False if self is not
306 """
307 return UserBan.query.get(self.id) is not None
308
309
310 def serialize(self, request):
311 published = UTC.localize(self.created)
312 user = {
313 "id": "acct:{0}@{1}".format(self.username, request.host),
314 "published": published.isoformat(),
315 "preferredUsername": self.username,
316 "displayName": "{0}@{1}".format(self.username, request.host),
317 "objectType": self.object_type,
318 "pump_io": {
319 "shared": False,
320 "followed": False,
321 },
322 "links": {
323 "self": {
324 "href": request.urlgen(
325 "mediagoblin.api.user.profile",
326 username=self.username,
327 qualified=True
328 ),
329 },
330 "activity-inbox": {
331 "href": request.urlgen(
332 "mediagoblin.api.inbox",
333 username=self.username,
334 qualified=True
335 )
336 },
337 "activity-outbox": {
338 "href": request.urlgen(
339 "mediagoblin.api.feed",
340 username=self.username,
341 qualified=True
342 )
343 },
344 },
345 }
346
347 if self.bio:
348 user.update({"summary": self.bio})
349 if self.url:
350 user.update({"url": self.url})
351 if self.location:
352 user.update({"location": self.get_location.serialize(request)})
353
354 return user
355
356 def unserialize(self, data):
357 if "summary" in data:
358 self.bio = data["summary"]
359
360 if "location" in data:
361 Location.create(data, self)
362
363 class Client(Base):
364 """
365 Model representing a client - Used for API Auth
366 """
367 __tablename__ = "core__clients"
368
369 id = Column(Unicode, nullable=True, primary_key=True)
370 secret = Column(Unicode, nullable=False)
371 expirey = Column(DateTime, nullable=True)
372 application_type = Column(Unicode, nullable=False)
373 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
374 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
375
376 # optional stuff
377 redirect_uri = Column(JSONEncoded, nullable=True)
378 logo_url = Column(Unicode, nullable=True)
379 application_name = Column(Unicode, nullable=True)
380 contacts = Column(JSONEncoded, nullable=True)
381
382 def __repr__(self):
383 if self.application_name:
384 return "<Client {0} - {1}>".format(self.application_name, self.id)
385 else:
386 return "<Client {0}>".format(self.id)
387
388 class RequestToken(Base):
389 """
390 Model for representing the request tokens
391 """
392 __tablename__ = "core__request_tokens"
393
394 token = Column(Unicode, primary_key=True)
395 secret = Column(Unicode, nullable=False)
396 client = Column(Unicode, ForeignKey(Client.id))
397 user = Column(Integer, ForeignKey(User.id), nullable=True)
398 used = Column(Boolean, default=False)
399 authenticated = Column(Boolean, default=False)
400 verifier = Column(Unicode, nullable=True)
401 callback = Column(Unicode, nullable=False, default=u"oob")
402 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
403 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
404
405 get_client = relationship(Client)
406
407 class AccessToken(Base):
408 """
409 Model for representing the access tokens
410 """
411 __tablename__ = "core__access_tokens"
412
413 token = Column(Unicode, nullable=False, primary_key=True)
414 secret = Column(Unicode, nullable=False)
415 user = Column(Integer, ForeignKey(User.id))
416 request_token = Column(Unicode, ForeignKey(RequestToken.token))
417 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
418 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
419
420 get_requesttoken = relationship(RequestToken)
421
422
423 class NonceTimestamp(Base):
424 """
425 A place the timestamp and nonce can be stored - this is for OAuth1
426 """
427 __tablename__ = "core__nonce_timestamps"
428
429 nonce = Column(Unicode, nullable=False, primary_key=True)
430 timestamp = Column(DateTime, nullable=False, primary_key=True)
431
432 class MediaEntry(Base, MediaEntryMixin):
433 """
434 TODO: Consider fetching the media_files using join
435 """
436 __tablename__ = "core__media_entries"
437
438 id = Column(Integer, primary_key=True)
439 uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
440 title = Column(Unicode, nullable=False)
441 slug = Column(Unicode)
442 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow,
443 index=True)
444 description = Column(UnicodeText) # ??
445 media_type = Column(Unicode, nullable=False)
446 state = Column(Unicode, default=u'unprocessed', nullable=False)
447 # or use sqlalchemy.types.Enum?
448 license = Column(Unicode)
449 file_size = Column(Integer, default=0)
450 location = Column(Integer, ForeignKey("core__locations.id"))
451 get_location = relationship("Location", lazy="joined")
452
453 fail_error = Column(Unicode)
454 fail_metadata = Column(JSONEncoded)
455
456 transcoding_progress = Column(SmallInteger)
457
458 queued_media_file = Column(PathTupleWithSlashes)
459
460 queued_task_id = Column(Unicode)
461
462 __table_args__ = (
463 UniqueConstraint('uploader', 'slug'),
464 {})
465
466 get_uploader = relationship(User)
467
468 media_files_helper = relationship("MediaFile",
469 collection_class=attribute_mapped_collection("name"),
470 cascade="all, delete-orphan"
471 )
472 media_files = association_proxy('media_files_helper', 'file_path',
473 creator=lambda k, v: MediaFile(name=k, file_path=v)
474 )
475
476 attachment_files_helper = relationship("MediaAttachmentFile",
477 cascade="all, delete-orphan",
478 order_by="MediaAttachmentFile.created"
479 )
480 attachment_files = association_proxy("attachment_files_helper", "dict_view",
481 creator=lambda v: MediaAttachmentFile(
482 name=v["name"], filepath=v["filepath"])
483 )
484
485 tags_helper = relationship("MediaTag",
486 cascade="all, delete-orphan" # should be automatically deleted
487 )
488 tags = association_proxy("tags_helper", "dict_view",
489 creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
490 )
491
492 collections_helper = relationship("CollectionItem",
493 cascade="all, delete-orphan"
494 )
495 collections = association_proxy("collections_helper", "in_collection")
496 media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
497 default=MutationDict())
498
499 activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
500
501 ## TODO
502 # fail_error
503
504 def get_comments(self, ascending=False):
505 order_col = MediaComment.created
506 if not ascending:
507 order_col = desc(order_col)
508 return self.all_comments.order_by(order_col)
509
510 def url_to_prev(self, urlgen):
511 """get the next 'newer' entry by this user"""
512 media = MediaEntry.query.filter(
513 (MediaEntry.uploader == self.uploader)
514 & (MediaEntry.state == u'processed')
515 & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()
516
517 if media is not None:
518 return media.url_for_self(urlgen)
519
520 def url_to_next(self, urlgen):
521 """get the next 'older' entry by this user"""
522 media = MediaEntry.query.filter(
523 (MediaEntry.uploader == self.uploader)
524 & (MediaEntry.state == u'processed')
525 & (MediaEntry.id < self.id)).order_by(desc(MediaEntry.id)).first()
526
527 if media is not None:
528 return media.url_for_self(urlgen)
529
530 def get_file_metadata(self, file_key, metadata_key=None):
531 """
532 Return the file_metadata dict of a MediaFile. If metadata_key is given,
533 return the value of the key.
534 """
535 media_file = MediaFile.query.filter_by(media_entry=self.id,
536 name=six.text_type(file_key)).first()
537
538 if media_file:
539 if metadata_key:
540 return media_file.file_metadata.get(metadata_key, None)
541
542 return media_file.file_metadata
543
544 def set_file_metadata(self, file_key, **kwargs):
545 """
546 Update the file_metadata of a MediaFile.
547 """
548 media_file = MediaFile.query.filter_by(media_entry=self.id,
549 name=six.text_type(file_key)).first()
550
551 file_metadata = media_file.file_metadata or {}
552
553 for key, value in six.iteritems(kwargs):
554 file_metadata[key] = value
555
556 media_file.file_metadata = file_metadata
557 media_file.save()
558
559 @property
560 def media_data(self):
561 return getattr(self, self.media_data_ref)
562
563 def media_data_init(self, **kwargs):
564 """
565 Initialize or update the contents of a media entry's media_data row
566 """
567 media_data = self.media_data
568
569 if media_data is None:
570 # Get the correct table:
571 table = import_component(self.media_type + '.models:DATA_MODEL')
572 # No media data, so actually add a new one
573 media_data = table(**kwargs)
574 # Get the relationship set up.
575 media_data.get_media_entry = self
576 else:
577 # Update old media data
578 for field, value in six.iteritems(kwargs):
579 setattr(media_data, field, value)
580
581 @memoized_property
582 def media_data_ref(self):
583 return import_component(self.media_type + '.models:BACKREF_NAME')
584
585 def __repr__(self):
586 if six.PY2:
587 # obj.__repr__() should return a str on Python 2
588 safe_title = self.title.encode('utf-8', 'replace')
589 else:
590 safe_title = self.title
591
592 return '<{classname} {id}: {title}>'.format(
593 classname=self.__class__.__name__,
594 id=self.id,
595 title=safe_title)
596
597 def delete(self, del_orphan_tags=True, **kwargs):
598 """Delete MediaEntry and all related files/attachments/comments
599
600 This will *not* automatically delete unused collections, which
601 can remain empty...
602
603 :param del_orphan_tags: True/false if we delete unused Tags too
604 :param commit: True/False if this should end the db transaction"""
605 # User's CollectionItems are automatically deleted via "cascade".
606 # Comments on this Media are deleted by cascade, hopefully.
607
608 # Delete all related files/attachments
609 try:
610 delete_media_files(self)
611 except OSError as error:
612 # Returns list of files we failed to delete
613 _log.error('No such files from the user "{1}" to delete: '
614 '{0}'.format(str(error), self.get_uploader))
615 _log.info('Deleted Media entry id "{0}"'.format(self.id))
616 # Related MediaTag's are automatically cleaned, but we might
617 # want to clean out unused Tag's too.
618 if del_orphan_tags:
619 # TODO: Import here due to cyclic imports!!!
620 # This cries for refactoring
621 from mediagoblin.db.util import clean_orphan_tags
622 clean_orphan_tags(commit=False)
623 # pass through commit=False/True in kwargs
624 super(MediaEntry, self).delete(**kwargs)
625
626 def serialize(self, request, show_comments=True):
627 """ Unserialize MediaEntry to object """
628 href = request.urlgen(
629 "mediagoblin.api.object",
630 object_type=self.object_type,
631 id=self.id,
632 qualified=True
633 )
634 author = self.get_uploader
635 published = UTC.localize(self.created)
636 updated = UTC.localize(self.created)
637 context = {
638 "id": href,
639 "author": author.serialize(request),
640 "objectType": self.object_type,
641 "url": self.url_for_self(request.urlgen, qualified=True),
642 "image": {
643 "url": request.host_url + self.thumb_url[1:],
644 },
645 "fullImage":{
646 "url": request.host_url + self.original_url[1:],
647 },
648 "published": published.isoformat(),
649 "updated": updated.isoformat(),
650 "pump_io": {
651 "shared": False,
652 },
653 "links": {
654 "self": {
655 "href": href,
656 },
657
658 }
659 }
660
661 if self.title:
662 context["displayName"] = self.title
663
664 if self.description:
665 context["content"] = self.description
666
667 if self.license:
668 context["license"] = self.license
669
670 if self.location:
671 context["location"] = self.get_location.serialize(request)
672
673 if show_comments:
674 comments = [
675 comment.serialize(request) for comment in self.get_comments()]
676 total = len(comments)
677 context["replies"] = {
678 "totalItems": total,
679 "items": comments,
680 "url": request.urlgen(
681 "mediagoblin.api.object.comments",
682 object_type=self.object_type,
683 id=self.id,
684 qualified=True
685 ),
686 }
687
688 # Add image height and width if possible. We didn't use to store this
689 # data and we're not able (and maybe not willing) to re-process all
690 # images so it's possible this might not exist.
691 if self.get_file_metadata("thumb", "height"):
692 height = self.get_file_metadata("thumb", "height")
693 context["image"]["height"] = height
694 if self.get_file_metadata("thumb", "width"):
695 width = self.get_file_metadata("thumb", "width")
696 context["image"]["width"] = width
697 if self.get_file_metadata("original", "height"):
698 height = self.get_file_metadata("original", "height")
699 context["fullImage"]["height"] = height
700 if self.get_file_metadata("original", "height"):
701 width = self.get_file_metadata("original", "width")
702 context["fullImage"]["width"] = width
703
704 return context
705
706 def unserialize(self, data):
707 """ Takes API objects and unserializes on existing MediaEntry """
708 if "displayName" in data:
709 self.title = data["displayName"]
710
711 if "content" in data:
712 self.description = data["content"]
713
714 if "license" in data:
715 self.license = data["license"]
716
717 if "location" in data:
718 Licence.create(data["location"], self)
719
720 return True
721
722 class FileKeynames(Base):
723 """
724 keywords for various places.
725 currently the MediaFile keys
726 """
727 __tablename__ = "core__file_keynames"
728 id = Column(Integer, primary_key=True)
729 name = Column(Unicode, unique=True)
730
731 def __repr__(self):
732 return "<FileKeyname %r: %r>" % (self.id, self.name)
733
734 @classmethod
735 def find_or_new(cls, name):
736 t = cls.query.filter_by(name=name).first()
737 if t is not None:
738 return t
739 return cls(name=name)
740
741
742 class MediaFile(Base):
743 """
744 TODO: Highly consider moving "name" into a new table.
745 TODO: Consider preloading said table in software
746 """
747 __tablename__ = "core__mediafiles"
748
749 media_entry = Column(
750 Integer, ForeignKey(MediaEntry.id),
751 nullable=False)
752 name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
753 file_path = Column(PathTupleWithSlashes)
754 file_metadata = Column(MutationDict.as_mutable(JSONEncoded))
755
756 __table_args__ = (
757 PrimaryKeyConstraint('media_entry', 'name_id'),
758 {})
759
760 def __repr__(self):
761 return "<MediaFile %s: %r>" % (self.name, self.file_path)
762
763 name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
764 name = association_proxy('name_helper', 'name',
765 creator=FileKeynames.find_or_new
766 )
767
768
769 class MediaAttachmentFile(Base):
770 __tablename__ = "core__attachment_files"
771
772 id = Column(Integer, primary_key=True)
773 media_entry = Column(
774 Integer, ForeignKey(MediaEntry.id),
775 nullable=False)
776 name = Column(Unicode, nullable=False)
777 filepath = Column(PathTupleWithSlashes)
778 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
779
780 @property
781 def dict_view(self):
782 """A dict like view on this object"""
783 return DictReadAttrProxy(self)
784
785
786 class Tag(Base):
787 __tablename__ = "core__tags"
788
789 id = Column(Integer, primary_key=True)
790 slug = Column(Unicode, nullable=False, unique=True)
791
792 def __repr__(self):
793 return "<Tag %r: %r>" % (self.id, self.slug)
794
795 @classmethod
796 def find_or_new(cls, slug):
797 t = cls.query.filter_by(slug=slug).first()
798 if t is not None:
799 return t
800 return cls(slug=slug)
801
802
803 class MediaTag(Base):
804 __tablename__ = "core__media_tags"
805
806 id = Column(Integer, primary_key=True)
807 media_entry = Column(
808 Integer, ForeignKey(MediaEntry.id),
809 nullable=False, index=True)
810 tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
811 name = Column(Unicode)
812 # created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
813
814 __table_args__ = (
815 UniqueConstraint('tag', 'media_entry'),
816 {})
817
818 tag_helper = relationship(Tag)
819 slug = association_proxy('tag_helper', 'slug',
820 creator=Tag.find_or_new
821 )
822
823 def __init__(self, name=None, slug=None):
824 Base.__init__(self)
825 if name is not None:
826 self.name = name
827 if slug is not None:
828 self.tag_helper = Tag.find_or_new(slug)
829
830 @property
831 def dict_view(self):
832 """A dict like view on this object"""
833 return DictReadAttrProxy(self)
834
835
836 class MediaComment(Base, MediaCommentMixin):
837 __tablename__ = "core__media_comments"
838
839 id = Column(Integer, primary_key=True)
840 media_entry = Column(
841 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
842 author = Column(Integer, ForeignKey(User.id), nullable=False)
843 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
844 content = Column(UnicodeText, nullable=False)
845 location = Column(Integer, ForeignKey("core__locations.id"))
846 get_location = relationship("Location", lazy="joined")
847
848 # Cascade: Comments are owned by their creator. So do the full thing.
849 # lazy=dynamic: People might post a *lot* of comments,
850 # so make the "posted_comments" a query-like thing.
851 get_author = relationship(User,
852 backref=backref("posted_comments",
853 lazy="dynamic",
854 cascade="all, delete-orphan"))
855 get_entry = relationship(MediaEntry,
856 backref=backref("comments",
857 lazy="dynamic",
858 cascade="all, delete-orphan"))
859
860 # Cascade: Comments are somewhat owned by their MediaEntry.
861 # So do the full thing.
862 # lazy=dynamic: MediaEntries might have many comments,
863 # so make the "all_comments" a query-like thing.
864 get_media_entry = relationship(MediaEntry,
865 backref=backref("all_comments",
866 lazy="dynamic",
867 cascade="all, delete-orphan"))
868
869
870 activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
871
872 def serialize(self, request):
873 """ Unserialize to python dictionary for API """
874 href = request.urlgen(
875 "mediagoblin.api.object",
876 object_type=self.object_type,
877 id=self.id,
878 qualified=True
879 )
880 media = MediaEntry.query.filter_by(id=self.media_entry).first()
881 author = self.get_author
882 published = UTC.localize(self.created)
883 context = {
884 "id": href,
885 "objectType": self.object_type,
886 "content": self.content,
887 "inReplyTo": media.serialize(request, show_comments=False),
888 "author": author.serialize(request),
889 "published": published.isoformat(),
890 "updated": published.isoformat(),
891 }
892
893 if self.location:
894 context["location"] = self.get_location.seralize(request)
895
896 return context
897
898 def unserialize(self, data, request):
899 """ Takes API objects and unserializes on existing comment """
900 # Handle changing the reply ID
901 if "inReplyTo" in data:
902 # Validate that the ID is correct
903 try:
904 media_id = int(extract_url_arguments(
905 url=data["inReplyTo"]["id"],
906 urlmap=request.app.url_map
907 )["id"])
908 except ValueError:
909 return False
910
911 media = MediaEntry.query.filter_by(id=media_id).first()
912 if media is None:
913 return False
914
915 self.media_entry = media.id
916
917 if "content" in data:
918 self.content = data["content"]
919
920 if "location" in data:
921 Location.create(data["location"], self)
922
923 return True
924
925
926
927 class Collection(Base, CollectionMixin):
928 """An 'album' or 'set' of media by a user.
929
930 On deletion, contained CollectionItems get automatically reaped via
931 SQL cascade"""
932 __tablename__ = "core__collections"
933
934 id = Column(Integer, primary_key=True)
935 title = Column(Unicode, nullable=False)
936 slug = Column(Unicode)
937 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow,
938 index=True)
939 description = Column(UnicodeText)
940 creator = Column(Integer, ForeignKey(User.id), nullable=False)
941 location = Column(Integer, ForeignKey("core__locations.id"))
942 get_location = relationship("Location", lazy="joined")
943
944 # TODO: No of items in Collection. Badly named, can we migrate to num_items?
945 items = Column(Integer, default=0)
946
947 # Cascade: Collections are owned by their creator. So do the full thing.
948 get_creator = relationship(User,
949 backref=backref("collections",
950 cascade="all, delete-orphan"))
951
952 activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
953
954 __table_args__ = (
955 UniqueConstraint('creator', 'slug'),
956 {})
957
958 def get_collection_items(self, ascending=False):
959 #TODO, is this still needed with self.collection_items being available?
960 order_col = CollectionItem.position
961 if not ascending:
962 order_col = desc(order_col)
963 return CollectionItem.query.filter_by(
964 collection=self.id).order_by(order_col)
965
966 def __repr__(self):
967 safe_title = self.title.encode('ascii', 'replace')
968 return '<{classname} #{id}: {title} by {creator}>'.format(
969 id=self.id,
970 classname=self.__class__.__name__,
971 creator=self.creator,
972 title=safe_title)
973
974 def serialize(self, request):
975 # Get all serialized output in a list
976 items = []
977 for item in self.get_collection_items():
978 items.append(item.serialize(request))
979
980 return {
981 "totalItems": self.items,
982 "url": self.url_for_self(request.urlgen, qualified=True),
983 "items": items,
984 }
985
986
987 class CollectionItem(Base, CollectionItemMixin):
988 __tablename__ = "core__collection_items"
989
990 id = Column(Integer, primary_key=True)
991 media_entry = Column(
992 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
993 collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
994 note = Column(UnicodeText, nullable=True)
995 added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
996 position = Column(Integer)
997
998 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
999 in_collection = relationship(Collection,
1000 backref=backref(
1001 "collection_items",
1002 cascade="all, delete-orphan"))
1003
1004 get_media_entry = relationship(MediaEntry)
1005
1006 __table_args__ = (
1007 UniqueConstraint('collection', 'media_entry'),
1008 {})
1009
1010 @property
1011 def dict_view(self):
1012 """A dict like view on this object"""
1013 return DictReadAttrProxy(self)
1014
1015 def __repr__(self):
1016 return '<{classname} #{id}: Entry {entry} in {collection}>'.format(
1017 id=self.id,
1018 classname=self.__class__.__name__,
1019 collection=self.collection,
1020 entry=self.media_entry)
1021
1022 def serialize(self, request):
1023 return self.get_media_entry.serialize(request)
1024
1025
1026 class ProcessingMetaData(Base):
1027 __tablename__ = 'core__processing_metadata'
1028
1029 id = Column(Integer, primary_key=True)
1030 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
1031 index=True)
1032 media_entry = relationship(MediaEntry,
1033 backref=backref('processing_metadata',
1034 cascade='all, delete-orphan'))
1035 callback_url = Column(Unicode)
1036
1037 @property
1038 def dict_view(self):
1039 """A dict like view on this object"""
1040 return DictReadAttrProxy(self)
1041
1042
1043 class CommentSubscription(Base):
1044 __tablename__ = 'core__comment_subscriptions'
1045 id = Column(Integer, primary_key=True)
1046
1047 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1048
1049 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
1050 media_entry = relationship(MediaEntry,
1051 backref=backref('comment_subscriptions',
1052 cascade='all, delete-orphan'))
1053
1054 user_id = Column(Integer, ForeignKey(User.id), nullable=False)
1055 user = relationship(User,
1056 backref=backref('comment_subscriptions',
1057 cascade='all, delete-orphan'))
1058
1059 notify = Column(Boolean, nullable=False, default=True)
1060 send_email = Column(Boolean, nullable=False, default=True)
1061
1062 def __repr__(self):
1063 return ('<{classname} #{id}: {user} {media} notify: '
1064 '{notify} email: {email}>').format(
1065 id=self.id,
1066 classname=self.__class__.__name__,
1067 user=self.user,
1068 media=self.media_entry,
1069 notify=self.notify,
1070 email=self.send_email)
1071
1072
1073 class Notification(Base):
1074 __tablename__ = 'core__notifications'
1075 id = Column(Integer, primary_key=True)
1076 type = Column(Unicode)
1077
1078 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1079
1080 user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False,
1081 index=True)
1082 seen = Column(Boolean, default=lambda: False, index=True)
1083 user = relationship(
1084 User,
1085 backref=backref('notifications', cascade='all, delete-orphan'))
1086
1087 __mapper_args__ = {
1088 'polymorphic_identity': 'notification',
1089 'polymorphic_on': type
1090 }
1091
1092 def __repr__(self):
1093 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1094 id=self.id,
1095 klass=self.__class__.__name__,
1096 user=self.user,
1097 subject=getattr(self, 'subject', None),
1098 seen='unseen' if not self.seen else 'seen')
1099
1100 def __unicode__(self):
1101 return u'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1102 id=self.id,
1103 klass=self.__class__.__name__,
1104 user=self.user,
1105 subject=getattr(self, 'subject', None),
1106 seen='unseen' if not self.seen else 'seen')
1107
1108
1109 class CommentNotification(Notification):
1110 __tablename__ = 'core__comment_notifications'
1111 id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
1112
1113 subject_id = Column(Integer, ForeignKey(MediaComment.id))
1114 subject = relationship(
1115 MediaComment,
1116 backref=backref('comment_notifications', cascade='all, delete-orphan'))
1117
1118 __mapper_args__ = {
1119 'polymorphic_identity': 'comment_notification'
1120 }
1121
1122
1123 class ProcessingNotification(Notification):
1124 __tablename__ = 'core__processing_notifications'
1125
1126 id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
1127
1128 subject_id = Column(Integer, ForeignKey(MediaEntry.id))
1129 subject = relationship(
1130 MediaEntry,
1131 backref=backref('processing_notifications',
1132 cascade='all, delete-orphan'))
1133
1134 __mapper_args__ = {
1135 'polymorphic_identity': 'processing_notification'
1136 }
1137
1138 # the with_polymorphic call has been moved to the bottom above MODELS
1139 # this is because it causes conflicts with relationship calls.
1140
1141 class ReportBase(Base):
1142 """
1143 This is the basic report object which the other reports are based off of.
1144
1145 :keyword reporter_id Holds the id of the user who created
1146 the report, as an Integer column.
1147 :keyword report_content Hold the explanation left by the repor-
1148 -ter to indicate why they filed the
1149 report in the first place, as a
1150 Unicode column.
1151 :keyword reported_user_id Holds the id of the user who created
1152 the content which was reported, as
1153 an Integer column.
1154 :keyword created Holds a datetime column of when the re-
1155 -port was filed.
1156 :keyword discriminator This column distinguishes between the
1157 different types of reports.
1158 :keyword resolver_id Holds the id of the moderator/admin who
1159 resolved the report.
1160 :keyword resolved Holds the DateTime object which descri-
1161 -bes when this report was resolved
1162 :keyword result Holds the UnicodeText column of the
1163 resolver's reasons for resolving
1164 the report this way. Some of this
1165 is auto-generated
1166 """
1167 __tablename__ = 'core__reports'
1168 id = Column(Integer, primary_key=True)
1169 reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
1170 reporter = relationship(
1171 User,
1172 backref=backref("reports_filed_by",
1173 lazy="dynamic",
1174 cascade="all, delete-orphan"),
1175 primaryjoin="User.id==ReportBase.reporter_id")
1176 report_content = Column(UnicodeText)
1177 reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
1178 reported_user = relationship(
1179 User,
1180 backref=backref("reports_filed_on",
1181 lazy="dynamic",
1182 cascade="all, delete-orphan"),
1183 primaryjoin="User.id==ReportBase.reported_user_id")
1184 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1185 discriminator = Column('type', Unicode(50))
1186 resolver_id = Column(Integer, ForeignKey(User.id))
1187 resolver = relationship(
1188 User,
1189 backref=backref("reports_resolved_by",
1190 lazy="dynamic",
1191 cascade="all, delete-orphan"),
1192 primaryjoin="User.id==ReportBase.resolver_id")
1193
1194 resolved = Column(DateTime)
1195 result = Column(UnicodeText)
1196 __mapper_args__ = {'polymorphic_on': discriminator}
1197
1198 def is_comment_report(self):
1199 return self.discriminator=='comment_report'
1200
1201 def is_media_entry_report(self):
1202 return self.discriminator=='media_report'
1203
1204 def is_archived_report(self):
1205 return self.resolved is not None
1206
1207 def archive(self,resolver_id, resolved, result):
1208 self.resolver_id = resolver_id
1209 self.resolved = resolved
1210 self.result = result
1211
1212
1213 class CommentReport(ReportBase):
1214 """
1215 Reports that have been filed on comments.
1216 :keyword comment_id Holds the integer value of the reported
1217 comment's ID
1218 """
1219 __tablename__ = 'core__reports_on_comments'
1220 __mapper_args__ = {'polymorphic_identity': 'comment_report'}
1221
1222 id = Column('id',Integer, ForeignKey('core__reports.id'),
1223 primary_key=True)
1224 comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=True)
1225 comment = relationship(
1226 MediaComment, backref=backref("reports_filed_on",
1227 lazy="dynamic"))
1228
1229
1230 class MediaReport(ReportBase):
1231 """
1232 Reports that have been filed on media entries
1233 :keyword media_entry_id Holds the integer value of the reported
1234 media entry's ID
1235 """
1236 __tablename__ = 'core__reports_on_media'
1237 __mapper_args__ = {'polymorphic_identity': 'media_report'}
1238
1239 id = Column('id',Integer, ForeignKey('core__reports.id'),
1240 primary_key=True)
1241 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=True)
1242 media_entry = relationship(
1243 MediaEntry,
1244 backref=backref("reports_filed_on",
1245 lazy="dynamic"))
1246
1247 class UserBan(Base):
1248 """
1249 Holds the information on a specific user's ban-state. As long as one of
1250 these is attached to a user, they are banned from accessing mediagoblin.
1251 When they try to log in, they are greeted with a page that tells them
1252 the reason why they are banned and when (if ever) the ban will be
1253 lifted
1254
1255 :keyword user_id Holds the id of the user this object is
1256 attached to. This is a one-to-one
1257 relationship.
1258 :keyword expiration_date Holds the date that the ban will be lifted.
1259 If this is null, the ban is permanent
1260 unless a moderator manually lifts it.
1261 :keyword reason Holds the reason why the user was banned.
1262 """
1263 __tablename__ = 'core__user_bans'
1264
1265 user_id = Column(Integer, ForeignKey(User.id), nullable=False,
1266 primary_key=True)
1267 expiration_date = Column(Date)
1268 reason = Column(UnicodeText, nullable=False)
1269
1270
1271 class Privilege(Base):
1272 """
1273 The Privilege table holds all of the different privileges a user can hold.
1274 If a user 'has' a privilege, the User object is in a relationship with the
1275 privilege object.
1276
1277 :keyword privilege_name Holds a unicode object that is the recognizable
1278 name of this privilege. This is the column
1279 used for identifying whether or not a user
1280 has a necessary privilege or not.
1281
1282 """
1283 __tablename__ = 'core__privileges'
1284
1285 id = Column(Integer, nullable=False, primary_key=True)
1286 privilege_name = Column(Unicode, nullable=False, unique=True)
1287 all_users = relationship(
1288 User,
1289 backref='all_privileges',
1290 secondary="core__privileges_users")
1291
1292 def __init__(self, privilege_name):
1293 '''
1294 Currently consructors are required for tables that are initialized thru
1295 the FOUNDATIONS system. This is because they need to be able to be con-
1296 -structed by a list object holding their arg*s
1297 '''
1298 self.privilege_name = privilege_name
1299
1300 def __repr__(self):
1301 return "<Privilege %s>" % (self.privilege_name)
1302
1303
1304 class PrivilegeUserAssociation(Base):
1305 '''
1306 This table holds the many-to-many relationship between User and Privilege
1307 '''
1308
1309 __tablename__ = 'core__privileges_users'
1310
1311 user = Column(
1312 "user",
1313 Integer,
1314 ForeignKey(User.id),
1315 primary_key=True)
1316 privilege = Column(
1317 "privilege",
1318 Integer,
1319 ForeignKey(Privilege.id),
1320 primary_key=True)
1321
1322 class Generator(Base):
1323 """ Information about what created an activity """
1324 __tablename__ = "core__generators"
1325
1326 id = Column(Integer, primary_key=True)
1327 name = Column(Unicode, nullable=False)
1328 published = Column(DateTime, default=datetime.datetime.utcnow)
1329 updated = Column(DateTime, default=datetime.datetime.utcnow)
1330 object_type = Column(Unicode, nullable=False)
1331
1332 def __repr__(self):
1333 return "<{klass} {name}>".format(
1334 klass=self.__class__.__name__,
1335 name=self.name
1336 )
1337
1338 def serialize(self, request):
1339 href = request.urlgen(
1340 "mediagoblin.api.object",
1341 object_type=self.object_type,
1342 id=self.id,
1343 qualified=True
1344 )
1345 published = UTC.localize(self.published)
1346 updated = UTC.localize(self.updated)
1347 return {
1348 "id": href,
1349 "displayName": self.name,
1350 "published": published.isoformat(),
1351 "updated": updated.isoformat(),
1352 "objectType": self.object_type,
1353 }
1354
1355 def unserialize(self, data):
1356 if "displayName" in data:
1357 self.name = data["displayName"]
1358
1359
1360 class ActivityIntermediator(Base):
1361 """
1362 This is used so that objects/targets can have a foreign key back to this
1363 object and activities can a foreign key to this object. This objects to be
1364 used multiple times for the activity object or target and also allows for
1365 different types of objects to be used as an Activity.
1366 """
1367 __tablename__ = "core__activity_intermediators"
1368
1369 id = Column(Integer, primary_key=True)
1370 type = Column(Unicode, nullable=False)
1371
1372 TYPES = {
1373 "user": User,
1374 "media": MediaEntry,
1375 "comment": MediaComment,
1376 "collection": Collection,
1377 }
1378
1379 def _find_model(self, obj):
1380 """ Finds the model for a given object """
1381 for key, model in self.TYPES.items():
1382 if isinstance(obj, model):
1383 return key, model
1384
1385 return None, None
1386
1387 def set(self, obj):
1388 """ This sets itself as the activity """
1389 key, model = self._find_model(obj)
1390 if key is None:
1391 raise ValueError("Invalid type of object given")
1392
1393 self.type = key
1394
1395 # We need to populate the self.id so we need to save but, we don't
1396 # want to save this AI in the database (yet) so commit=False.
1397 self.save(commit=False)
1398 obj.activity = self.id
1399 obj.save()
1400
1401 def get(self):
1402 """ Finds the object for an activity """
1403 if self.type is None:
1404 return None
1405
1406 model = self.TYPES[self.type]
1407 return model.query.filter_by(activity=self.id).first()
1408
1409 @validates("type")
1410 def validate_type(self, key, value):
1411 """ Validate that the type set is a valid type """
1412 assert value in self.TYPES
1413 return value
1414
1415 class Activity(Base, ActivityMixin):
1416 """
1417 This holds all the metadata about an activity such as uploading an image,
1418 posting a comment, etc.
1419 """
1420 __tablename__ = "core__activities"
1421
1422 id = Column(Integer, primary_key=True)
1423 actor = Column(Integer,
1424 ForeignKey("core__users.id"),
1425 nullable=False)
1426 published = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1427 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1428 verb = Column(Unicode, nullable=False)
1429 content = Column(Unicode, nullable=True)
1430 title = Column(Unicode, nullable=True)
1431 generator = Column(Integer,
1432 ForeignKey("core__generators.id"),
1433 nullable=True)
1434 object = Column(Integer,
1435 GenericForeignKey(),
1436 nullable=False)
1437 target = Column(Integer,
1438 GenericForeignKey(),
1439 nullable=True)
1440
1441 get_actor = relationship(User,
1442 backref=backref("activities",
1443 cascade="all, delete-orphan"))
1444 get_generator = relationship(Generator)
1445
1446 def __repr__(self):
1447 if self.content is None:
1448 return "<{klass} verb:{verb}>".format(
1449 klass=self.__class__.__name__,
1450 verb=self.verb
1451 )
1452 else:
1453 return "<{klass} {content}>".format(
1454 klass=self.__class__.__name__,
1455 content=self.content
1456 )
1457
1458 def save(self, set_updated=True, *args, **kwargs):
1459 if set_updated:
1460 self.updated = datetime.datetime.now()
1461 super(Activity, self).save(*args, **kwargs)
1462
1463 with_polymorphic(
1464 Notification,
1465 [ProcessingNotification, CommentNotification])
1466
1467 MODELS = [
1468 User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
1469 MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
1470 Notification, CommentNotification, ProcessingNotification, Client,
1471 CommentSubscription, ReportBase, CommentReport, MediaReport, UserBan,
1472 Privilege, PrivilegeUserAssociation,
1473 RequestToken, AccessToken, NonceTimestamp,
1474 Activity, ActivityIntermediator, Generator,
1475 Location, GenericModelReference]
1476
1477 """
1478 Foundations are the default rows that are created immediately after the tables
1479 are initialized. Each entry to this dictionary should be in the format of:
1480 ModelConstructorObject:List of Dictionaries
1481 (Each Dictionary represents a row on the Table to be created, containing each
1482 of the columns' names as a key string, and each of the columns' values as a
1483 value)
1484
1485 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1486 user_foundations = [{'name':u'Joanna', 'age':24},
1487 {'name':u'Andrea', 'age':41}]
1488
1489 FOUNDATIONS = {User:user_foundations}
1490 """
1491 privilege_foundations = [{'privilege_name':u'admin'},
1492 {'privilege_name':u'moderator'},
1493 {'privilege_name':u'uploader'},
1494 {'privilege_name':u'reporter'},
1495 {'privilege_name':u'commenter'},
1496 {'privilege_name':u'active'}]
1497 FOUNDATIONS = {Privilege:privilege_foundations}
1498
1499 ######################################################
1500 # Special, migrations-tracking table
1501 #
1502 # Not listed in MODELS because this is special and not
1503 # really migrated, but used for migrations (for now)
1504 ######################################################
1505
1506 class MigrationData(Base):
1507 __tablename__ = "core__migrations"
1508
1509 name = Column(Unicode, primary_key=True)
1510 version = Column(Integer, nullable=False, default=0)
1511
1512 ######################################################
1513
1514
1515 def show_table_init(engine_uri):
1516 if engine_uri is None:
1517 engine_uri = 'sqlite:///:memory:'
1518 from sqlalchemy import create_engine
1519 engine = create_engine(engine_uri, echo=True)
1520
1521 Base.metadata.create_all(engine)
1522
1523
1524 if __name__ == '__main__':
1525 from sys import argv
1526 print(repr(argv))
1527 if len(argv) == 2:
1528 uri = argv[1]
1529 else:
1530 uri = None
1531 show_table_init(uri)