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