Merge branch 'master' into OPW-Moderation-Update
[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
8394febb 107 def has_privilege(self,*priv_names):
108 if len(priv_names) == 1:
109 priv = Privilege.query.filter(
110 Privilege.privilege_name==priv_names[0]).one()
111 return (priv in self.all_privileges)
112 elif len(priv_names) > 1:
113 return self.has_privilege(priv_names[0]) or \
114 self.has_privilege(*priv_names[1:])
115 return False
116
4990b47c 117class Client(Base):
118 """
119 Model representing a client - Used for API Auth
120 """
121 __tablename__ = "core__clients"
122
123 id = Column(Unicode, nullable=True, primary_key=True)
124 secret = Column(Unicode, nullable=False)
125 expirey = Column(DateTime, nullable=True)
126 application_type = Column(Unicode, nullable=False)
127 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
128 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
129
130 # optional stuff
c33a34d4 131 redirect_uri = Column(JSONEncoded, nullable=True)
132 logo_url = Column(Unicode, nullable=True)
4990b47c 133 application_name = Column(Unicode, nullable=True)
c33a34d4 134 contacts = Column(JSONEncoded, nullable=True)
135
4990b47c 136 def __repr__(self):
c33a34d4 137 if self.application_name:
138 return "<Client {0} - {1}>".format(self.application_name, self.id)
139 else:
140 return "<Client {0}>".format(self.id)
4990b47c 141
d41c6a53 142class RequestToken(Base):
143 """
144 Model for representing the request tokens
145 """
146 __tablename__ = "core__request_tokens"
4990b47c 147
d41c6a53 148 token = Column(Unicode, primary_key=True)
149 secret = Column(Unicode, nullable=False)
150 client = Column(Unicode, ForeignKey(Client.id))
151 user = Column(Integer, ForeignKey(User.id), nullable=True)
152 used = Column(Boolean, default=False)
153 authenticated = Column(Boolean, default=False)
154 verifier = Column(Unicode, nullable=True)
405aa45a 155 callback = Column(Unicode, nullable=False, default=u"oob")
d41c6a53 156 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
157 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
b5059525 158
d41c6a53 159class AccessToken(Base):
160 """
161 Model for representing the access tokens
162 """
163 __tablename__ = "core__access_tokens"
164
165 token = Column(Unicode, nullable=False, primary_key=True)
166 secret = Column(Unicode, nullable=False)
167 user = Column(Integer, ForeignKey(User.id))
168 request_token = Column(Unicode, ForeignKey(RequestToken.token))
169 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
170 updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
b5059525 171
4990b47c 172
cfe7054c 173class NonceTimestamp(Base):
174 """
175 A place the timestamp and nonce can be stored - this is for OAuth1
176 """
177 __tablename__ = "core__nonce_timestamps"
178
179 nonce = Column(Unicode, nullable=False, primary_key=True)
180 timestamp = Column(DateTime, nullable=False, primary_key=True)
181
ccca0fbf 182
f42e49c3 183class MediaEntry(Base, MediaEntryMixin):
eea6d276
E
184 """
185 TODO: Consider fetching the media_files using join
186 """
2f5ce68c 187 __tablename__ = "core__media_entries"
ccca0fbf
CAW
188
189 id = Column(Integer, primary_key=True)
ecd538bb 190 uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
7c2c56a5 191 title = Column(Unicode, nullable=False)
3e907d55 192 slug = Column(Unicode)
ecd538bb
E
193 created = Column(DateTime, nullable=False, default=datetime.datetime.now,
194 index=True)
ccca0fbf 195 description = Column(UnicodeText) # ??
ccca0fbf 196 media_type = Column(Unicode, nullable=False)
51fba991
E
197 state = Column(Unicode, default=u'unprocessed', nullable=False)
198 # or use sqlalchemy.types.Enum?
2788e6a1 199 license = Column(Unicode)
be5be115 200 collected = Column(Integer, default=0)
fbad3a9f 201
ccca0fbf 202 fail_error = Column(Unicode)
cf27accc 203 fail_metadata = Column(JSONEncoded)
ccca0fbf 204
64712915
JW
205 transcoding_progress = Column(SmallInteger)
206
02db7e0a 207 queued_media_file = Column(PathTupleWithSlashes)
ccca0fbf
CAW
208
209 queued_task_id = Column(Unicode)
210
211 __table_args__ = (
212 UniqueConstraint('uploader', 'slug'),
213 {})
214
88e90f41
E
215 get_uploader = relationship(User)
216
02db7e0a
E
217 media_files_helper = relationship("MediaFile",
218 collection_class=attribute_mapped_collection("name"),
219 cascade="all, delete-orphan"
220 )
221 media_files = association_proxy('media_files_helper', 'file_path',
fbad3a9f 222 creator=lambda k, v: MediaFile(name=k, file_path=v)
02db7e0a
E
223 )
224
35029581 225 attachment_files_helper = relationship("MediaAttachmentFile",
df5b142a 226 cascade="all, delete-orphan",
35029581
E
227 order_by="MediaAttachmentFile.created"
228 )
229 attachment_files = association_proxy("attachment_files_helper", "dict_view",
230 creator=lambda v: MediaAttachmentFile(
231 name=v["name"], filepath=v["filepath"])
232 )
233
de917303 234 tags_helper = relationship("MediaTag",
fdc34b8b 235 cascade="all, delete-orphan" # should be automatically deleted
de917303
E
236 )
237 tags = association_proxy("tags_helper", "dict_view",
238 creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
239 )
240
be5be115
AW
241 collections_helper = relationship("CollectionItem",
242 cascade="all, delete-orphan"
243 )
244 collections = association_proxy("collections_helper", "in_collection")
245
ccca0fbf 246 ## TODO
ccca0fbf
CAW
247 # fail_error
248
02ede858
E
249 def get_comments(self, ascending=False):
250 order_col = MediaComment.created
251 if not ascending:
252 order_col = desc(order_col)
b98882e1 253 return self.all_comments.order_by(order_col)
02ede858 254
c47a03b9
E
255 def url_to_prev(self, urlgen):
256 """get the next 'newer' entry by this user"""
257 media = MediaEntry.query.filter(
258 (MediaEntry.uploader == self.uploader)
5bd0adeb 259 & (MediaEntry.state == u'processed')
c47a03b9
E
260 & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()
261
262 if media is not None:
263 return media.url_for_self(urlgen)
264
265 def url_to_next(self, urlgen):
266 """get the next 'older' entry by this user"""
267 media = MediaEntry.query.filter(
268 (MediaEntry.uploader == self.uploader)
5bd0adeb 269 & (MediaEntry.state == u'processed')
c47a03b9
E
270 & (MediaEntry.id < self.id)).order_by(desc(MediaEntry.id)).first()
271
272 if media is not None:
273 return media.url_for_self(urlgen)
274
5fe1fd07
E
275 @property
276 def media_data(self):
485404a9 277 return getattr(self, self.media_data_ref)
5fe1fd07 278
acb21949 279 def media_data_init(self, **kwargs):
007ac2e7
CAW
280 """
281 Initialize or update the contents of a media entry's media_data row
282 """
57f8d263 283 media_data = self.media_data
007ac2e7 284
99c2f9f0 285 if media_data is None:
139c6c09
E
286 # Get the correct table:
287 table = import_component(self.media_type + '.models:DATA_MODEL')
57f8d263 288 # No media data, so actually add a new one
139c6c09 289 media_data = table(**kwargs)
57f8d263
E
290 # Get the relationship set up.
291 media_data.get_media_entry = self
007ac2e7 292 else:
57f8d263 293 # Update old media data
007ac2e7
CAW
294 for field, value in kwargs.iteritems():
295 setattr(media_data, field, value)
296
57f8d263
E
297 @memoized_property
298 def media_data_ref(self):
299 return import_component(self.media_type + '.models:BACKREF_NAME')
acb21949 300
64712915 301 def __repr__(self):
79f28e0b
JW
302 safe_title = self.title.encode('ascii', 'replace')
303
64712915
JW
304 return '<{classname} {id}: {title}>'.format(
305 classname=self.__class__.__name__,
306 id=self.id,
79f28e0b 307 title=safe_title)
64712915 308
fdc34b8b
SS
309 def delete(self, del_orphan_tags=True, **kwargs):
310 """Delete MediaEntry and all related files/attachments/comments
311
312 This will *not* automatically delete unused collections, which
313 can remain empty...
314
315 :param del_orphan_tags: True/false if we delete unused Tags too
316 :param commit: True/False if this should end the db transaction"""
317 # User's CollectionItems are automatically deleted via "cascade".
b98882e1 318 # Comments on this Media are deleted by cascade, hopefully.
fdc34b8b
SS
319
320 # Delete all related files/attachments
321 try:
322 delete_media_files(self)
323 except OSError, error:
324 # Returns list of files we failed to delete
325 _log.error('No such files from the user "{1}" to delete: '
326 '{0}'.format(str(error), self.get_uploader))
327 _log.info('Deleted Media entry id "{0}"'.format(self.id))
328 # Related MediaTag's are automatically cleaned, but we might
329 # want to clean out unused Tag's too.
330 if del_orphan_tags:
331 # TODO: Import here due to cyclic imports!!!
332 # This cries for refactoring
333 from mediagoblin.db.util import clean_orphan_tags
334 clean_orphan_tags(commit=False)
335 # pass through commit=False/True in kwargs
336 super(MediaEntry, self).delete(**kwargs)
337
ccca0fbf 338
a9dac7c8
E
339class FileKeynames(Base):
340 """
341 keywords for various places.
342 currently the MediaFile keys
343 """
344 __tablename__ = "core__file_keynames"
345 id = Column(Integer, primary_key=True)
346 name = Column(Unicode, unique=True)
347
348 def __repr__(self):
349 return "<FileKeyname %r: %r>" % (self.id, self.name)
350
351 @classmethod
352 def find_or_new(cls, name):
353 t = cls.query.filter_by(name=name).first()
354 if t is not None:
355 return t
356 return cls(name=name)
357
358
02db7e0a 359class MediaFile(Base):
eea6d276
E
360 """
361 TODO: Highly consider moving "name" into a new table.
362 TODO: Consider preloading said table in software
363 """
2f5ce68c 364 __tablename__ = "core__mediafiles"
02db7e0a
E
365
366 media_entry = Column(
367 Integer, ForeignKey(MediaEntry.id),
a9dac7c8
E
368 nullable=False)
369 name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
02db7e0a
E
370 file_path = Column(PathTupleWithSlashes)
371
a9dac7c8
E
372 __table_args__ = (
373 PrimaryKeyConstraint('media_entry', 'name_id'),
374 {})
375
02db7e0a
E
376 def __repr__(self):
377 return "<MediaFile %s: %r>" % (self.name, self.file_path)
378
a9dac7c8
E
379 name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
380 name = association_proxy('name_helper', 'name',
381 creator=FileKeynames.find_or_new
382 )
383
02db7e0a 384
35029581
E
385class MediaAttachmentFile(Base):
386 __tablename__ = "core__attachment_files"
387
388 id = Column(Integer, primary_key=True)
389 media_entry = Column(
390 Integer, ForeignKey(MediaEntry.id),
391 nullable=False)
392 name = Column(Unicode, nullable=False)
393 filepath = Column(PathTupleWithSlashes)
394 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
395
396 @property
397 def dict_view(self):
398 """A dict like view on this object"""
399 return DictReadAttrProxy(self)
400
401
ccca0fbf 402class Tag(Base):
2f5ce68c 403 __tablename__ = "core__tags"
ccca0fbf
CAW
404
405 id = Column(Integer, primary_key=True)
406 slug = Column(Unicode, nullable=False, unique=True)
407
de917303
E
408 def __repr__(self):
409 return "<Tag %r: %r>" % (self.id, self.slug)
410
411 @classmethod
412 def find_or_new(cls, slug):
413 t = cls.query.filter_by(slug=slug).first()
414 if t is not None:
415 return t
416 return cls(slug=slug)
417
ccca0fbf
CAW
418
419class MediaTag(Base):
2f5ce68c 420 __tablename__ = "core__media_tags"
ccca0fbf
CAW
421
422 id = Column(Integer, primary_key=True)
ccca0fbf 423 media_entry = Column(
de917303 424 Integer, ForeignKey(MediaEntry.id),
ecd538bb
E
425 nullable=False, index=True)
426 tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
de917303 427 name = Column(Unicode)
ccca0fbf
CAW
428 # created = Column(DateTime, nullable=False, default=datetime.datetime.now)
429
430 __table_args__ = (
431 UniqueConstraint('tag', 'media_entry'),
432 {})
433
de917303
E
434 tag_helper = relationship(Tag)
435 slug = association_proxy('tag_helper', 'slug',
436 creator=Tag.find_or_new
437 )
438
6456cefa 439 def __init__(self, name=None, slug=None):
de917303 440 Base.__init__(self)
6456cefa
E
441 if name is not None:
442 self.name = name
443 if slug is not None:
444 self.tag_helper = Tag.find_or_new(slug)
de917303
E
445
446 @property
447 def dict_view(self):
448 """A dict like view on this object"""
449 return DictReadAttrProxy(self)
450
ccca0fbf 451
feba5c52 452class MediaComment(Base, MediaCommentMixin):
2f5ce68c 453 __tablename__ = "core__media_comments"
fbad3a9f 454
ccca0fbf
CAW
455 id = Column(Integer, primary_key=True)
456 media_entry = Column(
ecd538bb
E
457 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
458 author = Column(Integer, ForeignKey(User.id), nullable=False)
ccca0fbf
CAW
459 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
460 content = Column(UnicodeText, nullable=False)
e365f980 461
ff68ca9f 462 # Cascade: Comments are owned by their creator. So do the full thing.
b98882e1
E
463 # lazy=dynamic: People might post a *lot* of comments,
464 # so make the "posted_comments" a query-like thing.
ff68ca9f
E
465 get_author = relationship(User,
466 backref=backref("posted_comments",
467 lazy="dynamic",
468 cascade="all, delete-orphan"))
2d7b6bde
JW
469 get_entry = relationship(MediaEntry,
470 backref=backref("comments",
471 lazy="dynamic",
472 cascade="all, delete-orphan"))
88e90f41 473
b98882e1
E
474 # Cascade: Comments are somewhat owned by their MediaEntry.
475 # So do the full thing.
476 # lazy=dynamic: MediaEntries might have many comments,
477 # so make the "all_comments" a query-like thing.
478 get_media_entry = relationship(MediaEntry,
479 backref=backref("all_comments",
480 lazy="dynamic",
481 cascade="all, delete-orphan"))
482
e365f980 483
be5be115 484class Collection(Base, CollectionMixin):
242776e3
SS
485 """An 'album' or 'set' of media by a user.
486
487 On deletion, contained CollectionItems get automatically reaped via
488 SQL cascade"""
be5be115
AW
489 __tablename__ = "core__collections"
490
491 id = Column(Integer, primary_key=True)
492 title = Column(Unicode, nullable=False)
493 slug = Column(Unicode)
494 created = Column(DateTime, nullable=False, default=datetime.datetime.now,
34d8bc98 495 index=True)
88a9662b 496 description = Column(UnicodeText)
be5be115 497 creator = Column(Integer, ForeignKey(User.id), nullable=False)
242776e3 498 # TODO: No of items in Collection. Badly named, can we migrate to num_items?
be5be115
AW
499 items = Column(Integer, default=0)
500
6194344b
E
501 # Cascade: Collections are owned by their creator. So do the full thing.
502 get_creator = relationship(User,
503 backref=backref("collections",
504 cascade="all, delete-orphan"))
88a9662b 505
34d8bc98
RE
506 __table_args__ = (
507 UniqueConstraint('creator', 'slug'),
508 {})
509
be5be115 510 def get_collection_items(self, ascending=False):
242776e3 511 #TODO, is this still needed with self.collection_items being available?
be5be115
AW
512 order_col = CollectionItem.position
513 if not ascending:
514 order_col = desc(order_col)
515 return CollectionItem.query.filter_by(
516 collection=self.id).order_by(order_col)
517
be5be115
AW
518
519class CollectionItem(Base, CollectionItemMixin):
520 __tablename__ = "core__collection_items"
521
522 id = Column(Integer, primary_key=True)
523 media_entry = Column(
524 Integer, ForeignKey(MediaEntry.id), nullable=False, index=True)
525 collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
526 note = Column(UnicodeText, nullable=True)
527 added = Column(DateTime, nullable=False, default=datetime.datetime.now)
528 position = Column(Integer)
6194344b
E
529
530 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
531 in_collection = relationship(Collection,
242776e3
SS
532 backref=backref(
533 "collection_items",
534 cascade="all, delete-orphan"))
be5be115
AW
535
536 get_media_entry = relationship(MediaEntry)
537
be5be115
AW
538 __table_args__ = (
539 UniqueConstraint('collection', 'media_entry'),
540 {})
541
542 @property
543 def dict_view(self):
544 """A dict like view on this object"""
545 return DictReadAttrProxy(self)
546
547
5354f954
JW
548class ProcessingMetaData(Base):
549 __tablename__ = 'core__processing_metadata'
550
551 id = Column(Integer, primary_key=True)
552 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
553 index=True)
942084fb
JW
554 media_entry = relationship(MediaEntry,
555 backref=backref('processing_metadata',
556 cascade='all, delete-orphan'))
5354f954
JW
557 callback_url = Column(Unicode)
558
559 @property
560 def dict_view(self):
561 """A dict like view on this object"""
562 return DictReadAttrProxy(self)
563
564
2d7b6bde
JW
565class CommentSubscription(Base):
566 __tablename__ = 'core__comment_subscriptions'
567 id = Column(Integer, primary_key=True)
568
569 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
570
571 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
572 media_entry = relationship(MediaEntry,
573 backref=backref('comment_subscriptions',
574 cascade='all, delete-orphan'))
575
576 user_id = Column(Integer, ForeignKey(User.id), nullable=False)
577 user = relationship(User,
578 backref=backref('comment_subscriptions',
579 cascade='all, delete-orphan'))
580
581 notify = Column(Boolean, nullable=False, default=True)
582 send_email = Column(Boolean, nullable=False, default=True)
583
584 def __repr__(self):
585 return ('<{classname} #{id}: {user} {media} notify: '
586 '{notify} email: {email}>').format(
587 id=self.id,
588 classname=self.__class__.__name__,
589 user=self.user,
590 media=self.media_entry,
591 notify=self.notify,
592 email=self.send_email)
593
594
595class Notification(Base):
596 __tablename__ = 'core__notifications'
597 id = Column(Integer, primary_key=True)
598 type = Column(Unicode)
599
600 created = Column(DateTime, nullable=False, default=datetime.datetime.now)
601
602 user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False,
603 index=True)
604 seen = Column(Boolean, default=lambda: False, index=True)
605 user = relationship(
606 User,
607 backref=backref('notifications', cascade='all, delete-orphan'))
608
609 __mapper_args__ = {
610 'polymorphic_identity': 'notification',
611 'polymorphic_on': type
612 }
613
614 def __repr__(self):
615 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
616 id=self.id,
617 klass=self.__class__.__name__,
618 user=self.user,
619 subject=getattr(self, 'subject', None),
620 seen='unseen' if not self.seen else 'seen')
621
622
623class CommentNotification(Notification):
624 __tablename__ = 'core__comment_notifications'
625 id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
626
627 subject_id = Column(Integer, ForeignKey(MediaComment.id))
628 subject = relationship(
629 MediaComment,
630 backref=backref('comment_notifications', cascade='all, delete-orphan'))
631
632 __mapper_args__ = {
633 'polymorphic_identity': 'comment_notification'
634 }
635
636
637class ProcessingNotification(Notification):
638 __tablename__ = 'core__processing_notifications'
639
640 id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
641
642 subject_id = Column(Integer, ForeignKey(MediaEntry.id))
643 subject = relationship(
644 MediaEntry,
645 backref=backref('processing_notifications',
646 cascade='all, delete-orphan'))
647
648 __mapper_args__ = {
649 'polymorphic_identity': 'processing_notification'
650 }
5354f954 651
30a9fe7c 652class ReportBase(Base):
653 """
6bba33d7 654 This is the basic report table which the other reports are based off of.
655 :keyword reporter_id
656 :keyword report_content
657 :keyword reported_user_id
658 :keyword created
659 :keyword resolved
660 :keyword result
661 :keyword discriminator
30a9fe7c 662
663 """
664 __tablename__ = 'core__reports'
665 id = Column(Integer, primary_key=True)
666 reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
9b8ef022 667 reporter = relationship(
668 User,
669 backref=backref("reports_filed_by",
670 lazy="dynamic",
3ce0c611 671 cascade="all, delete-orphan"),
672 primaryjoin="User.id==ReportBase.reporter_id")
30a9fe7c 673 report_content = Column(UnicodeText)
3ce0c611 674 reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
675 reported_user = relationship(
676 User,
677 backref=backref("reports_filed_on",
678 lazy="dynamic",
679 cascade="all, delete-orphan"),
680 primaryjoin="User.id==ReportBase.reported_user_id")
9b8ef022 681 created = Column(DateTime, nullable=False, default=datetime.datetime.now())
30a9fe7c 682 discriminator = Column('type', Unicode(50))
683 __mapper_args__ = {'polymorphic_on': discriminator}
684
3aa3871b 685 def is_comment_report(self):
686 return self.discriminator=='comment_report'
687
688 def is_media_entry_report(self):
689 return self.discriminator=='media_report'
690
691 def is_archived_report(self):
692 return self.discriminator=='archived_report'
693
30a9fe7c 694
695class CommentReport(ReportBase):
696 """
697 A class to keep track of reports that have been filed on comments
698 """
699 __tablename__ = 'core__reports_on_comments'
700 __mapper_args__ = {'polymorphic_identity': 'comment_report'}
701
702 id = Column('id',Integer, ForeignKey('core__reports.id'),
703 primary_key=True)
704 comment_id = Column(Integer, ForeignKey(MediaComment.id), nullable=False)
9b8ef022 705 comment = relationship(
706 MediaComment, backref=backref("reports_filed_on",
707 lazy="dynamic",
708 cascade="all, delete-orphan"))
30a9fe7c 709
710class MediaReport(ReportBase):
711 """
712 A class to keep track of reports that have been filed on media entries
713 """
714 __tablename__ = 'core__reports_on_media'
715 __mapper_args__ = {'polymorphic_identity': 'media_report'}
716
717 id = Column('id',Integer, ForeignKey('core__reports.id'),
718 primary_key=True)
719 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
9b8ef022 720 media_entry = relationship(
721 MediaEntry,
3aa3871b 722 backref=backref("reports_filed_onmod/reports/1/",
9b8ef022 723 lazy="dynamic",
724 cascade="all, delete-orphan"))
30a9fe7c 725
3aa3871b 726class ArchivedReport(ReportBase):
727 """
728 A table to keep track of reports that have been resolved
729 """
730 __tablename__ = 'core__reports_archived'
731 __mapper_args__ = {'polymorphic_identity': 'archived_report'}
732 id = Column('id',Integer, ForeignKey('core__reports.id'),
733 primary_key=True)
734
735 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id))
736 media_entry = relationship(
737 MediaEntry,
738 backref=backref("past_reports_filed_on",
739 lazy="dynamic"))
740 comment_id = Column(Integer, ForeignKey(MediaComment.id))
741 comment = relationship(
742 MediaComment, backref=backref("past_reports_filed_on",
743 lazy="dynamic"))
744
745 resolver_id = Column(Integer, ForeignKey(User.id), nullable=False)
746 resolver = relationship(
747 User,
748 backref=backref("reports_resolved_by",
749 lazy="dynamic",
750 cascade="all, delete-orphan"),
751 primaryjoin="User.id==ArchivedReport.resolver_id")
752
753 resolved = Column(DateTime)
754 result = Column(UnicodeText)
755
30a9fe7c 756class UserBan(Base):
757 """
9b8ef022 758 Holds the information on a specific user's ban-state. As long as one of
759 these is attached to a user, they are banned from accessing mediagoblin.
760 When they try to log in, they are greeted with a page that tells them
761 the reason why they are banned and when (if ever) the ban will be
762 lifted
763
6bba33d7 764 :keyword user_id Holds the id of the user this object is
9b8ef022 765 attached to. This is a one-to-one
766 relationship.
6bba33d7 767 :keyword expiration_date Holds the date that the ban will be lifted.
9b8ef022 768 If this is null, the ban is permanent
769 unless a moderator manually lifts it.
6bba33d7 770 :keyword reason Holds the reason why the user was banned.
30a9fe7c 771 """
772 __tablename__ = 'core__user_bans'
773
9b8ef022 774 user_id = Column(Integer, ForeignKey(User.id), nullable=False,
775 primary_key=True)
30a9fe7c 776 expiration_date = Column(DateTime)
777 reason = Column(UnicodeText, nullable=False)
778
779
3fb96fc9 780class Privilege(Base):
6bba33d7 781 """
782 The Privilege table holds all of the different privileges a user can hold.
783 If a user 'has' a privilege, the User object is in a relationship with the
784 privilege object.
785
786 :keyword privilege_name Holds a unicode object that is the recognizable
787 name of this privilege. This is the column
788 used for identifying whether or not a user
789 has a necessary privilege or not.
790
791 """
3fb96fc9 792 __tablename__ = 'core__privileges'
30a9fe7c 793
794 id = Column(Integer, nullable=False, primary_key=True)
3fb96fc9 795 privilege_name = Column(Unicode, nullable=False, unique=True)
9b8ef022 796 all_users = relationship(
797 User,
3fb96fc9 798 backref='all_privileges',
799 secondary="core__privileges_users")
9b8ef022 800
3fb96fc9 801 def __init__(self, privilege_name):
6bba33d7 802 '''
803 Currently consructors are required for tables that are initialized thru
804 the FOUNDATIONS system. This is because they need to be able to be con-
805 -structed by a list object holding their arg*s
806 '''
3fb96fc9 807 self.privilege_name = privilege_name
30a9fe7c 808
809 def __repr__(self):
3fb96fc9 810 return "<Privilege %s>" % (self.privilege_name)
30a9fe7c 811
6bba33d7 812
3fb96fc9 813class PrivilegeUserAssociation(Base):
6bba33d7 814 '''
815 This table holds the many-to-many relationship between User and Privilege
816 '''
817
3fb96fc9 818 __tablename__ = 'core__privileges_users'
30a9fe7c 819
3fb96fc9 820 privilege_id = Column(
821 'core__privilege_id',
9b8ef022 822 Integer,
823 ForeignKey(User.id),
824 primary_key=True)
825 user_id = Column(
826 'core__user_id',
827 Integer,
3fb96fc9 828 ForeignKey(Privilege.id),
9b8ef022 829 primary_key=True)
30a9fe7c 830
2d7b6bde
JW
831with_polymorphic(
832 Notification,
833 [ProcessingNotification, CommentNotification])
30a9fe7c 834
70b44584 835MODELS = [
9e204e49 836 User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
837 MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
838 Notification, CommentNotification, ProcessingNotification,
839 CommentSubscription, ReportBase, CommentReport, MediaReport, UserBan,
840 Privilege, PrivilegeUserAssociation, ArchivedReport, ArchivedReport]
9b8ef022 841
f2b2008d 842"""
b5059525 843 Foundations are the default rows that are created immediately after the tables
f2b2008d 844 are initialized. Each entry to this dictionary should be in the format of:
845 ModelConstructorObject:List of Dictionaries
846 (Each Dictionary represents a row on the Table to be created, containing each
847 of the columns' names as a key string, and each of the columns' values as a
848 value)
849
850 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
851 user_foundations = [{'name':u'Joanna', 'age':24},
852 {'name':u'Andrea', 'age':41}]
853
854 FOUNDATIONS = {User:user_foundations}
855"""
52a355b2 856privilege_foundations = [{'privilege_name':u'admin'},
857 {'privilege_name':u'moderator'},
858 {'privilege_name':u'uploader'},
859 {'privilege_name':u'reporter'},
860 {'privilege_name':u'commenter'},
861 {'privilege_name':u'active'}]
3fb96fc9 862FOUNDATIONS = {Privilege:privilege_foundations}
70b44584
CAW
863
864######################################################
865# Special, migrations-tracking table
866#
867# Not listed in MODELS because this is special and not
868# really migrated, but used for migrations (for now)
869######################################################
870
871class MigrationData(Base):
2f5ce68c 872 __tablename__ = "core__migrations"
70b44584 873
bf813828 874 name = Column(Unicode, primary_key=True)
70b44584
CAW
875 version = Column(Integer, nullable=False, default=0)
876
877######################################################
878
879
eea6d276
E
880def show_table_init(engine_uri):
881 if engine_uri is None:
882 engine_uri = 'sqlite:///:memory:'
e365f980 883 from sqlalchemy import create_engine
eea6d276 884 engine = create_engine(engine_uri, echo=True)
e365f980
E
885
886 Base.metadata.create_all(engine)
887
888
889if __name__ == '__main__':
eea6d276
E
890 from sys import argv
891 print repr(argv)
892 if len(argv) == 2:
893 uri = argv[1]
894 else:
895 uri = None
896 show_table_init(uri)