0974676a5577352c68550834a67bfcba235daa13
[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 from __future__ import print_function
22
23 import logging
24 import datetime
25
26 from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
27 Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
28 SmallInteger, Date, types, Float
29 from sqlalchemy.orm import relationship, backref, with_polymorphic, validates, \
30 class_mapper
31 from sqlalchemy.orm.collections import attribute_mapped_collection
32 from sqlalchemy.sql import and_
33 from sqlalchemy.sql.expression import desc
34 from sqlalchemy.ext.associationproxy import association_proxy
35 from sqlalchemy.util import memoized_property
36
37 from mediagoblin.db.extratypes import (PathTupleWithSlashes, JSONEncoded,
38 MutationDict)
39 from mediagoblin.db.base import Base, DictReadAttrProxy, FakeCursor
40 from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
41 CollectionMixin, CollectionItemMixin, ActivityMixin, TextCommentMixin, \
42 CommentingMixin
43 from mediagoblin.tools.files import delete_media_files
44 from mediagoblin.tools.common import import_component
45 from mediagoblin.tools.routing import extract_url_arguments
46 from mediagoblin.tools.text import convert_to_tag_list_of_dicts
47
48 import six
49 from six.moves.urllib.parse import urljoin
50 from pytz import UTC
51
52 _log = logging.getLogger(__name__)
53
54 class GenericModelReference(Base):
55 """
56 Represents a relationship to any model that is defined with a integer pk
57 """
58 __tablename__ = "core__generic_model_reference"
59
60 id = Column(Integer, primary_key=True)
61 obj_pk = Column(Integer, nullable=False)
62
63 # This will be the tablename of the model
64 model_type = Column(Unicode, nullable=False)
65
66 # Constrain it so obj_pk and model_type have to be unique
67 # They should be this order as the index is generated, "model_type" will be
68 # the major order as it's put first.
69 __table_args__ = (
70 UniqueConstraint("model_type", "obj_pk"),
71 {})
72
73 def get_object(self):
74 # This can happen if it's yet to be saved
75 if self.model_type is None or self.obj_pk is None:
76 return None
77
78 model = self._get_model_from_type(self.model_type)
79 return model.query.filter_by(id=self.obj_pk).first()
80
81 def set_object(self, obj):
82 model = obj.__class__
83
84 # Check we've been given a object
85 if not issubclass(model, Base):
86 raise ValueError("Only models can be set as using the GMR")
87
88 # Check that the model has an explicit __tablename__ declaration
89 if getattr(model, "__tablename__", None) is None:
90 raise ValueError("Models must have __tablename__ attribute")
91
92 # Check that it's not a composite primary key
93 primary_keys = [key.name for key in class_mapper(model).primary_key]
94 if len(primary_keys) > 1:
95 raise ValueError("Models can not have composite primary keys")
96
97 # Check that the field on the model is a an integer field
98 pk_column = getattr(model, primary_keys[0])
99 if not isinstance(pk_column.type, Integer):
100 raise ValueError("Only models with integer pks can be set")
101
102 if getattr(obj, pk_column.key) is None:
103 obj.save(commit=False)
104
105 self.obj_pk = getattr(obj, pk_column.key)
106 self.model_type = obj.__tablename__
107
108 def _get_model_from_type(self, model_type):
109 """ Gets a model from a tablename (model type) """
110 if getattr(type(self), "_TYPE_MAP", None) is None:
111 # We want to build on the class (not the instance) a map of all the
112 # models by the table name (type) for easy lookup, this is done on
113 # the class so it can be shared between all instances
114
115 # to prevent circular imports do import here
116 registry = dict(Base._decl_class_registry).values()
117 self._TYPE_MAP = dict(
118 ((m.__tablename__, m) for m in registry if hasattr(m, "__tablename__"))
119 )
120 setattr(type(self), "_TYPE_MAP", self._TYPE_MAP)
121
122 return self.__class__._TYPE_MAP[model_type]
123
124 @classmethod
125 def find_for_obj(cls, obj):
126 """ Finds a GMR for an object or returns None """
127 # Is there one for this already.
128 model = type(obj)
129 pk = getattr(obj, "id")
130
131 gmr = cls.query.filter_by(
132 obj_pk=pk,
133 model_type=model.__tablename__
134 )
135
136 return gmr.first()
137
138 @classmethod
139 def find_or_new(cls, obj):
140 """ Finds an existing GMR or creates a new one for the object """
141 gmr = cls.find_for_obj(obj)
142
143 # If there isn't one already create one
144 if gmr is None:
145 gmr = cls(
146 obj_pk=obj.id,
147 model_type=type(obj).__tablename__
148 )
149
150 return gmr
151
152 class Location(Base):
153 """ Represents a physical location """
154 __tablename__ = "core__locations"
155
156 id = Column(Integer, primary_key=True)
157 name = Column(Unicode)
158
159 # GPS coordinates
160 position = Column(MutationDict.as_mutable(JSONEncoded))
161 address = Column(MutationDict.as_mutable(JSONEncoded))
162
163 @classmethod
164 def create(cls, data, obj):
165 location = cls()
166 location.unserialize(data)
167 location.save()
168 obj.location = location.id
169 return location
170
171 def serialize(self, request):
172 location = {"objectType": "place"}
173
174 if self.name is not None:
175 location["displayName"] = self.name
176
177 if self.position:
178 location["position"] = self.position
179
180 if self.address:
181 location["address"] = self.address
182
183 return location
184
185 def unserialize(self, data):
186 if "displayName" in data:
187 self.name = data["displayName"]
188
189 self.position = {}
190 self.address = {}
191
192 # nicer way to do this?
193 if "position" in data:
194 # TODO: deal with ISO 9709 formatted string as position
195 if "altitude" in data["position"]:
196 self.position["altitude"] = data["position"]["altitude"]
197
198 if "direction" in data["position"]:
199 self.position["direction"] = data["position"]["direction"]
200
201 if "longitude" in data["position"]:
202 self.position["longitude"] = data["position"]["longitude"]
203
204 if "latitude" in data["position"]:
205 self.position["latitude"] = data["position"]["latitude"]
206
207 if "address" in data:
208 if "formatted" in data["address"]:
209 self.address["formatted"] = data["address"]["formatted"]
210
211 if "streetAddress" in data["address"]:
212 self.address["streetAddress"] = data["address"]["streetAddress"]
213
214 if "locality" in data["address"]:
215 self.address["locality"] = data["address"]["locality"]
216
217 if "region" in data["address"]:
218 self.address["region"] = data["address"]["region"]
219
220 if "postalCode" in data["address"]:
221 self.address["postalCode"] = data["addresss"]["postalCode"]
222
223 if "country" in data["address"]:
224 self.address["country"] = data["address"]["country"]
225
226 class User(Base, UserMixin):
227 """
228 Base user that is common amongst LocalUser and RemoteUser.
229
230 This holds all the fields which are common between both the Local and Remote
231 user models.
232
233 NB: ForeignKeys should reference this User model and NOT the LocalUser or
234 RemoteUser models.
235 """
236 __tablename__ = "core__users"
237
238 id = Column(Integer, primary_key=True)
239 url = Column(Unicode)
240 bio = Column(UnicodeText)
241 name = Column(Unicode)
242
243 # This is required for the polymorphic inheritance
244 type = Column(Unicode)
245
246 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
247 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
248
249 location = Column(Integer, ForeignKey("core__locations.id"))
250
251 # Lazy getters
252 get_location = relationship("Location", lazy="joined")
253
254 __mapper_args__ = {
255 'polymorphic_identity': 'user',
256 'polymorphic_on': type,
257 }
258
259 deletion_mode = Base.SOFT_DELETE
260
261 def soft_delete(self, *args, **kwargs):
262 # Find all the Collections and delete those
263 for collection in Collection.query.filter_by(actor=self.id):
264 collection.delete(**kwargs)
265
266 # Find all the comments and delete those too
267 for comment in TextComment.query.filter_by(actor=self.id):
268 comment.delete(**kwargs)
269
270 # Find all the activities and delete those too
271 for activity in Activity.query.filter_by(actor=self.id):
272 activity.delete(**kwargs)
273
274 super(User, self).soft_delete(*args, **kwargs)
275
276
277 def delete(self, *args, **kwargs):
278 """Deletes a User and all related entries/comments/files/..."""
279 # Collections get deleted by relationships.
280
281 media_entries = MediaEntry.query.filter(MediaEntry.actor == self.id)
282 for media in media_entries:
283 # TODO: Make sure that "MediaEntry.delete()" also deletes
284 # all related files/Comments
285 media.delete(del_orphan_tags=False, commit=False)
286
287 # Delete now unused tags
288 # TODO: import here due to cyclic imports!!! This cries for refactoring
289 from mediagoblin.db.util import clean_orphan_tags
290 clean_orphan_tags(commit=False)
291
292 # Delete user, pass through commit=False/True in kwargs
293 username = self.username
294 super(User, self).delete(*args, **kwargs)
295 _log.info('Deleted user "{0}" account'.format(username))
296
297 def has_privilege(self, privilege, allow_admin=True):
298 """
299 This method checks to make sure a user has all the correct privileges
300 to access a piece of content.
301
302 :param privilege A unicode object which represent the different
303 privileges which may give the user access to
304 content.
305
306 :param allow_admin If this is set to True the then if the user is
307 an admin, then this will always return True
308 even if the user hasn't been given the
309 privilege. (defaults to True)
310 """
311 priv = Privilege.query.filter_by(privilege_name=privilege).one()
312 if priv in self.all_privileges:
313 return True
314 elif allow_admin and self.has_privilege(u'admin', allow_admin=False):
315 return True
316
317 return False
318
319 def is_banned(self):
320 """
321 Checks if this user is banned.
322
323 :returns True if self is banned
324 :returns False if self is not
325 """
326 return UserBan.query.get(self.id) is not None
327
328 def serialize(self, request):
329 published = UTC.localize(self.created)
330 updated = UTC.localize(self.updated)
331 user = {
332 "published": published.isoformat(),
333 "updated": updated.isoformat(),
334 "objectType": self.object_type,
335 "pump_io": {
336 "shared": False,
337 "followed": False,
338 },
339 }
340
341 if self.bio:
342 user.update({"summary": self.bio})
343 if self.url:
344 user.update({"url": self.url})
345 if self.location:
346 user.update({"location": self.get_location.serialize(request)})
347
348 return user
349
350 def unserialize(self, data):
351 if "summary" in data:
352 self.bio = data["summary"]
353
354 if "location" in data:
355 Location.create(data, self)
356
357 class LocalUser(User):
358 """ This represents a user registered on this instance """
359 __tablename__ = "core__local_users"
360
361 id = Column(Integer, ForeignKey("core__users.id"), primary_key=True)
362 username = Column(Unicode, nullable=False, unique=True)
363 # Note: no db uniqueness constraint on email because it's not
364 # reliable (many email systems case insensitive despite against
365 # the RFC) and because it would be a mess to implement at this
366 # point.
367 email = Column(Unicode, nullable=False)
368 pw_hash = Column(Unicode)
369
370 # Intented to be nullable=False, but migrations would not work for it
371 # set to nullable=True implicitly.
372 wants_comment_notification = Column(Boolean, default=True)
373 wants_notifications = Column(Boolean, default=True)
374 license_preference = Column(Unicode)
375 uploaded = Column(Integer, default=0)
376 upload_limit = Column(Integer)
377
378 __mapper_args__ = {
379 "polymorphic_identity": "user_local",
380 }
381
382 ## TODO
383 # plugin data would be in a separate model
384
385 def __repr__(self):
386 return '<{0} #{1} {2} {3} "{4}">'.format(
387 self.__class__.__name__,
388 self.id,
389 'verified' if self.has_privilege(u'active') else 'non-verified',
390 'admin' if self.has_privilege(u'admin') else 'user',
391 self.username)
392
393 def get_public_id(self, host):
394 return "acct:{0}@{1}".format(self.username, host)
395
396 def serialize(self, request):
397 user = {
398 "id": self.get_public_id(request.host),
399 "preferredUsername": self.username,
400 "displayName": self.get_public_id(request.host).split(":", 1)[1],
401 "links": {
402 "self": {
403 "href": request.urlgen(
404 "mediagoblin.api.user.profile",
405 username=self.username,
406 qualified=True
407 ),
408 },
409 "activity-inbox": {
410 "href": request.urlgen(
411 "mediagoblin.api.inbox",
412 username=self.username,
413 qualified=True
414 )
415 },
416 "activity-outbox": {
417 "href": request.urlgen(
418 "mediagoblin.api.feed",
419 username=self.username,
420 qualified=True
421 )
422 },
423 },
424 }
425
426 user.update(super(LocalUser, self).serialize(request))
427 return user
428
429 class RemoteUser(User):
430 """ User that is on another (remote) instance """
431 __tablename__ = "core__remote_users"
432
433 id = Column(Integer, ForeignKey("core__users.id"), primary_key=True)
434 webfinger = Column(Unicode, unique=True)
435
436 __mapper_args__ = {
437 'polymorphic_identity': 'user_remote'
438 }
439
440 def __repr__(self):
441 return "<{0} #{1} {2}>".format(
442 self.__class__.__name__,
443 self.id,
444 self.webfinger
445 )
446
447
448 class Client(Base):
449 """
450 Model representing a client - Used for API Auth
451 """
452 __tablename__ = "core__clients"
453
454 id = Column(Unicode, nullable=True, primary_key=True)
455 secret = Column(Unicode, nullable=False)
456 expirey = Column(DateTime, nullable=True)
457 application_type = Column(Unicode, nullable=False)
458 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
459 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
460
461 # optional stuff
462 redirect_uri = Column(JSONEncoded, nullable=True)
463 logo_url = Column(Unicode, nullable=True)
464 application_name = Column(Unicode, nullable=True)
465 contacts = Column(JSONEncoded, nullable=True)
466
467 def __repr__(self):
468 if self.application_name:
469 return "<Client {0} - {1}>".format(self.application_name, self.id)
470 else:
471 return "<Client {0}>".format(self.id)
472
473 class RequestToken(Base):
474 """
475 Model for representing the request tokens
476 """
477 __tablename__ = "core__request_tokens"
478
479 token = Column(Unicode, primary_key=True)
480 secret = Column(Unicode, nullable=False)
481 client = Column(Unicode, ForeignKey(Client.id))
482 actor = Column(Integer, ForeignKey(User.id), nullable=True)
483 used = Column(Boolean, default=False)
484 authenticated = Column(Boolean, default=False)
485 verifier = Column(Unicode, nullable=True)
486 callback = Column(Unicode, nullable=False, default=u"oob")
487 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
488 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
489
490 get_client = relationship(Client)
491
492 class AccessToken(Base):
493 """
494 Model for representing the access tokens
495 """
496 __tablename__ = "core__access_tokens"
497
498 token = Column(Unicode, nullable=False, primary_key=True)
499 secret = Column(Unicode, nullable=False)
500 actor = Column(Integer, ForeignKey(User.id))
501 request_token = Column(Unicode, ForeignKey(RequestToken.token))
502 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
503 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
504
505 get_requesttoken = relationship(RequestToken)
506
507
508 class NonceTimestamp(Base):
509 """
510 A place the timestamp and nonce can be stored - this is for OAuth1
511 """
512 __tablename__ = "core__nonce_timestamps"
513
514 nonce = Column(Unicode, nullable=False, primary_key=True)
515 timestamp = Column(DateTime, nullable=False, primary_key=True)
516
517 class MediaEntry(Base, MediaEntryMixin, CommentingMixin):
518 """
519 TODO: Consider fetching the media_files using join
520 """
521 __tablename__ = "core__media_entries"
522
523 id = Column(Integer, primary_key=True)
524 public_id = Column(Unicode, unique=True, nullable=True)
525 remote = Column(Boolean, default=False)
526
527 actor = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
528 title = Column(Unicode, nullable=False)
529 slug = Column(Unicode)
530 description = Column(UnicodeText) # ??
531 media_type = Column(Unicode, nullable=False)
532 state = Column(Unicode, default=u'unprocessed', nullable=False)
533 # or use sqlalchemy.types.Enum?
534 license = Column(Unicode)
535 file_size = Column(Integer, default=0)
536 location = Column(Integer, ForeignKey("core__locations.id"))
537 get_location = relationship("Location", lazy="joined")
538
539 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow,
540 index=True)
541 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
542
543 fail_error = Column(Unicode)
544 fail_metadata = Column(JSONEncoded)
545
546 transcoding_progress = Column(Float, default=0)
547 main_transcoding_progress = Column(Float, default=0)
548
549 queued_media_file = Column(PathTupleWithSlashes)
550
551 queued_task_id = Column(Unicode)
552
553 __table_args__ = (
554 UniqueConstraint('actor', 'slug'),
555 {})
556
557 deletion_mode = Base.SOFT_DELETE
558
559 get_actor = relationship(User)
560
561 media_files_helper = relationship("MediaFile",
562 collection_class=attribute_mapped_collection("name"),
563 cascade="all, delete-orphan"
564 )
565 media_files = association_proxy('media_files_helper', 'file_path',
566 creator=lambda k, v: MediaFile(name=k, file_path=v)
567 )
568
569 attachment_files_helper = relationship("MediaAttachmentFile",
570 cascade="all, delete-orphan",
571 order_by="MediaAttachmentFile.created"
572 )
573 attachment_files = association_proxy("attachment_files_helper", "dict_view",
574 creator=lambda v: MediaAttachmentFile(
575 name=v["name"], filepath=v["filepath"])
576 )
577
578 subtitle_files_helper = relationship("MediaSubtitleFile",
579 cascade="all, delete-orphan",
580 order_by="MediaSubtitleFile.created"
581 )
582 subtitle_files = association_proxy("subtitle_files_helper", "dict_view",
583 creator=lambda v: MediaSubtitleFile(
584 name=v["name"], filepath=v["filepath"])
585 )
586
587 tags_helper = relationship("MediaTag",
588 cascade="all, delete-orphan" # should be automatically deleted
589 )
590 tags = association_proxy("tags_helper", "dict_view",
591 creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
592 )
593
594 media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
595 default=MutationDict())
596
597 ## TODO
598 # fail_error
599
600 @property
601 def get_uploader(self):
602 # for compatibility
603 return self.get_actor
604
605 @property
606 def uploader(self):
607 # for compatibility
608 return self.actor
609
610 @property
611 def collections(self):
612 """ Get any collections that this MediaEntry is in """
613 return list(Collection.query.join(Collection.collection_items).join(
614 CollectionItem.object_helper
615 ).filter(
616 and_(
617 GenericModelReference.model_type == self.__tablename__,
618 GenericModelReference.obj_pk == self.id
619 )
620 ))
621
622 def get_comments(self, ascending=False):
623 query = Comment.query.join(Comment.target_helper).filter(and_(
624 GenericModelReference.obj_pk == self.id,
625 GenericModelReference.model_type == self.__tablename__
626 ))
627
628 if ascending:
629 query = query.order_by(Comment.added.asc())
630 else:
631 query = query.order_by(Comment.added.desc())
632
633 return query
634
635 def url_to_prev(self, urlgen):
636 """get the next 'newer' entry by this user"""
637 media = MediaEntry.query.filter(
638 (MediaEntry.actor == self.actor)
639 & (MediaEntry.state == u'processed')
640 & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()
641
642 if media is not None:
643 return media.url_for_self(urlgen)
644
645 def url_to_next(self, urlgen):
646 """get the next 'older' entry by this user"""
647 media = MediaEntry.query.filter(
648 (MediaEntry.actor == self.actor)
649 & (MediaEntry.state == u'processed')
650 & (MediaEntry.id < self.id)).order_by(desc(MediaEntry.id)).first()
651
652 if media is not None:
653 return media.url_for_self(urlgen)
654
655 def get_file_metadata(self, file_key, metadata_key=None):
656 """
657 Return the file_metadata dict of a MediaFile. If metadata_key is given,
658 return the value of the key.
659 """
660 media_file = MediaFile.query.filter_by(media_entry=self.id,
661 name=six.text_type(file_key)).first()
662
663 if media_file:
664 if metadata_key:
665 return media_file.file_metadata.get(metadata_key, None)
666
667 return media_file.file_metadata
668
669 def set_file_metadata(self, file_key, **kwargs):
670 """
671 Update the file_metadata of a MediaFile.
672 """
673 media_file = MediaFile.query.filter_by(media_entry=self.id,
674 name=six.text_type(file_key)).first()
675
676 file_metadata = media_file.file_metadata or {}
677
678 for key, value in six.iteritems(kwargs):
679 file_metadata[key] = value
680
681 media_file.file_metadata = file_metadata
682 media_file.save()
683
684 @property
685 def media_data(self):
686 return getattr(self, self.media_data_ref)
687
688 def media_data_init(self, **kwargs):
689 """
690 Initialize or update the contents of a media entry's media_data row
691 """
692 media_data = self.media_data
693
694 if media_data is None:
695 # Get the correct table:
696 table = import_component(self.media_type + '.models:DATA_MODEL')
697 # No media data, so actually add a new one
698 media_data = table(**kwargs)
699 # Get the relationship set up.
700 media_data.get_media_entry = self
701 else:
702 # Update old media data
703 for field, value in six.iteritems(kwargs):
704 setattr(media_data, field, value)
705
706 @memoized_property
707 def media_data_ref(self):
708 return import_component(self.media_type + '.models:BACKREF_NAME')
709
710 def __repr__(self):
711 if six.PY2:
712 # obj.__repr__() should return a str on Python 2
713 safe_title = self.title.encode('utf-8', 'replace')
714 else:
715 safe_title = self.title
716
717 return '<{classname} {id}: {title}>'.format(
718 classname=self.__class__.__name__,
719 id=self.id,
720 title=safe_title)
721
722 def soft_delete(self, *args, **kwargs):
723 # Find all of the media comments for this and delete them
724 for comment in self.get_comments():
725 comment.delete(*args, **kwargs)
726
727 super(MediaEntry, self).soft_delete(*args, **kwargs)
728
729 def delete(self, del_orphan_tags=True, **kwargs):
730 """Delete MediaEntry and all related files/attachments/comments
731
732 This will *not* automatically delete unused collections, which
733 can remain empty...
734
735 :param del_orphan_tags: True/false if we delete unused Tags too
736 :param commit: True/False if this should end the db transaction"""
737 # User's CollectionItems are automatically deleted via "cascade".
738 # Comments on this Media are deleted by cascade, hopefully.
739
740 # Delete all related files/attachments
741 try:
742 delete_media_files(self)
743 except OSError as error:
744 # Returns list of files we failed to delete
745 _log.error('No such files from the user "{1}" to delete: '
746 '{0}'.format(str(error), self.get_actor))
747 _log.info('Deleted Media entry id "{0}"'.format(self.id))
748 # Related MediaTag's are automatically cleaned, but we might
749 # want to clean out unused Tag's too.
750 if del_orphan_tags:
751 # TODO: Import here due to cyclic imports!!!
752 # This cries for refactoring
753 from mediagoblin.db.util import clean_orphan_tags
754 clean_orphan_tags(commit=False)
755 # pass through commit=False/True in kwargs
756 super(MediaEntry, self).delete(**kwargs)
757
758 def serialize(self, request, show_comments=True):
759 """ Unserialize MediaEntry to object """
760 author = self.get_actor
761 published = UTC.localize(self.created)
762 updated = UTC.localize(self.updated)
763 public_id = self.get_public_id(request.urlgen)
764 context = {
765 "id": public_id,
766 "author": author.serialize(request),
767 "objectType": self.object_type,
768 "url": self.url_for_self(request.urlgen, qualified=True),
769 "image": {
770 "url": urljoin(request.host_url, self.thumb_url),
771 },
772 "fullImage":{
773 "url": urljoin(request.host_url, self.original_url),
774 },
775 "published": published.isoformat(),
776 "updated": updated.isoformat(),
777 "pump_io": {
778 "shared": False,
779 },
780 "links": {
781 "self": {
782 "href": public_id,
783 },
784 }
785 }
786
787 if self.title:
788 context["displayName"] = self.title
789
790 if self.description:
791 context["content"] = self.description
792
793 if self.license:
794 context["license"] = self.license
795
796 if self.location:
797 context["location"] = self.get_location.serialize(request)
798
799 # Always show tags, even if empty list
800 if self.tags:
801 context["tags"] = [tag['name'] for tag in self.tags]
802 else:
803 context["tags"] = []
804
805 if show_comments:
806 comments = [
807 l.comment().serialize(request) for l in self.get_comments()]
808 total = len(comments)
809 context["replies"] = {
810 "totalItems": total,
811 "items": comments,
812 "url": request.urlgen(
813 "mediagoblin.api.object.comments",
814 object_type=self.object_type,
815 id=self.id,
816 qualified=True
817 ),
818 }
819
820 # Add image height and width if possible. We didn't use to store this
821 # data and we're not able (and maybe not willing) to re-process all
822 # images so it's possible this might not exist.
823 if self.get_file_metadata("thumb", "height"):
824 height = self.get_file_metadata("thumb", "height")
825 context["image"]["height"] = height
826 if self.get_file_metadata("thumb", "width"):
827 width = self.get_file_metadata("thumb", "width")
828 context["image"]["width"] = width
829 if self.get_file_metadata("original", "height"):
830 height = self.get_file_metadata("original", "height")
831 context["fullImage"]["height"] = height
832 if self.get_file_metadata("original", "height"):
833 width = self.get_file_metadata("original", "width")
834 context["fullImage"]["width"] = width
835
836 return context
837
838 def unserialize(self, data):
839 """ Takes API objects and unserializes on existing MediaEntry """
840 if "displayName" in data:
841 self.title = data["displayName"]
842
843 if "content" in data:
844 self.description = data["content"]
845
846 if "license" in data:
847 self.license = data["license"]
848
849 if "location" in data:
850 License.create(data["location"], self)
851
852 if "tags" in data:
853 self.tags = convert_to_tag_list_of_dicts(', '.join(data["tags"]))
854
855 return True
856
857 class FileKeynames(Base):
858 """
859 keywords for various places.
860 currently the MediaFile keys
861 """
862 __tablename__ = "core__file_keynames"
863 id = Column(Integer, primary_key=True)
864 name = Column(Unicode, unique=True)
865
866 def __repr__(self):
867 return "<FileKeyname %r: %r>" % (self.id, self.name)
868
869 @classmethod
870 def find_or_new(cls, name):
871 t = cls.query.filter_by(name=name).first()
872 if t is not None:
873 return t
874 return cls(name=name)
875
876
877 class MediaFile(Base):
878 """
879 TODO: Highly consider moving "name" into a new table.
880 TODO: Consider preloading said table in software
881 """
882 __tablename__ = "core__mediafiles"
883
884 media_entry = Column(
885 Integer, ForeignKey(MediaEntry.id),
886 nullable=False)
887 name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
888 file_path = Column(PathTupleWithSlashes)
889 file_metadata = Column(MutationDict.as_mutable(JSONEncoded))
890
891 __table_args__ = (
892 PrimaryKeyConstraint('media_entry', 'name_id'),
893 {})
894
895 def __repr__(self):
896 return "<MediaFile %s: %r>" % (self.name, self.file_path)
897
898 name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
899 name = association_proxy('name_helper', 'name',
900 creator=FileKeynames.find_or_new
901 )
902
903
904 class MediaAttachmentFile(Base):
905 __tablename__ = "core__attachment_files"
906
907 id = Column(Integer, primary_key=True)
908 media_entry = Column(
909 Integer, ForeignKey(MediaEntry.id),
910 nullable=False)
911 name = Column(Unicode, nullable=False)
912 filepath = Column(PathTupleWithSlashes)
913 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
914
915 @property
916 def dict_view(self):
917 """A dict like view on this object"""
918 return DictReadAttrProxy(self)
919
920 class MediaSubtitleFile(Base):
921 __tablename__ = "core__subtitle_files"
922
923 id = Column(Integer, primary_key=True)
924 media_entry = Column(
925 Integer, ForeignKey(MediaEntry.id),
926 nullable=False)
927 name = Column(Unicode, nullable=False)
928 filepath = Column(PathTupleWithSlashes)
929 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
930
931 @property
932 def dict_view(self):
933 """A dict like view on this object"""
934 return DictReadAttrProxy(self)
935
936
937 class Tag(Base):
938 __tablename__ = "core__tags"
939
940 id = Column(Integer, primary_key=True)
941 slug = Column(Unicode, nullable=False, unique=True)
942
943 def __repr__(self):
944 return "<Tag %r: %r>" % (self.id, self.slug)
945
946 @classmethod
947 def find_or_new(cls, slug):
948 t = cls.query.filter_by(slug=slug).first()
949 if t is not None:
950 return t
951 return cls(slug=slug)
952
953
954 class MediaTag(Base):
955 __tablename__ = "core__media_tags"
956
957 id = Column(Integer, primary_key=True)
958 media_entry = Column(
959 Integer, ForeignKey(MediaEntry.id),
960 nullable=False, index=True)
961 tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
962 name = Column(Unicode)
963 # created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
964
965 __table_args__ = (
966 UniqueConstraint('tag', 'media_entry'),
967 {})
968
969 tag_helper = relationship(Tag)
970 slug = association_proxy('tag_helper', 'slug',
971 creator=Tag.find_or_new
972 )
973
974 def __init__(self, name=None, slug=None):
975 Base.__init__(self)
976 if name is not None:
977 self.name = name
978 if slug is not None:
979 self.tag_helper = Tag.find_or_new(slug)
980
981 @property
982 def dict_view(self):
983 """A dict like view on this object"""
984 return DictReadAttrProxy(self)
985
986 class Comment(Base):
987 """
988 Link table between a response and another object that can have replies.
989
990 This acts as a link table between an object and the comments on it, it's
991 done like this so that you can look up all the comments without knowing
992 whhich comments are on an object before hand. Any object can be a comment
993 and more or less any object can accept comments too.
994
995 Important: This is NOT the old MediaComment table.
996 """
997 __tablename__ = "core__comment_links"
998
999 id = Column(Integer, primary_key=True)
1000
1001 # The GMR to the object the comment is on.
1002 target_id = Column(
1003 Integer,
1004 ForeignKey(GenericModelReference.id),
1005 nullable=False
1006 )
1007 target_helper = relationship(
1008 GenericModelReference,
1009 foreign_keys=[target_id]
1010 )
1011 target = association_proxy("target_helper", "get_object",
1012 creator=GenericModelReference.find_or_new)
1013
1014 # The comment object
1015 comment_id = Column(
1016 Integer,
1017 ForeignKey(GenericModelReference.id),
1018 nullable=False
1019 )
1020 comment_helper = relationship(
1021 GenericModelReference,
1022 foreign_keys=[comment_id]
1023 )
1024 comment = association_proxy("comment_helper", "get_object",
1025 creator=GenericModelReference.find_or_new)
1026
1027 # When it was added
1028 added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1029
1030 @property
1031 def get_author(self):
1032 # for compatibility
1033 return self.comment().get_actor # noqa
1034
1035 def __getattr__(self, attr):
1036 if attr.startswith('_'):
1037 # if attr starts with '_', then it's probably some internal
1038 # sqlalchemy variable. Since __getattr__ is called when
1039 # non-existing attributes are being accessed, we should not try to
1040 # fetch it from self.comment()
1041 raise AttributeError
1042 try:
1043 _log.debug('Old attr is being accessed: {0}'.format(attr))
1044 return getattr(self.comment(), attr) # noqa
1045 except Exception as e:
1046 _log.error(e)
1047 raise
1048
1049 class TextComment(Base, TextCommentMixin, CommentingMixin):
1050 """
1051 A basic text comment, this is a usually short amount of text and nothing else
1052 """
1053 # This is a legacy from when Comments where just on MediaEntry objects.
1054 __tablename__ = "core__media_comments"
1055
1056 id = Column(Integer, primary_key=True)
1057 public_id = Column(Unicode, unique=True)
1058 actor = Column(Integer, ForeignKey(User.id), nullable=False)
1059 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1060 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1061 content = Column(UnicodeText, nullable=False)
1062 location = Column(Integer, ForeignKey("core__locations.id"))
1063 get_location = relationship("Location", lazy="joined")
1064
1065 # Cascade: Comments are owned by their creator. So do the full thing.
1066 # lazy=dynamic: People might post a *lot* of comments,
1067 # so make the "posted_comments" a query-like thing.
1068 get_actor = relationship(User,
1069 backref=backref("posted_comments",
1070 lazy="dynamic",
1071 cascade="all, delete-orphan"))
1072 deletion_mode = Base.SOFT_DELETE
1073
1074 def serialize(self, request):
1075 """ Unserialize to python dictionary for API """
1076 target = self.get_reply_to()
1077 # If this is target just.. give them nothing?
1078 if target is None:
1079 target = {}
1080 else:
1081 target = target.serialize(request, show_comments=False)
1082
1083
1084 author = self.get_actor
1085 published = UTC.localize(self.created)
1086 context = {
1087 "id": self.get_public_id(request.urlgen),
1088 "objectType": self.object_type,
1089 "content": self.content,
1090 "inReplyTo": target,
1091 "author": author.serialize(request),
1092 "published": published.isoformat(),
1093 "updated": published.isoformat(),
1094 }
1095
1096 if self.location:
1097 context["location"] = self.get_location.seralize(request)
1098
1099 return context
1100
1101 def unserialize(self, data, request):
1102 """ Takes API objects and unserializes on existing comment """
1103 if "content" in data:
1104 self.content = data["content"]
1105
1106 if "location" in data:
1107 Location.create(data["location"], self)
1108
1109
1110 # Handle changing the reply ID
1111 if "inReplyTo" in data:
1112 # Validate that the ID is correct
1113 try:
1114 id = extract_url_arguments(
1115 url=data["inReplyTo"]["id"],
1116 urlmap=request.app.url_map
1117 )["id"]
1118 except ValueError:
1119 raise False
1120
1121 public_id = request.urlgen(
1122 "mediagoblin.api.object",
1123 id=id,
1124 object_type=data["inReplyTo"]["objectType"],
1125 qualified=True
1126 )
1127
1128 media = MediaEntry.query.filter_by(public_id=public_id).first()
1129 if media is None:
1130 return False
1131
1132 # We need an ID for this model.
1133 self.save(commit=False)
1134
1135 # Create the link
1136 link = Comment()
1137 link.target = media
1138 link.comment = self
1139 link.save()
1140
1141 return True
1142
1143 class Collection(Base, CollectionMixin, CommentingMixin):
1144 """A representation of a collection of objects.
1145
1146 This holds a group/collection of objects that could be a user defined album
1147 or their inbox, outbox, followers, etc. These are always ordered and accessable
1148 via the API and web.
1149
1150 The collection has a number of types which determine what kind of collection
1151 it is, for example the users inbox will be of `Collection.INBOX_TYPE` that will
1152 be stored on the `Collection.type` field. It's important to set the correct type.
1153
1154 On deletion, contained CollectionItems get automatically reaped via
1155 SQL cascade"""
1156 __tablename__ = "core__collections"
1157
1158 id = Column(Integer, primary_key=True)
1159 public_id = Column(Unicode, unique=True)
1160 title = Column(Unicode, nullable=False)
1161 slug = Column(Unicode)
1162 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow,
1163 index=True)
1164 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1165 description = Column(UnicodeText)
1166 actor = Column(Integer, ForeignKey(User.id), nullable=False)
1167 num_items = Column(Integer, default=0)
1168
1169 # There are lots of different special types of collections in the pump.io API
1170 # for example: followers, following, inbox, outbox, etc. See type constants
1171 # below the fields on this model.
1172 type = Column(Unicode, nullable=False)
1173
1174 # Location
1175 location = Column(Integer, ForeignKey("core__locations.id"))
1176 get_location = relationship("Location", lazy="joined")
1177
1178 # Cascade: Collections are owned by their creator. So do the full thing.
1179 get_actor = relationship(User,
1180 backref=backref("collections",
1181 cascade="all, delete-orphan"))
1182 __table_args__ = (
1183 UniqueConstraint("actor", "slug"),
1184 {})
1185
1186 deletion_mode = Base.SOFT_DELETE
1187
1188 # These are the types, It's strongly suggested if new ones are invented they
1189 # are prefixed to ensure they're unique from other types. Any types used in
1190 # the main mediagoblin should be prefixed "core-"
1191 INBOX_TYPE = "core-inbox"
1192 OUTBOX_TYPE = "core-outbox"
1193 FOLLOWER_TYPE = "core-followers"
1194 FOLLOWING_TYPE = "core-following"
1195 COMMENT_TYPE = "core-comments"
1196 USER_DEFINED_TYPE = "core-user-defined"
1197
1198 def get_collection_items(self, ascending=False):
1199 #TODO, is this still needed with self.collection_items being available?
1200 order_col = CollectionItem.position
1201 if not ascending:
1202 order_col = desc(order_col)
1203 return CollectionItem.query.filter_by(
1204 collection=self.id).order_by(order_col)
1205
1206 def __repr__(self):
1207 safe_title = self.title.encode('ascii', 'replace')
1208 return '<{classname} #{id}: {title} by {actor}>'.format(
1209 id=self.id,
1210 classname=self.__class__.__name__,
1211 actor=self.actor,
1212 title=safe_title)
1213
1214 def serialize(self, request):
1215 # Get all serialized output in a list
1216 items = [i.serialize(request) for i in self.get_collection_items()]
1217 return {
1218 "totalItems": self.num_items,
1219 "url": self.url_for_self(request.urlgen, qualified=True),
1220 "items": items,
1221 }
1222
1223
1224 class CollectionItem(Base, CollectionItemMixin):
1225 __tablename__ = "core__collection_items"
1226
1227 id = Column(Integer, primary_key=True)
1228
1229 collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
1230 note = Column(UnicodeText, nullable=True)
1231 added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1232 position = Column(Integer)
1233 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
1234 in_collection = relationship(Collection,
1235 backref=backref(
1236 "collection_items",
1237 cascade="all, delete-orphan"))
1238
1239 # Link to the object (could be anything.
1240 object_id = Column(
1241 Integer,
1242 ForeignKey(GenericModelReference.id),
1243 nullable=False,
1244 index=True
1245 )
1246 object_helper = relationship(
1247 GenericModelReference,
1248 foreign_keys=[object_id]
1249 )
1250 get_object = association_proxy(
1251 "object_helper",
1252 "get_object",
1253 creator=GenericModelReference.find_or_new
1254 )
1255
1256 __table_args__ = (
1257 UniqueConstraint('collection', 'object_id'),
1258 {})
1259
1260 @property
1261 def dict_view(self):
1262 """A dict like view on this object"""
1263 return DictReadAttrProxy(self)
1264
1265 def __repr__(self):
1266 return '<{classname} #{id}: Object {obj} in {collection}>'.format(
1267 id=self.id,
1268 classname=self.__class__.__name__,
1269 collection=self.collection,
1270 obj=self.get_object()
1271 )
1272
1273 def serialize(self, request):
1274 return self.get_object().serialize(request)
1275
1276
1277 class ProcessingMetaData(Base):
1278 __tablename__ = 'core__processing_metadata'
1279
1280 id = Column(Integer, primary_key=True)
1281 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
1282 index=True)
1283 media_entry = relationship(MediaEntry,
1284 backref=backref('processing_metadata',
1285 cascade='all, delete-orphan'))
1286 callback_url = Column(Unicode)
1287
1288 @property
1289 def dict_view(self):
1290 """A dict like view on this object"""
1291 return DictReadAttrProxy(self)
1292
1293
1294 class CommentSubscription(Base):
1295 __tablename__ = 'core__comment_subscriptions'
1296 id = Column(Integer, primary_key=True)
1297
1298 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1299
1300 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
1301 media_entry = relationship(MediaEntry,
1302 backref=backref('comment_subscriptions',
1303 cascade='all, delete-orphan'))
1304
1305 user_id = Column(Integer, ForeignKey(User.id), nullable=False)
1306 user = relationship(User,
1307 backref=backref('comment_subscriptions',
1308 cascade='all, delete-orphan'))
1309
1310 notify = Column(Boolean, nullable=False, default=True)
1311 send_email = Column(Boolean, nullable=False, default=True)
1312
1313 def __repr__(self):
1314 return ('<{classname} #{id}: {user} {media} notify: '
1315 '{notify} email: {email}>').format(
1316 id=self.id,
1317 classname=self.__class__.__name__,
1318 user=self.user,
1319 media=self.media_entry,
1320 notify=self.notify,
1321 email=self.send_email)
1322
1323
1324 class Notification(Base):
1325 __tablename__ = 'core__notifications'
1326 id = Column(Integer, primary_key=True)
1327
1328 object_id = Column(Integer, ForeignKey(GenericModelReference.id))
1329 object_helper = relationship(GenericModelReference)
1330 obj = association_proxy("object_helper", "get_object",
1331 creator=GenericModelReference.find_or_new)
1332
1333 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1334 user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False,
1335 index=True)
1336 seen = Column(Boolean, default=lambda: False, index=True)
1337 user = relationship(
1338 User,
1339 backref=backref('notifications', cascade='all, delete-orphan'))
1340
1341 def __repr__(self):
1342 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1343 id=self.id,
1344 klass=self.__class__.__name__,
1345 user=self.user,
1346 subject=getattr(self, 'subject', None),
1347 seen='unseen' if not self.seen else 'seen')
1348
1349 def __unicode__(self):
1350 return u'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1351 id=self.id,
1352 klass=self.__class__.__name__,
1353 user=self.user,
1354 subject=getattr(self, 'subject', None),
1355 seen='unseen' if not self.seen else 'seen')
1356
1357 class Report(Base):
1358 """
1359 Represents a report that someone might file against Media, Comments, etc.
1360
1361 :keyword reporter_id Holds the id of the user who created
1362 the report, as an Integer column.
1363 :keyword report_content Hold the explanation left by the repor-
1364 -ter to indicate why they filed the
1365 report in the first place, as a
1366 Unicode column.
1367 :keyword reported_user_id Holds the id of the user who created
1368 the content which was reported, as
1369 an Integer column.
1370 :keyword created Holds a datetime column of when the re-
1371 -port was filed.
1372 :keyword resolver_id Holds the id of the moderator/admin who
1373 resolved the report.
1374 :keyword resolved Holds the DateTime object which descri-
1375 -bes when this report was resolved
1376 :keyword result Holds the UnicodeText column of the
1377 resolver's reasons for resolving
1378 the report this way. Some of this
1379 is auto-generated
1380 :keyword object_id Holds the ID of the GenericModelReference
1381 which points to the reported object.
1382 """
1383 __tablename__ = 'core__reports'
1384
1385 id = Column(Integer, primary_key=True)
1386 reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
1387 reporter = relationship(
1388 User,
1389 backref=backref("reports_filed_by",
1390 lazy="dynamic",
1391 cascade="all, delete-orphan"),
1392 primaryjoin="User.id==Report.reporter_id")
1393 report_content = Column(UnicodeText)
1394 reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
1395 reported_user = relationship(
1396 User,
1397 backref=backref("reports_filed_on",
1398 lazy="dynamic",
1399 cascade="all, delete-orphan"),
1400 primaryjoin="User.id==Report.reported_user_id")
1401 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1402 resolver_id = Column(Integer, ForeignKey(User.id))
1403 resolver = relationship(
1404 User,
1405 backref=backref("reports_resolved_by",
1406 lazy="dynamic",
1407 cascade="all, delete-orphan"),
1408 primaryjoin="User.id==Report.resolver_id")
1409
1410 resolved = Column(DateTime)
1411 result = Column(UnicodeText)
1412
1413 object_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=True)
1414 object_helper = relationship(GenericModelReference)
1415 obj = association_proxy("object_helper", "get_object",
1416 creator=GenericModelReference.find_or_new)
1417
1418 def is_archived_report(self):
1419 return self.resolved is not None
1420
1421 def is_comment_report(self):
1422 if self.object_id is None:
1423 return False
1424 return isinstance(self.obj(), TextComment)
1425
1426 def is_media_entry_report(self):
1427 if self.object_id is None:
1428 return False
1429 return isinstance(self.obj(), MediaEntry)
1430
1431 def archive(self,resolver_id, resolved, result):
1432 self.resolver_id = resolver_id
1433 self.resolved = resolved
1434 self.result = result
1435
1436 class UserBan(Base):
1437 """
1438 Holds the information on a specific user's ban-state. As long as one of
1439 these is attached to a user, they are banned from accessing mediagoblin.
1440 When they try to log in, they are greeted with a page that tells them
1441 the reason why they are banned and when (if ever) the ban will be
1442 lifted
1443
1444 :keyword user_id Holds the id of the user this object is
1445 attached to. This is a one-to-one
1446 relationship.
1447 :keyword expiration_date Holds the date that the ban will be lifted.
1448 If this is null, the ban is permanent
1449 unless a moderator manually lifts it.
1450 :keyword reason Holds the reason why the user was banned.
1451 """
1452 __tablename__ = 'core__user_bans'
1453
1454 user_id = Column(Integer, ForeignKey(User.id), nullable=False,
1455 primary_key=True)
1456 expiration_date = Column(Date)
1457 reason = Column(UnicodeText, nullable=False)
1458
1459
1460 class Privilege(Base):
1461 """
1462 The Privilege table holds all of the different privileges a user can hold.
1463 If a user 'has' a privilege, the User object is in a relationship with the
1464 privilege object.
1465
1466 :keyword privilege_name Holds a unicode object that is the recognizable
1467 name of this privilege. This is the column
1468 used for identifying whether or not a user
1469 has a necessary privilege or not.
1470
1471 """
1472 __tablename__ = 'core__privileges'
1473
1474 id = Column(Integer, nullable=False, primary_key=True)
1475 privilege_name = Column(Unicode, nullable=False, unique=True)
1476 all_users = relationship(
1477 User,
1478 backref='all_privileges',
1479 secondary="core__privileges_users")
1480
1481 def __init__(self, privilege_name):
1482 '''
1483 Currently consructors are required for tables that are initialized thru
1484 the FOUNDATIONS system. This is because they need to be able to be con-
1485 -structed by a list object holding their arg*s
1486 '''
1487 self.privilege_name = privilege_name
1488
1489 def __repr__(self):
1490 return "<Privilege %s>" % (self.privilege_name)
1491
1492
1493 class PrivilegeUserAssociation(Base):
1494 '''
1495 This table holds the many-to-many relationship between User and Privilege
1496 '''
1497
1498 __tablename__ = 'core__privileges_users'
1499
1500 user = Column(
1501 "user",
1502 Integer,
1503 ForeignKey(User.id),
1504 primary_key=True)
1505 privilege = Column(
1506 "privilege",
1507 Integer,
1508 ForeignKey(Privilege.id),
1509 primary_key=True)
1510
1511 class Generator(Base):
1512 """ Information about what created an activity """
1513 __tablename__ = "core__generators"
1514
1515 id = Column(Integer, primary_key=True)
1516 name = Column(Unicode, nullable=False)
1517 published = Column(DateTime, default=datetime.datetime.utcnow)
1518 updated = Column(DateTime, default=datetime.datetime.utcnow)
1519 object_type = Column(Unicode, nullable=False)
1520
1521 deletion_mode = Base.SOFT_DELETE
1522
1523 def __repr__(self):
1524 return "<{klass} {name}>".format(
1525 klass=self.__class__.__name__,
1526 name=self.name
1527 )
1528
1529 def serialize(self, request):
1530 href = request.urlgen(
1531 "mediagoblin.api.object",
1532 object_type=self.object_type,
1533 id=self.id,
1534 qualified=True
1535 )
1536 published = UTC.localize(self.published)
1537 updated = UTC.localize(self.updated)
1538 return {
1539 "id": href,
1540 "displayName": self.name,
1541 "published": published.isoformat(),
1542 "updated": updated.isoformat(),
1543 "objectType": self.object_type,
1544 }
1545
1546 def unserialize(self, data):
1547 if "displayName" in data:
1548 self.name = data["displayName"]
1549
1550 class Activity(Base, ActivityMixin):
1551 """
1552 This holds all the metadata about an activity such as uploading an image,
1553 posting a comment, etc.
1554 """
1555 __tablename__ = "core__activities"
1556
1557 id = Column(Integer, primary_key=True)
1558 public_id = Column(Unicode, unique=True)
1559 actor = Column(Integer,
1560 ForeignKey("core__users.id"),
1561 nullable=False)
1562 published = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1563 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1564
1565 verb = Column(Unicode, nullable=False)
1566 content = Column(Unicode, nullable=True)
1567 title = Column(Unicode, nullable=True)
1568 generator = Column(Integer,
1569 ForeignKey("core__generators.id"),
1570 nullable=True)
1571
1572 # Create the generic foreign keys for the object
1573 object_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=False)
1574 object_helper = relationship(GenericModelReference, foreign_keys=[object_id])
1575 object = association_proxy("object_helper", "get_object",
1576 creator=GenericModelReference.find_or_new)
1577
1578 # Create the generic foreign Key for the target
1579 target_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=True)
1580 target_helper = relationship(GenericModelReference, foreign_keys=[target_id])
1581 target = association_proxy("target_helper", "get_object",
1582 creator=GenericModelReference.find_or_new)
1583
1584 get_actor = relationship(User,
1585 backref=backref("activities",
1586 cascade="all, delete-orphan"))
1587 get_generator = relationship(Generator)
1588
1589 deletion_mode = Base.SOFT_DELETE
1590
1591 def __repr__(self):
1592 if self.content is None:
1593 return "<{klass} verb:{verb}>".format(
1594 klass=self.__class__.__name__,
1595 verb=self.verb
1596 )
1597 else:
1598 return "<{klass} {content}>".format(
1599 klass=self.__class__.__name__,
1600 content=self.content
1601 )
1602
1603 def save(self, set_updated=True, *args, **kwargs):
1604 if set_updated:
1605 self.updated = datetime.datetime.now()
1606 super(Activity, self).save(*args, **kwargs)
1607
1608 class Graveyard(Base):
1609 """ Where models come to die """
1610 __tablename__ = "core__graveyard"
1611
1612 id = Column(Integer, primary_key=True)
1613 public_id = Column(Unicode, nullable=True, unique=True)
1614
1615 deleted = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1616 object_type = Column(Unicode, nullable=False)
1617
1618 # This could either be a deleted actor or a real actor, this must be
1619 # nullable as it we shouldn't have it set for deleted actor
1620 actor_id = Column(Integer, ForeignKey(GenericModelReference.id))
1621 actor_helper = relationship(GenericModelReference)
1622 actor = association_proxy("actor_helper", "get_object",
1623 creator=GenericModelReference.find_or_new)
1624
1625 def __repr__(self):
1626 return "<{klass} deleted {obj_type}>".format(
1627 klass=type(self).__name__,
1628 obj_type=self.object_type
1629 )
1630
1631 def serialize(self, request):
1632 deleted = UTC.localize(self.deleted).isoformat()
1633 context = {
1634 "id": self.public_id,
1635 "objectType": self.object_type,
1636 "published": deleted,
1637 "updated": deleted,
1638 "deleted": deleted,
1639 }
1640
1641 if self.actor_id is not None:
1642 context["actor"] = self.actor().serialize(request)
1643
1644 return context
1645 MODELS = [
1646 LocalUser, RemoteUser, User, MediaEntry, Tag, MediaTag, Comment, TextComment,
1647 Collection, CollectionItem, MediaFile, FileKeynames, MediaAttachmentFile, MediaSubtitleFile,
1648 ProcessingMetaData, Notification, Client, CommentSubscription, Report,
1649 UserBan, Privilege, PrivilegeUserAssociation, RequestToken, AccessToken,
1650 NonceTimestamp, Activity, Generator, Location, GenericModelReference, Graveyard]
1651
1652 """
1653 Foundations are the default rows that are created immediately after the tables
1654 are initialized. Each entry to this dictionary should be in the format of:
1655 ModelConstructorObject:List of Dictionaries
1656 (Each Dictionary represents a row on the Table to be created, containing each
1657 of the columns' names as a key string, and each of the columns' values as a
1658 value)
1659
1660 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1661 user_foundations = [{'name':u'Joanna', 'age':24},
1662 {'name':u'Andrea', 'age':41}]
1663
1664 FOUNDATIONS = {User:user_foundations}
1665 """
1666 privilege_foundations = [{'privilege_name':u'admin'},
1667 {'privilege_name':u'moderator'},
1668 {'privilege_name':u'uploader'},
1669 {'privilege_name':u'reporter'},
1670 {'privilege_name':u'commenter'},
1671 {'privilege_name':u'active'}]
1672 FOUNDATIONS = {Privilege:privilege_foundations}
1673
1674 ######################################################
1675 # Special, migrations-tracking table
1676 #
1677 # Not listed in MODELS because this is special and not
1678 # really migrated, but used for migrations (for now)
1679 ######################################################
1680
1681 class MigrationData(Base):
1682 __tablename__ = "core__migrations"
1683
1684 name = Column(Unicode, primary_key=True)
1685 version = Column(Integer, nullable=False, default=0)
1686
1687 ######################################################
1688
1689
1690 def show_table_init(engine_uri):
1691 if engine_uri is None:
1692 engine_uri = 'sqlite:///:memory:'
1693 from sqlalchemy import create_engine
1694 engine = create_engine(engine_uri, echo=True)
1695
1696 Base.metadata.create_all(engine)
1697
1698
1699 if __name__ == '__main__':
1700 from sys import argv
1701 print(repr(argv))
1702 if len(argv) == 2:
1703 uri = argv[1]
1704 else:
1705 uri = None
1706 show_table_init(uri)