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