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