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