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