make all JSONEncoded columns mutable
[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 import logging
22 import datetime
23
24 from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
25 Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
26 SmallInteger
27 from sqlalchemy.orm import relationship, backref, with_polymorphic
28 from sqlalchemy.orm.collections import attribute_mapped_collection
29 from sqlalchemy.sql.expression import desc
30 from sqlalchemy.ext.associationproxy import association_proxy
31 from sqlalchemy.util import memoized_property
32
33
34 from mediagoblin.db.extratypes import (PathTupleWithSlashes, JSONEncoded,
35 MutationDict)
36 from mediagoblin.db.base import Base, DictReadAttrProxy
37 from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
38 MediaCommentMixin, CollectionMixin, CollectionItemMixin
39 from mediagoblin.tools.files import delete_media_files
40 from mediagoblin.tools.common import import_component
41
42 # It's actually kind of annoying how sqlalchemy-migrate does this, if
43 # I understand it right, but whatever. Anyway, don't remove this :P
44 #
45 # We could do migration calls more manually instead of relying on
46 # this import-based meddling...
47 from migrate import changeset
48
49 _log = logging.getLogger(__name__)
50
51 MutationDict.associate_with(JSONEncoded)
52
53
54 class User(Base, UserMixin):
55 """
56 TODO: We should consider moving some rarely used fields
57 into some sort of "shadow" table.
58 """
59 __tablename__ = "core__users"
60
61 id = Column(Integer, primary_key=True)
62 username = Column(Unicode, nullable=False, unique=True)
63 # Note: no db uniqueness constraint on email because it's not
64 # reliable (many email systems case insensitive despite against
65 # the RFC) and because it would be a mess to implement at this
66 # point.
67 email = Column(Unicode, nullable=False)
68 pw_hash = Column(Unicode)
69 email_verified = Column(Boolean, default=False)
70 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
71 status = Column(Unicode, default=u"needs_email_verification", nullable=False)
72 # Intented to be nullable=False, but migrations would not work for it
73 # set to nullable=True implicitly.
74 wants_comment_notification = Column(Boolean, default=True)
75 wants_notifications = Column(Boolean, default=True)
76 license_preference = Column(Unicode)
77 is_admin = Column(Boolean, default=False, nullable=False)
78 url = Column(Unicode)
79 bio = Column(UnicodeText) # ??
80 uploaded = Column(Integer, default=0)
81 upload_limit = Column(Integer)
82
83 ## TODO
84 # plugin data would be in a separate model
85
86 def __repr__(self):
87 return '<{0} #{1} {2} {3} "{4}">'.format(
88 self.__class__.__name__,
89 self.id,
90 'verified' if self.email_verified else 'non-verified',
91 'admin' if self.is_admin else 'user',
92 self.username)
93
94 def delete(self, **kwargs):
95 """Deletes a User and all related entries/comments/files/..."""
96 # Collections get deleted by relationships.
97
98 media_entries = MediaEntry.query.filter(MediaEntry.uploader == self.id)
99 for media in media_entries:
100 # TODO: Make sure that "MediaEntry.delete()" also deletes
101 # all related files/Comments
102 media.delete(del_orphan_tags=False, commit=False)
103
104 # Delete now unused tags
105 # TODO: import here due to cyclic imports!!! This cries for refactoring
106 from mediagoblin.db.util import clean_orphan_tags
107 clean_orphan_tags(commit=False)
108
109 # Delete user, pass through commit=False/True in kwargs
110 super(User, self).delete(**kwargs)
111 _log.info('Deleted user "{0}" account'.format(self.username))
112
113
114 class Client(Base):
115 """
116 Model representing a client - Used for API Auth
117 """
118 __tablename__ = "core__clients"
119
120 id = Column(Unicode, nullable=True, primary_key=True)
121 secret = Column(Unicode, nullable=False)
122 expirey = Column(DateTime, nullable=True)
123 application_type = Column(Unicode, nullable=False)
124 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
125 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
126
127 # optional stuff
128 redirect_uri = Column(JSONEncoded, nullable=True)
129 logo_url = Column(Unicode, nullable=True)
130 application_name = Column(Unicode, nullable=True)
131 contacts = Column(JSONEncoded, nullable=True)
132
133 def __repr__(self):
134 if self.application_name:
135 return "<Client {0} - {1}>".format(self.application_name, self.id)
136 else:
137 return "<Client {0}>".format(self.id)
138
139 class RequestToken(Base):
140 """
141 Model for representing the request tokens
142 """
143 __tablename__ = "core__request_tokens"
144
145 token = Column(Unicode, primary_key=True)
146 secret = Column(Unicode, nullable=False)
147 client = Column(Unicode, ForeignKey(Client.id))
148 user = Column(Integer, ForeignKey(User.id), nullable=True)
149 used = Column(Boolean, default=False)
150 authenticated = Column(Boolean, default=False)
151 verifier = Column(Unicode, nullable=True)
152 callback = Column(Unicode, nullable=False, default=u"oob")
153 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
154 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
155
156 class AccessToken(Base):
157 """
158 Model for representing the access tokens
159 """
160 __tablename__ = "core__access_tokens"
161
162 token = Column(Unicode, nullable=False, primary_key=True)
163 secret = Column(Unicode, nullable=False)
164 user = Column(Integer, ForeignKey(User.id))
165 request_token = Column(Unicode, ForeignKey(RequestToken.token))
166 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
167 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
168
169
170 class NonceTimestamp(Base):
171 """
172 A place the timestamp and nonce can be stored - this is for OAuth1
173 """
174 __tablename__ = "core__nonce_timestamps"
175
176 nonce = Column(Unicode, nullable=False, primary_key=True)
177 timestamp = Column(DateTime, nullable=False, primary_key=True)
178
179
180 class MediaEntry(Base, MediaEntryMixin):
181 """
182 TODO: Consider fetching the media_files using join
183 """
184 __tablename__ = "core__media_entries"
185
186 id = Column(Integer, primary_key=True)
187 uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
188 title = Column(Unicode, nullable=False)
189 slug = Column(Unicode)
190 created = Column(DateTime, nullable=False, default=datetime.datetime.now,
191 index=True)
192 description = Column(UnicodeText) # ??
193 media_type = Column(Unicode, nullable=False)
194 state = Column(Unicode, default=u'unprocessed', nullable=False)
195 # or use sqlalchemy.types.Enum?
196 license = Column(Unicode)
197 collected = Column(Integer, default=0)
198 file_size = Column(Integer, default=0)
199
200 fail_error = Column(Unicode)
201 fail_metadata = Column(JSONEncoded)
202
203 transcoding_progress = Column(SmallInteger)
204
205 queued_media_file = Column(PathTupleWithSlashes)
206
207 queued_task_id = Column(Unicode)
208
209 __table_args__ = (
210 UniqueConstraint('uploader', 'slug'),
211 {})
212
213 get_uploader = relationship(User)
214
215 media_files_helper = relationship("MediaFile",
216 collection_class=attribute_mapped_collection("name"),
217 cascade="all, delete-orphan"
218 )
219 media_files = association_proxy('media_files_helper', 'file_path',
220 creator=lambda k, v: MediaFile(name=k, file_path=v)
221 )
222
223 attachment_files_helper = relationship("MediaAttachmentFile",
224 cascade="all, delete-orphan",
225 order_by="MediaAttachmentFile.created"
226 )
227 attachment_files = association_proxy("attachment_files_helper", "dict_view",
228 creator=lambda v: MediaAttachmentFile(
229 name=v["name"], filepath=v["filepath"])
230 )
231
232 tags_helper = relationship("MediaTag",
233 cascade="all, delete-orphan" # should be automatically deleted
234 )
235 tags = association_proxy("tags_helper", "dict_view",
236 creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
237 )
238
239 collections_helper = relationship("CollectionItem",
240 cascade="all, delete-orphan"
241 )
242 collections = association_proxy("collections_helper", "in_collection")
243
244 ## TODO
245 # fail_error
246
247 def get_comments(self, ascending=False):
248 order_col = MediaComment.created
249 if not ascending:
250 order_col = desc(order_col)
251 return self.all_comments.order_by(order_col)
252
253 def url_to_prev(self, urlgen):
254 """get the next 'newer' entry by this user"""
255 media = MediaEntry.query.filter(
256 (MediaEntry.uploader == self.uploader)
257 & (MediaEntry.state == u'processed')
258 & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()
259
260 if media is not None:
261 return media.url_for_self(urlgen)
262
263 def url_to_next(self, urlgen):
264 """get the next 'older' entry by this user"""
265 media = MediaEntry.query.filter(
266 (MediaEntry.uploader == self.uploader)
267 & (MediaEntry.state == u'processed')
268 & (MediaEntry.id < self.id)).order_by(desc(MediaEntry.id)).first()
269
270 if media is not None:
271 return media.url_for_self(urlgen)
272
273 def get_file_metadata(self, file_key, metadata_key=None):
274 """
275 Return the file_metadata dict of a MediaFile. If metadata_key is given,
276 return the value of the key.
277 """
278 media_file = MediaFile.query.filter_by(media_entry=self.id,
279 name=unicode(file_key)).first()
280
281 if media_file:
282 if metadata_key:
283 return media_file.file_metadata.get(metadata_key, None)
284
285 return media_file.file_metadata
286
287 def set_file_metadata(self, file_key, **kwargs):
288 """
289 Update the file_metadata of a MediaFile.
290 """
291 media_file = MediaFile.query.filter_by(media_entry=self.id,
292 name=unicode(file_key)).first()
293
294 file_metadata = media_file.file_metadata or {}
295
296 for key, value in kwargs.iteritems():
297 file_metadata[key] = value
298
299 media_file.file_metadata = file_metadata
300 media_file.save()
301
302 @property
303 def media_data(self):
304 return getattr(self, self.media_data_ref)
305
306 def media_data_init(self, **kwargs):
307 """
308 Initialize or update the contents of a media entry's media_data row
309 """
310 media_data = self.media_data
311
312 if media_data is None:
313 # Get the correct table:
314 table = import_component(self.media_type + '.models:DATA_MODEL')
315 # No media data, so actually add a new one
316 media_data = table(**kwargs)
317 # Get the relationship set up.
318 media_data.get_media_entry = self
319 else:
320 # Update old media data
321 for field, value in kwargs.iteritems():
322 setattr(media_data, field, value)
323
324 @memoized_property
325 def media_data_ref(self):
326 return import_component(self.media_type + '.models:BACKREF_NAME')
327
328 def __repr__(self):
329 safe_title = self.title.encode('ascii', 'replace')
330
331 return '<{classname} {id}: {title}>'.format(
332 classname=self.__class__.__name__,
333 id=self.id,
334 title=safe_title)
335
336 def delete(self, del_orphan_tags=True, **kwargs):
337 """Delete MediaEntry and all related files/attachments/comments
338
339 This will *not* automatically delete unused collections, which
340 can remain empty...
341
342 :param del_orphan_tags: True/false if we delete unused Tags too
343 :param commit: True/False if this should end the db transaction"""
344 # User's CollectionItems are automatically deleted via "cascade".
345 # Comments on this Media are deleted by cascade, hopefully.
346
347 # Delete all related files/attachments
348 try:
349 delete_media_files(self)
350 except OSError, error:
351 # Returns list of files we failed to delete
352 _log.error('No such files from the user "{1}" to delete: '
353 '{0}'.format(str(error), self.get_uploader))
354 _log.info('Deleted Media entry id "{0}"'.format(self.id))
355 # Related MediaTag's are automatically cleaned, but we might
356 # want to clean out unused Tag's too.
357 if del_orphan_tags:
358 # TODO: Import here due to cyclic imports!!!
359 # This cries for refactoring
360 from mediagoblin.db.util import clean_orphan_tags
361 clean_orphan_tags(commit=False)
362 # pass through commit=False/True in kwargs
363 super(MediaEntry, self).delete(**kwargs)
364
365
366 class FileKeynames(Base):
367 """
368 keywords for various places.
369 currently the MediaFile keys
370 """
371 __tablename__ = "core__file_keynames"
372 id = Column(Integer, primary_key=True)
373 name = Column(Unicode, unique=True)
374
375 def __repr__(self):
376 return "<FileKeyname %r: %r>" % (self.id, self.name)
377
378 @classmethod
379 def find_or_new(cls, name):
380 t = cls.query.filter_by(name=name).first()
381 if t is not None:
382 return t
383 return cls(name=name)
384
385
386 class MediaFile(Base):
387 """
388 TODO: Highly consider moving "name" into a new table.
389 TODO: Consider preloading said table in software
390 """
391 __tablename__ = "core__mediafiles"
392
393 media_entry = Column(
394 Integer, ForeignKey(MediaEntry.id),
395 nullable=False)
396 name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
397 file_path = Column(PathTupleWithSlashes)
398 file_metadata = Column(JSONEncoded)
399
400 __table_args__ = (
401 PrimaryKeyConstraint('media_entry', 'name_id'),
402 {})
403
404 def __repr__(self):
405 return "<MediaFile %s: %r>" % (self.name, self.file_path)
406
407 name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
408 name = association_proxy('name_helper', 'name',
409 creator=FileKeynames.find_or_new
410 )
411
412
413 class MediaAttachmentFile(Base):
414 __tablename__ = "core__attachment_files"
415
416 id = Column(Integer, primary_key=True)
417 media_entry = Column(
418 Integer, ForeignKey(MediaEntry.id),
419 nullable=False)
420 name = Column(Unicode, nullable=False)
421 filepath = Column(PathTupleWithSlashes)
422 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
423
424 @property
425 def dict_view(self):
426 """A dict like view on this object"""
427 return DictReadAttrProxy(self)
428
429
430 class Tag(Base):
431 __tablename__ = "core__tags"
432
433 id = Column(Integer, primary_key=True)
434 slug = Column(Unicode, nullable=False, unique=True)
435
436 def __repr__(self):
437 return "<Tag %r: %r>" % (self.id, self.slug)
438
439 @classmethod
440 def find_or_new(cls, slug):
441 t = cls.query.filter_by(slug=slug).first()
442 if t is not None:
443 return t
444 return cls(slug=slug)
445
446
447 class MediaTag(Base):
448 __tablename__ = "core__media_tags"
449
450 id = Column(Integer, primary_key=True)
451 media_entry = Column(
452 Integer, ForeignKey(MediaEntry.id),
453 nullable=False, index=True)
454 tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
455 name = Column(Unicode)
456 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
457
458 __table_args__ = (
459 UniqueConstraint('tag', 'media_entry'),
460 {})
461
462 tag_helper = relationship(Tag)
463 slug = association_proxy('tag_helper', 'slug',
464 creator=Tag.find_or_new
465 )
466
467 def __init__(self, name=None, slug=None):
468 Base.__init__(self)
469 if name is not None:
470 self.name = name
471 if slug is not None:
472 self.tag_helper = Tag.find_or_new(slug)
473
474 @property
475 def dict_view(self):
476 """A dict like view on this object"""
477 return DictReadAttrProxy(self)
478
479
480 class MediaComment(Base, MediaCommentMixin):
481 __tablename__ = "core__media_comments"
482
483 id = Column(Integer, primary_key=True)
484 media_entry = Column(
485 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
486 author = Column(Integer, ForeignKey(User.id), nullable=False)
487 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
488 content = Column(UnicodeText, nullable=False)
489
490 # Cascade: Comments are owned by their creator. So do the full thing.
491 # lazy=dynamic: People might post a *lot* of comments,
492 # so make the "posted_comments" a query-like thing.
493 get_author = relationship(User,
494 backref=backref("posted_comments",
495 lazy="dynamic",
496 cascade="all, delete-orphan"))
497 get_entry = relationship(MediaEntry,
498 backref=backref("comments",
499 lazy="dynamic",
500 cascade="all, delete-orphan"))
501
502 # Cascade: Comments are somewhat owned by their MediaEntry.
503 # So do the full thing.
504 # lazy=dynamic: MediaEntries might have many comments,
505 # so make the "all_comments" a query-like thing.
506 get_media_entry = relationship(MediaEntry,
507 backref=backref("all_comments",
508 lazy="dynamic",
509 cascade="all, delete-orphan"))
510
511
512 class Collection(Base, CollectionMixin):
513 """An 'album' or 'set' of media by a user.
514
515 On deletion, contained CollectionItems get automatically reaped via
516 SQL cascade"""
517 __tablename__ = "core__collections"
518
519 id = Column(Integer, primary_key=True)
520 title = Column(Unicode, nullable=False)
521 slug = Column(Unicode)
522 created = Column(DateTime, nullable=False, default=datetime.datetime.now,
523 index=True)
524 description = Column(UnicodeText)
525 creator = Column(Integer, ForeignKey(User.id), nullable=False)
526 # TODO: No of items in Collection. Badly named, can we migrate to num_items?
527 items = Column(Integer, default=0)
528
529 # Cascade: Collections are owned by their creator. So do the full thing.
530 get_creator = relationship(User,
531 backref=backref("collections",
532 cascade="all, delete-orphan"))
533
534 __table_args__ = (
535 UniqueConstraint('creator', 'slug'),
536 {})
537
538 def get_collection_items(self, ascending=False):
539 #TODO, is this still needed with self.collection_items being available?
540 order_col = CollectionItem.position
541 if not ascending:
542 order_col = desc(order_col)
543 return CollectionItem.query.filter_by(
544 collection=self.id).order_by(order_col)
545
546
547 class CollectionItem(Base, CollectionItemMixin):
548 __tablename__ = "core__collection_items"
549
550 id = Column(Integer, primary_key=True)
551 media_entry = Column(
552 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
553 collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
554 note = Column(UnicodeText, nullable=True)
555 added = Column(DateTime, nullable=False, default=datetime.datetime.now)
556 position = Column(Integer)
557
558 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
559 in_collection = relationship(Collection,
560 backref=backref(
561 "collection_items",
562 cascade="all, delete-orphan"))
563
564 get_media_entry = relationship(MediaEntry)
565
566 __table_args__ = (
567 UniqueConstraint('collection', 'media_entry'),
568 {})
569
570 @property
571 def dict_view(self):
572 """A dict like view on this object"""
573 return DictReadAttrProxy(self)
574
575
576 class ProcessingMetaData(Base):
577 __tablename__ = 'core__processing_metadata'
578
579 id = Column(Integer, primary_key=True)
580 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
581 index=True)
582 media_entry = relationship(MediaEntry,
583 backref=backref('processing_metadata',
584 cascade='all, delete-orphan'))
585 callback_url = Column(Unicode)
586
587 @property
588 def dict_view(self):
589 """A dict like view on this object"""
590 return DictReadAttrProxy(self)
591
592
593 class CommentSubscription(Base):
594 __tablename__ = 'core__comment_subscriptions'
595 id = Column(Integer, primary_key=True)
596
597 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
598
599 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
600 media_entry = relationship(MediaEntry,
601 backref=backref('comment_subscriptions',
602 cascade='all, delete-orphan'))
603
604 user_id = Column(Integer, ForeignKey(User.id), nullable=False)
605 user = relationship(User,
606 backref=backref('comment_subscriptions',
607 cascade='all, delete-orphan'))
608
609 notify = Column(Boolean, nullable=False, default=True)
610 send_email = Column(Boolean, nullable=False, default=True)
611
612 def __repr__(self):
613 return ('<{classname} #{id}: {user} {media} notify: '
614 '{notify} email: {email}>').format(
615 id=self.id,
616 classname=self.__class__.__name__,
617 user=self.user,
618 media=self.media_entry,
619 notify=self.notify,
620 email=self.send_email)
621
622
623 class Notification(Base):
624 __tablename__ = 'core__notifications'
625 id = Column(Integer, primary_key=True)
626 type = Column(Unicode)
627
628 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
629
630 user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False,
631 index=True)
632 seen = Column(Boolean, default=lambda: False, index=True)
633 user = relationship(
634 User,
635 backref=backref('notifications', cascade='all, delete-orphan'))
636
637 __mapper_args__ = {
638 'polymorphic_identity': 'notification',
639 'polymorphic_on': type
640 }
641
642 def __repr__(self):
643 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
644 id=self.id,
645 klass=self.__class__.__name__,
646 user=self.user,
647 subject=getattr(self, 'subject', None),
648 seen='unseen' if not self.seen else 'seen')
649
650
651 class CommentNotification(Notification):
652 __tablename__ = 'core__comment_notifications'
653 id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
654
655 subject_id = Column(Integer, ForeignKey(MediaComment.id))
656 subject = relationship(
657 MediaComment,
658 backref=backref('comment_notifications', cascade='all, delete-orphan'))
659
660 __mapper_args__ = {
661 'polymorphic_identity': 'comment_notification'
662 }
663
664
665 class ProcessingNotification(Notification):
666 __tablename__ = 'core__processing_notifications'
667
668 id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
669
670 subject_id = Column(Integer, ForeignKey(MediaEntry.id))
671 subject = relationship(
672 MediaEntry,
673 backref=backref('processing_notifications',
674 cascade='all, delete-orphan'))
675
676 __mapper_args__ = {
677 'polymorphic_identity': 'processing_notification'
678 }
679
680
681 with_polymorphic(
682 Notification,
683 [ProcessingNotification, CommentNotification])
684
685 MODELS = [
686 User, Client, RequestToken, AccessToken, NonceTimestamp, MediaEntry, Tag,
687 MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
688 MediaAttachmentFile, ProcessingMetaData, Notification, CommentNotification,
689 ProcessingNotification, CommentSubscription]
690
691 """
692 Foundations are the default rows that are created immediately after the tables
693 are initialized. Each entry to this dictionary should be in the format of:
694 ModelConstructorObject:List of Dictionaries
695 (Each Dictionary represents a row on the Table to be created, containing each
696 of the columns' names as a key string, and each of the columns' values as a
697 value)
698
699 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
700 user_foundations = [{'name':u'Joanna', 'age':24},
701 {'name':u'Andrea', 'age':41}]
702
703 FOUNDATIONS = {User:user_foundations}
704 """
705 FOUNDATIONS = {}
706
707 ######################################################
708 # Special, migrations-tracking table
709 #
710 # Not listed in MODELS because this is special and not
711 # really migrated, but used for migrations (for now)
712 ######################################################
713
714 class MigrationData(Base):
715 __tablename__ = "core__migrations"
716
717 name = Column(Unicode, primary_key=True)
718 version = Column(Integer, nullable=False, default=0)
719
720 ######################################################
721
722
723 def show_table_init(engine_uri):
724 if engine_uri is None:
725 engine_uri = 'sqlite:///:memory:'
726 from sqlalchemy import create_engine
727 engine = create_engine(engine_uri, echo=True)
728
729 Base.metadata.create_all(engine)
730
731
732 if __name__ == '__main__':
733 from sys import argv
734 print repr(argv)
735 if len(argv) == 2:
736 uri = argv[1]
737 else:
738 uri = None
739 show_table_init(uri)