Fix server crash on blog about page [#5572]
[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 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(SmallInteger)
547
548 queued_media_file = Column(PathTupleWithSlashes)
549
550 queued_task_id = Column(Unicode)
551
552 __table_args__ = (
553 UniqueConstraint('actor', 'slug'),
554 {})
555
556 deletion_mode = Base.SOFT_DELETE
557
558 get_actor = relationship(User)
559
560 media_files_helper = relationship("MediaFile",
561 collection_class=attribute_mapped_collection("name"),
562 cascade="all, delete-orphan"
563 )
564 media_files = association_proxy('media_files_helper', 'file_path',
565 creator=lambda k, v: MediaFile(name=k, file_path=v)
566 )
567
568 attachment_files_helper = relationship("MediaAttachmentFile",
569 cascade="all, delete-orphan",
570 order_by="MediaAttachmentFile.created"
571 )
572 attachment_files = association_proxy("attachment_files_helper", "dict_view",
573 creator=lambda v: MediaAttachmentFile(
574 name=v["name"], filepath=v["filepath"])
575 )
576
577 tags_helper = relationship("MediaTag",
578 cascade="all, delete-orphan" # should be automatically deleted
579 )
580 tags = association_proxy("tags_helper", "dict_view",
581 creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
582 )
583
584 media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
585 default=MutationDict())
586
587 ## TODO
588 # fail_error
589
590 @property
591 def get_uploader(self):
592 # for compatibility
593 return self.get_actor
594
595 @property
596 def uploader(self):
597 # for compatibility
598 return self.actor
599
600 @property
601 def collections(self):
602 """ Get any collections that this MediaEntry is in """
603 return list(Collection.query.join(Collection.collection_items).join(
604 CollectionItem.object_helper
605 ).filter(
606 and_(
607 GenericModelReference.model_type == self.__tablename__,
608 GenericModelReference.obj_pk == self.id
609 )
610 ))
611
612 def get_comments(self, ascending=False):
613 query = Comment.query.join(Comment.target_helper).filter(and_(
614 GenericModelReference.obj_pk == self.id,
615 GenericModelReference.model_type == self.__tablename__
616 ))
617
618 if ascending:
619 query = query.order_by(Comment.added.asc())
620 else:
621 query = query.order_by(Comment.added.desc())
622
623 return query
624
625 def url_to_prev(self, urlgen):
626 """get the next 'newer' entry by this user"""
627 media = MediaEntry.query.filter(
628 (MediaEntry.actor == self.actor)
629 & (MediaEntry.state == u'processed')
630 & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()
631
632 if media is not None:
633 return media.url_for_self(urlgen)
634
635 def url_to_next(self, urlgen):
636 """get the next 'older' 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(desc(MediaEntry.id)).first()
641
642 if media is not None:
643 return media.url_for_self(urlgen)
644
645 def get_file_metadata(self, file_key, metadata_key=None):
646 """
647 Return the file_metadata dict of a MediaFile. If metadata_key is given,
648 return the value of the key.
649 """
650 media_file = MediaFile.query.filter_by(media_entry=self.id,
651 name=six.text_type(file_key)).first()
652
653 if media_file:
654 if metadata_key:
655 return media_file.file_metadata.get(metadata_key, None)
656
657 return media_file.file_metadata
658
659 def set_file_metadata(self, file_key, **kwargs):
660 """
661 Update the file_metadata of a MediaFile.
662 """
663 media_file = MediaFile.query.filter_by(media_entry=self.id,
664 name=six.text_type(file_key)).first()
665
666 file_metadata = media_file.file_metadata or {}
667
668 for key, value in six.iteritems(kwargs):
669 file_metadata[key] = value
670
671 media_file.file_metadata = file_metadata
672 media_file.save()
673
674 @property
675 def media_data(self):
676 return getattr(self, self.media_data_ref)
677
678 def media_data_init(self, **kwargs):
679 """
680 Initialize or update the contents of a media entry's media_data row
681 """
682 media_data = self.media_data
683
684 if media_data is None:
685 # Get the correct table:
686 table = import_component(self.media_type + '.models:DATA_MODEL')
687 # No media data, so actually add a new one
688 media_data = table(**kwargs)
689 # Get the relationship set up.
690 media_data.get_media_entry = self
691 else:
692 # Update old media data
693 for field, value in six.iteritems(kwargs):
694 setattr(media_data, field, value)
695
696 @memoized_property
697 def media_data_ref(self):
698 return import_component(self.media_type + '.models:BACKREF_NAME')
699
700 def __repr__(self):
701 if six.PY2:
702 # obj.__repr__() should return a str on Python 2
703 safe_title = self.title.encode('utf-8', 'replace')
704 else:
705 safe_title = self.title
706
707 return '<{classname} {id}: {title}>'.format(
708 classname=self.__class__.__name__,
709 id=self.id,
710 title=safe_title)
711
712 def soft_delete(self, *args, **kwargs):
713 # Find all of the media comments for this and delete them
714 for comment in self.get_comments():
715 comment.delete(*args, **kwargs)
716
717 super(MediaEntry, self).soft_delete(*args, **kwargs)
718
719 def delete(self, del_orphan_tags=True, **kwargs):
720 """Delete MediaEntry and all related files/attachments/comments
721
722 This will *not* automatically delete unused collections, which
723 can remain empty...
724
725 :param del_orphan_tags: True/false if we delete unused Tags too
726 :param commit: True/False if this should end the db transaction"""
727 # User's CollectionItems are automatically deleted via "cascade".
728 # Comments on this Media are deleted by cascade, hopefully.
729
730 # Delete all related files/attachments
731 try:
732 delete_media_files(self)
733 except OSError as error:
734 # Returns list of files we failed to delete
735 _log.error('No such files from the user "{1}" to delete: '
736 '{0}'.format(str(error), self.get_actor))
737 _log.info('Deleted Media entry id "{0}"'.format(self.id))
738 # Related MediaTag's are automatically cleaned, but we might
739 # want to clean out unused Tag's too.
740 if del_orphan_tags:
741 # TODO: Import here due to cyclic imports!!!
742 # This cries for refactoring
743 from mediagoblin.db.util import clean_orphan_tags
744 clean_orphan_tags(commit=False)
745 # pass through commit=False/True in kwargs
746 super(MediaEntry, self).delete(**kwargs)
747
748 def serialize(self, request, show_comments=True):
749 """ Unserialize MediaEntry to object """
750 author = self.get_actor
751 published = UTC.localize(self.created)
752 updated = UTC.localize(self.updated)
753 public_id = self.get_public_id(request.urlgen)
754 context = {
755 "id": public_id,
756 "author": author.serialize(request),
757 "objectType": self.object_type,
758 "url": self.url_for_self(request.urlgen, qualified=True),
759 "image": {
760 "url": urljoin(request.host_url, self.thumb_url),
761 },
762 "fullImage":{
763 "url": urljoin(request.host_url, self.original_url),
764 },
765 "published": published.isoformat(),
766 "updated": updated.isoformat(),
767 "pump_io": {
768 "shared": False,
769 },
770 "links": {
771 "self": {
772 "href": public_id,
773 },
774 }
775 }
776
777 if self.title:
778 context["displayName"] = self.title
779
780 if self.description:
781 context["content"] = self.description
782
783 if self.license:
784 context["license"] = self.license
785
786 if self.location:
787 context["location"] = self.get_location.serialize(request)
788
789 # Always show tags, even if empty list
790 if self.tags:
791 context["tags"] = [tag['name'] for tag in self.tags]
792 else:
793 context["tags"] = []
794
795 if show_comments:
796 comments = [
797 l.comment().serialize(request) for l in self.get_comments()]
798 total = len(comments)
799 context["replies"] = {
800 "totalItems": total,
801 "items": comments,
802 "url": request.urlgen(
803 "mediagoblin.api.object.comments",
804 object_type=self.object_type,
805 id=self.id,
806 qualified=True
807 ),
808 }
809
810 # Add image height and width if possible. We didn't use to store this
811 # data and we're not able (and maybe not willing) to re-process all
812 # images so it's possible this might not exist.
813 if self.get_file_metadata("thumb", "height"):
814 height = self.get_file_metadata("thumb", "height")
815 context["image"]["height"] = height
816 if self.get_file_metadata("thumb", "width"):
817 width = self.get_file_metadata("thumb", "width")
818 context["image"]["width"] = width
819 if self.get_file_metadata("original", "height"):
820 height = self.get_file_metadata("original", "height")
821 context["fullImage"]["height"] = height
822 if self.get_file_metadata("original", "height"):
823 width = self.get_file_metadata("original", "width")
824 context["fullImage"]["width"] = width
825
826 return context
827
828 def unserialize(self, data):
829 """ Takes API objects and unserializes on existing MediaEntry """
830 if "displayName" in data:
831 self.title = data["displayName"]
832
833 if "content" in data:
834 self.description = data["content"]
835
836 if "license" in data:
837 self.license = data["license"]
838
839 if "location" in data:
840 License.create(data["location"], self)
841
842 if "tags" in data:
843 self.tags = convert_to_tag_list_of_dicts(', '.join(data["tags"]))
844
845 return True
846
847 class FileKeynames(Base):
848 """
849 keywords for various places.
850 currently the MediaFile keys
851 """
852 __tablename__ = "core__file_keynames"
853 id = Column(Integer, primary_key=True)
854 name = Column(Unicode, unique=True)
855
856 def __repr__(self):
857 return "<FileKeyname %r: %r>" % (self.id, self.name)
858
859 @classmethod
860 def find_or_new(cls, name):
861 t = cls.query.filter_by(name=name).first()
862 if t is not None:
863 return t
864 return cls(name=name)
865
866
867 class MediaFile(Base):
868 """
869 TODO: Highly consider moving "name" into a new table.
870 TODO: Consider preloading said table in software
871 """
872 __tablename__ = "core__mediafiles"
873
874 media_entry = Column(
875 Integer, ForeignKey(MediaEntry.id),
876 nullable=False)
877 name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
878 file_path = Column(PathTupleWithSlashes)
879 file_metadata = Column(MutationDict.as_mutable(JSONEncoded))
880
881 __table_args__ = (
882 PrimaryKeyConstraint('media_entry', 'name_id'),
883 {})
884
885 def __repr__(self):
886 return "<MediaFile %s: %r>" % (self.name, self.file_path)
887
888 name_helper = relationship(FileKeynames, lazy="joined", innerjoin=True)
889 name = association_proxy('name_helper', 'name',
890 creator=FileKeynames.find_or_new
891 )
892
893
894 class MediaAttachmentFile(Base):
895 __tablename__ = "core__attachment_files"
896
897 id = Column(Integer, primary_key=True)
898 media_entry = Column(
899 Integer, ForeignKey(MediaEntry.id),
900 nullable=False)
901 name = Column(Unicode, nullable=False)
902 filepath = Column(PathTupleWithSlashes)
903 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
904
905 @property
906 def dict_view(self):
907 """A dict like view on this object"""
908 return DictReadAttrProxy(self)
909
910
911 class Tag(Base):
912 __tablename__ = "core__tags"
913
914 id = Column(Integer, primary_key=True)
915 slug = Column(Unicode, nullable=False, unique=True)
916
917 def __repr__(self):
918 return "<Tag %r: %r>" % (self.id, self.slug)
919
920 @classmethod
921 def find_or_new(cls, slug):
922 t = cls.query.filter_by(slug=slug).first()
923 if t is not None:
924 return t
925 return cls(slug=slug)
926
927
928 class MediaTag(Base):
929 __tablename__ = "core__media_tags"
930
931 id = Column(Integer, primary_key=True)
932 media_entry = Column(
933 Integer, ForeignKey(MediaEntry.id),
934 nullable=False, index=True)
935 tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True)
936 name = Column(Unicode)
937 # created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
938
939 __table_args__ = (
940 UniqueConstraint('tag', 'media_entry'),
941 {})
942
943 tag_helper = relationship(Tag)
944 slug = association_proxy('tag_helper', 'slug',
945 creator=Tag.find_or_new
946 )
947
948 def __init__(self, name=None, slug=None):
949 Base.__init__(self)
950 if name is not None:
951 self.name = name
952 if slug is not None:
953 self.tag_helper = Tag.find_or_new(slug)
954
955 @property
956 def dict_view(self):
957 """A dict like view on this object"""
958 return DictReadAttrProxy(self)
959
960 class Comment(Base):
961 """
962 Link table between a response and another object that can have replies.
963
964 This acts as a link table between an object and the comments on it, it's
965 done like this so that you can look up all the comments without knowing
966 whhich comments are on an object before hand. Any object can be a comment
967 and more or less any object can accept comments too.
968
969 Important: This is NOT the old MediaComment table.
970 """
971 __tablename__ = "core__comment_links"
972
973 id = Column(Integer, primary_key=True)
974
975 # The GMR to the object the comment is on.
976 target_id = Column(
977 Integer,
978 ForeignKey(GenericModelReference.id),
979 nullable=False
980 )
981 target_helper = relationship(
982 GenericModelReference,
983 foreign_keys=[target_id]
984 )
985 target = association_proxy("target_helper", "get_object",
986 creator=GenericModelReference.find_or_new)
987
988 # The comment object
989 comment_id = Column(
990 Integer,
991 ForeignKey(GenericModelReference.id),
992 nullable=False
993 )
994 comment_helper = relationship(
995 GenericModelReference,
996 foreign_keys=[comment_id]
997 )
998 comment = association_proxy("comment_helper", "get_object",
999 creator=GenericModelReference.find_or_new)
1000
1001 # When it was added
1002 added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1003
1004 @property
1005 def get_author(self):
1006 # for compatibility
1007 return self.comment().get_actor # noqa
1008
1009 def __getattr__(self, attr):
1010 if attr.startswith('_'):
1011 # if attr starts with '_', then it's probably some internal
1012 # sqlalchemy variable. Since __getattr__ is called when
1013 # non-existing attributes are being accessed, we should not try to
1014 # fetch it from self.comment()
1015 raise AttributeError
1016 try:
1017 _log.debug('Old attr is being accessed: {0}'.format(attr))
1018 return getattr(self.comment(), attr) # noqa
1019 except Exception as e:
1020 _log.error(e)
1021 raise
1022
1023 class TextComment(Base, TextCommentMixin, CommentingMixin):
1024 """
1025 A basic text comment, this is a usually short amount of text and nothing else
1026 """
1027 # This is a legacy from when Comments where just on MediaEntry objects.
1028 __tablename__ = "core__media_comments"
1029
1030 id = Column(Integer, primary_key=True)
1031 public_id = Column(Unicode, unique=True)
1032 actor = Column(Integer, ForeignKey(User.id), nullable=False)
1033 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1034 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1035 content = Column(UnicodeText, nullable=False)
1036 location = Column(Integer, ForeignKey("core__locations.id"))
1037 get_location = relationship("Location", lazy="joined")
1038
1039 # Cascade: Comments are owned by their creator. So do the full thing.
1040 # lazy=dynamic: People might post a *lot* of comments,
1041 # so make the "posted_comments" a query-like thing.
1042 get_actor = relationship(User,
1043 backref=backref("posted_comments",
1044 lazy="dynamic",
1045 cascade="all, delete-orphan"))
1046 deletion_mode = Base.SOFT_DELETE
1047
1048 def serialize(self, request):
1049 """ Unserialize to python dictionary for API """
1050 target = self.get_reply_to()
1051 # If this is target just.. give them nothing?
1052 if target is None:
1053 target = {}
1054 else:
1055 target = target.serialize(request, show_comments=False)
1056
1057
1058 author = self.get_actor
1059 published = UTC.localize(self.created)
1060 context = {
1061 "id": self.get_public_id(request.urlgen),
1062 "objectType": self.object_type,
1063 "content": self.content,
1064 "inReplyTo": target,
1065 "author": author.serialize(request),
1066 "published": published.isoformat(),
1067 "updated": published.isoformat(),
1068 }
1069
1070 if self.location:
1071 context["location"] = self.get_location.seralize(request)
1072
1073 return context
1074
1075 def unserialize(self, data, request):
1076 """ Takes API objects and unserializes on existing comment """
1077 if "content" in data:
1078 self.content = data["content"]
1079
1080 if "location" in data:
1081 Location.create(data["location"], self)
1082
1083
1084 # Handle changing the reply ID
1085 if "inReplyTo" in data:
1086 # Validate that the ID is correct
1087 try:
1088 id = extract_url_arguments(
1089 url=data["inReplyTo"]["id"],
1090 urlmap=request.app.url_map
1091 )["id"]
1092 except ValueError:
1093 raise False
1094
1095 public_id = request.urlgen(
1096 "mediagoblin.api.object",
1097 id=id,
1098 object_type=data["inReplyTo"]["objectType"],
1099 qualified=True
1100 )
1101
1102 media = MediaEntry.query.filter_by(public_id=public_id).first()
1103 if media is None:
1104 return False
1105
1106 # We need an ID for this model.
1107 self.save(commit=False)
1108
1109 # Create the link
1110 link = Comment()
1111 link.target = media
1112 link.comment = self
1113 link.save()
1114
1115 return True
1116
1117 class Collection(Base, CollectionMixin, CommentingMixin):
1118 """A representation of a collection of objects.
1119
1120 This holds a group/collection of objects that could be a user defined album
1121 or their inbox, outbox, followers, etc. These are always ordered and accessable
1122 via the API and web.
1123
1124 The collection has a number of types which determine what kind of collection
1125 it is, for example the users inbox will be of `Collection.INBOX_TYPE` that will
1126 be stored on the `Collection.type` field. It's important to set the correct type.
1127
1128 On deletion, contained CollectionItems get automatically reaped via
1129 SQL cascade"""
1130 __tablename__ = "core__collections"
1131
1132 id = Column(Integer, primary_key=True)
1133 public_id = Column(Unicode, unique=True)
1134 title = Column(Unicode, nullable=False)
1135 slug = Column(Unicode)
1136 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow,
1137 index=True)
1138 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1139 description = Column(UnicodeText)
1140 actor = Column(Integer, ForeignKey(User.id), nullable=False)
1141 num_items = Column(Integer, default=0)
1142
1143 # There are lots of different special types of collections in the pump.io API
1144 # for example: followers, following, inbox, outbox, etc. See type constants
1145 # below the fields on this model.
1146 type = Column(Unicode, nullable=False)
1147
1148 # Location
1149 location = Column(Integer, ForeignKey("core__locations.id"))
1150 get_location = relationship("Location", lazy="joined")
1151
1152 # Cascade: Collections are owned by their creator. So do the full thing.
1153 get_actor = relationship(User,
1154 backref=backref("collections",
1155 cascade="all, delete-orphan"))
1156 __table_args__ = (
1157 UniqueConstraint("actor", "slug"),
1158 {})
1159
1160 deletion_mode = Base.SOFT_DELETE
1161
1162 # These are the types, It's strongly suggested if new ones are invented they
1163 # are prefixed to ensure they're unique from other types. Any types used in
1164 # the main mediagoblin should be prefixed "core-"
1165 INBOX_TYPE = "core-inbox"
1166 OUTBOX_TYPE = "core-outbox"
1167 FOLLOWER_TYPE = "core-followers"
1168 FOLLOWING_TYPE = "core-following"
1169 COMMENT_TYPE = "core-comments"
1170 USER_DEFINED_TYPE = "core-user-defined"
1171
1172 def get_collection_items(self, ascending=False):
1173 #TODO, is this still needed with self.collection_items being available?
1174 order_col = CollectionItem.position
1175 if not ascending:
1176 order_col = desc(order_col)
1177 return CollectionItem.query.filter_by(
1178 collection=self.id).order_by(order_col)
1179
1180 def __repr__(self):
1181 safe_title = self.title.encode('ascii', 'replace')
1182 return '<{classname} #{id}: {title} by {actor}>'.format(
1183 id=self.id,
1184 classname=self.__class__.__name__,
1185 actor=self.actor,
1186 title=safe_title)
1187
1188 def serialize(self, request):
1189 # Get all serialized output in a list
1190 items = [i.serialize(request) for i in self.get_collection_items()]
1191 return {
1192 "totalItems": self.num_items,
1193 "url": self.url_for_self(request.urlgen, qualified=True),
1194 "items": items,
1195 }
1196
1197
1198 class CollectionItem(Base, CollectionItemMixin):
1199 __tablename__ = "core__collection_items"
1200
1201 id = Column(Integer, primary_key=True)
1202
1203 collection = Column(Integer, ForeignKey(Collection.id), nullable=False)
1204 note = Column(UnicodeText, nullable=True)
1205 added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1206 position = Column(Integer)
1207 # Cascade: CollectionItems are owned by their Collection. So do the full thing.
1208 in_collection = relationship(Collection,
1209 backref=backref(
1210 "collection_items",
1211 cascade="all, delete-orphan"))
1212
1213 # Link to the object (could be anything.
1214 object_id = Column(
1215 Integer,
1216 ForeignKey(GenericModelReference.id),
1217 nullable=False,
1218 index=True
1219 )
1220 object_helper = relationship(
1221 GenericModelReference,
1222 foreign_keys=[object_id]
1223 )
1224 get_object = association_proxy(
1225 "object_helper",
1226 "get_object",
1227 creator=GenericModelReference.find_or_new
1228 )
1229
1230 __table_args__ = (
1231 UniqueConstraint('collection', 'object_id'),
1232 {})
1233
1234 @property
1235 def dict_view(self):
1236 """A dict like view on this object"""
1237 return DictReadAttrProxy(self)
1238
1239 def __repr__(self):
1240 return '<{classname} #{id}: Object {obj} in {collection}>'.format(
1241 id=self.id,
1242 classname=self.__class__.__name__,
1243 collection=self.collection,
1244 obj=self.get_object()
1245 )
1246
1247 def serialize(self, request):
1248 return self.get_object().serialize(request)
1249
1250
1251 class ProcessingMetaData(Base):
1252 __tablename__ = 'core__processing_metadata'
1253
1254 id = Column(Integer, primary_key=True)
1255 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False,
1256 index=True)
1257 media_entry = relationship(MediaEntry,
1258 backref=backref('processing_metadata',
1259 cascade='all, delete-orphan'))
1260 callback_url = Column(Unicode)
1261
1262 @property
1263 def dict_view(self):
1264 """A dict like view on this object"""
1265 return DictReadAttrProxy(self)
1266
1267
1268 class CommentSubscription(Base):
1269 __tablename__ = 'core__comment_subscriptions'
1270 id = Column(Integer, primary_key=True)
1271
1272 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1273
1274 media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
1275 media_entry = relationship(MediaEntry,
1276 backref=backref('comment_subscriptions',
1277 cascade='all, delete-orphan'))
1278
1279 user_id = Column(Integer, ForeignKey(User.id), nullable=False)
1280 user = relationship(User,
1281 backref=backref('comment_subscriptions',
1282 cascade='all, delete-orphan'))
1283
1284 notify = Column(Boolean, nullable=False, default=True)
1285 send_email = Column(Boolean, nullable=False, default=True)
1286
1287 def __repr__(self):
1288 return ('<{classname} #{id}: {user} {media} notify: '
1289 '{notify} email: {email}>').format(
1290 id=self.id,
1291 classname=self.__class__.__name__,
1292 user=self.user,
1293 media=self.media_entry,
1294 notify=self.notify,
1295 email=self.send_email)
1296
1297
1298 class Notification(Base):
1299 __tablename__ = 'core__notifications'
1300 id = Column(Integer, primary_key=True)
1301
1302 object_id = Column(Integer, ForeignKey(GenericModelReference.id))
1303 object_helper = relationship(GenericModelReference)
1304 obj = association_proxy("object_helper", "get_object",
1305 creator=GenericModelReference.find_or_new)
1306
1307 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1308 user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False,
1309 index=True)
1310 seen = Column(Boolean, default=lambda: False, index=True)
1311 user = relationship(
1312 User,
1313 backref=backref('notifications', cascade='all, delete-orphan'))
1314
1315 def __repr__(self):
1316 return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1317 id=self.id,
1318 klass=self.__class__.__name__,
1319 user=self.user,
1320 subject=getattr(self, 'subject', None),
1321 seen='unseen' if not self.seen else 'seen')
1322
1323 def __unicode__(self):
1324 return u'<{klass} #{id}: {user}: {subject} ({seen})>'.format(
1325 id=self.id,
1326 klass=self.__class__.__name__,
1327 user=self.user,
1328 subject=getattr(self, 'subject', None),
1329 seen='unseen' if not self.seen else 'seen')
1330
1331 class Report(Base):
1332 """
1333 Represents a report that someone might file against Media, Comments, etc.
1334
1335 :keyword reporter_id Holds the id of the user who created
1336 the report, as an Integer column.
1337 :keyword report_content Hold the explanation left by the repor-
1338 -ter to indicate why they filed the
1339 report in the first place, as a
1340 Unicode column.
1341 :keyword reported_user_id Holds the id of the user who created
1342 the content which was reported, as
1343 an Integer column.
1344 :keyword created Holds a datetime column of when the re-
1345 -port was filed.
1346 :keyword resolver_id Holds the id of the moderator/admin who
1347 resolved the report.
1348 :keyword resolved Holds the DateTime object which descri-
1349 -bes when this report was resolved
1350 :keyword result Holds the UnicodeText column of the
1351 resolver's reasons for resolving
1352 the report this way. Some of this
1353 is auto-generated
1354 :keyword object_id Holds the ID of the GenericModelReference
1355 which points to the reported object.
1356 """
1357 __tablename__ = 'core__reports'
1358
1359 id = Column(Integer, primary_key=True)
1360 reporter_id = Column(Integer, ForeignKey(User.id), nullable=False)
1361 reporter = relationship(
1362 User,
1363 backref=backref("reports_filed_by",
1364 lazy="dynamic",
1365 cascade="all, delete-orphan"),
1366 primaryjoin="User.id==Report.reporter_id")
1367 report_content = Column(UnicodeText)
1368 reported_user_id = Column(Integer, ForeignKey(User.id), nullable=False)
1369 reported_user = relationship(
1370 User,
1371 backref=backref("reports_filed_on",
1372 lazy="dynamic",
1373 cascade="all, delete-orphan"),
1374 primaryjoin="User.id==Report.reported_user_id")
1375 created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1376 resolver_id = Column(Integer, ForeignKey(User.id))
1377 resolver = relationship(
1378 User,
1379 backref=backref("reports_resolved_by",
1380 lazy="dynamic",
1381 cascade="all, delete-orphan"),
1382 primaryjoin="User.id==Report.resolver_id")
1383
1384 resolved = Column(DateTime)
1385 result = Column(UnicodeText)
1386
1387 object_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=True)
1388 object_helper = relationship(GenericModelReference)
1389 obj = association_proxy("object_helper", "get_object",
1390 creator=GenericModelReference.find_or_new)
1391
1392 def is_archived_report(self):
1393 return self.resolved is not None
1394
1395 def is_comment_report(self):
1396 if self.object_id is None:
1397 return False
1398 return isinstance(self.obj(), TextComment)
1399
1400 def is_media_entry_report(self):
1401 if self.object_id is None:
1402 return False
1403 return isinstance(self.obj(), MediaEntry)
1404
1405 def archive(self,resolver_id, resolved, result):
1406 self.resolver_id = resolver_id
1407 self.resolved = resolved
1408 self.result = result
1409
1410 class UserBan(Base):
1411 """
1412 Holds the information on a specific user's ban-state. As long as one of
1413 these is attached to a user, they are banned from accessing mediagoblin.
1414 When they try to log in, they are greeted with a page that tells them
1415 the reason why they are banned and when (if ever) the ban will be
1416 lifted
1417
1418 :keyword user_id Holds the id of the user this object is
1419 attached to. This is a one-to-one
1420 relationship.
1421 :keyword expiration_date Holds the date that the ban will be lifted.
1422 If this is null, the ban is permanent
1423 unless a moderator manually lifts it.
1424 :keyword reason Holds the reason why the user was banned.
1425 """
1426 __tablename__ = 'core__user_bans'
1427
1428 user_id = Column(Integer, ForeignKey(User.id), nullable=False,
1429 primary_key=True)
1430 expiration_date = Column(Date)
1431 reason = Column(UnicodeText, nullable=False)
1432
1433
1434 class Privilege(Base):
1435 """
1436 The Privilege table holds all of the different privileges a user can hold.
1437 If a user 'has' a privilege, the User object is in a relationship with the
1438 privilege object.
1439
1440 :keyword privilege_name Holds a unicode object that is the recognizable
1441 name of this privilege. This is the column
1442 used for identifying whether or not a user
1443 has a necessary privilege or not.
1444
1445 """
1446 __tablename__ = 'core__privileges'
1447
1448 id = Column(Integer, nullable=False, primary_key=True)
1449 privilege_name = Column(Unicode, nullable=False, unique=True)
1450 all_users = relationship(
1451 User,
1452 backref='all_privileges',
1453 secondary="core__privileges_users")
1454
1455 def __init__(self, privilege_name):
1456 '''
1457 Currently consructors are required for tables that are initialized thru
1458 the FOUNDATIONS system. This is because they need to be able to be con-
1459 -structed by a list object holding their arg*s
1460 '''
1461 self.privilege_name = privilege_name
1462
1463 def __repr__(self):
1464 return "<Privilege %s>" % (self.privilege_name)
1465
1466
1467 class PrivilegeUserAssociation(Base):
1468 '''
1469 This table holds the many-to-many relationship between User and Privilege
1470 '''
1471
1472 __tablename__ = 'core__privileges_users'
1473
1474 user = Column(
1475 "user",
1476 Integer,
1477 ForeignKey(User.id),
1478 primary_key=True)
1479 privilege = Column(
1480 "privilege",
1481 Integer,
1482 ForeignKey(Privilege.id),
1483 primary_key=True)
1484
1485 class Generator(Base):
1486 """ Information about what created an activity """
1487 __tablename__ = "core__generators"
1488
1489 id = Column(Integer, primary_key=True)
1490 name = Column(Unicode, nullable=False)
1491 published = Column(DateTime, default=datetime.datetime.utcnow)
1492 updated = Column(DateTime, default=datetime.datetime.utcnow)
1493 object_type = Column(Unicode, nullable=False)
1494
1495 deletion_mode = Base.SOFT_DELETE
1496
1497 def __repr__(self):
1498 return "<{klass} {name}>".format(
1499 klass=self.__class__.__name__,
1500 name=self.name
1501 )
1502
1503 def serialize(self, request):
1504 href = request.urlgen(
1505 "mediagoblin.api.object",
1506 object_type=self.object_type,
1507 id=self.id,
1508 qualified=True
1509 )
1510 published = UTC.localize(self.published)
1511 updated = UTC.localize(self.updated)
1512 return {
1513 "id": href,
1514 "displayName": self.name,
1515 "published": published.isoformat(),
1516 "updated": updated.isoformat(),
1517 "objectType": self.object_type,
1518 }
1519
1520 def unserialize(self, data):
1521 if "displayName" in data:
1522 self.name = data["displayName"]
1523
1524 class Activity(Base, ActivityMixin):
1525 """
1526 This holds all the metadata about an activity such as uploading an image,
1527 posting a comment, etc.
1528 """
1529 __tablename__ = "core__activities"
1530
1531 id = Column(Integer, primary_key=True)
1532 public_id = Column(Unicode, unique=True)
1533 actor = Column(Integer,
1534 ForeignKey("core__users.id"),
1535 nullable=False)
1536 published = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1537 updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1538
1539 verb = Column(Unicode, nullable=False)
1540 content = Column(Unicode, nullable=True)
1541 title = Column(Unicode, nullable=True)
1542 generator = Column(Integer,
1543 ForeignKey("core__generators.id"),
1544 nullable=True)
1545
1546 # Create the generic foreign keys for the object
1547 object_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=False)
1548 object_helper = relationship(GenericModelReference, foreign_keys=[object_id])
1549 object = association_proxy("object_helper", "get_object",
1550 creator=GenericModelReference.find_or_new)
1551
1552 # Create the generic foreign Key for the target
1553 target_id = Column(Integer, ForeignKey(GenericModelReference.id), nullable=True)
1554 target_helper = relationship(GenericModelReference, foreign_keys=[target_id])
1555 target = association_proxy("target_helper", "get_object",
1556 creator=GenericModelReference.find_or_new)
1557
1558 get_actor = relationship(User,
1559 backref=backref("activities",
1560 cascade="all, delete-orphan"))
1561 get_generator = relationship(Generator)
1562
1563 deletion_mode = Base.SOFT_DELETE
1564
1565 def __repr__(self):
1566 if self.content is None:
1567 return "<{klass} verb:{verb}>".format(
1568 klass=self.__class__.__name__,
1569 verb=self.verb
1570 )
1571 else:
1572 return "<{klass} {content}>".format(
1573 klass=self.__class__.__name__,
1574 content=self.content
1575 )
1576
1577 def save(self, set_updated=True, *args, **kwargs):
1578 if set_updated:
1579 self.updated = datetime.datetime.now()
1580 super(Activity, self).save(*args, **kwargs)
1581
1582 class Graveyard(Base):
1583 """ Where models come to die """
1584 __tablename__ = "core__graveyard"
1585
1586 id = Column(Integer, primary_key=True)
1587 public_id = Column(Unicode, nullable=True, unique=True)
1588
1589 deleted = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
1590 object_type = Column(Unicode, nullable=False)
1591
1592 # This could either be a deleted actor or a real actor, this must be
1593 # nullable as it we shouldn't have it set for deleted actor
1594 actor_id = Column(Integer, ForeignKey(GenericModelReference.id))
1595 actor_helper = relationship(GenericModelReference)
1596 actor = association_proxy("actor_helper", "get_object",
1597 creator=GenericModelReference.find_or_new)
1598
1599 def __repr__(self):
1600 return "<{klass} deleted {obj_type}>".format(
1601 klass=type(self).__name__,
1602 obj_type=self.object_type
1603 )
1604
1605 def serialize(self, request):
1606 deleted = UTC.localize(self.deleted).isoformat()
1607 context = {
1608 "id": self.public_id,
1609 "objectType": self.object_type,
1610 "published": deleted,
1611 "updated": deleted,
1612 "deleted": deleted,
1613 }
1614
1615 if self.actor_id is not None:
1616 context["actor"] = self.actor().serialize(request)
1617
1618 return context
1619 MODELS = [
1620 LocalUser, RemoteUser, User, MediaEntry, Tag, MediaTag, Comment, TextComment,
1621 Collection, CollectionItem, MediaFile, FileKeynames, MediaAttachmentFile,
1622 ProcessingMetaData, Notification, Client, CommentSubscription, Report,
1623 UserBan, Privilege, PrivilegeUserAssociation, RequestToken, AccessToken,
1624 NonceTimestamp, Activity, Generator, Location, GenericModelReference, Graveyard]
1625
1626 """
1627 Foundations are the default rows that are created immediately after the tables
1628 are initialized. Each entry to this dictionary should be in the format of:
1629 ModelConstructorObject:List of Dictionaries
1630 (Each Dictionary represents a row on the Table to be created, containing each
1631 of the columns' names as a key string, and each of the columns' values as a
1632 value)
1633
1634 ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
1635 user_foundations = [{'name':u'Joanna', 'age':24},
1636 {'name':u'Andrea', 'age':41}]
1637
1638 FOUNDATIONS = {User:user_foundations}
1639 """
1640 privilege_foundations = [{'privilege_name':u'admin'},
1641 {'privilege_name':u'moderator'},
1642 {'privilege_name':u'uploader'},
1643 {'privilege_name':u'reporter'},
1644 {'privilege_name':u'commenter'},
1645 {'privilege_name':u'active'}]
1646 FOUNDATIONS = {Privilege:privilege_foundations}
1647
1648 ######################################################
1649 # Special, migrations-tracking table
1650 #
1651 # Not listed in MODELS because this is special and not
1652 # really migrated, but used for migrations (for now)
1653 ######################################################
1654
1655 class MigrationData(Base):
1656 __tablename__ = "core__migrations"
1657
1658 name = Column(Unicode, primary_key=True)
1659 version = Column(Integer, nullable=False, default=0)
1660
1661 ######################################################
1662
1663
1664 def show_table_init(engine_uri):
1665 if engine_uri is None:
1666 engine_uri = 'sqlite:///:memory:'
1667 from sqlalchemy import create_engine
1668 engine = create_engine(engine_uri, echo=True)
1669
1670 Base.metadata.create_all(engine)
1671
1672
1673 if __name__ == '__main__':
1674 from sys import argv
1675 print(repr(argv))
1676 if len(argv) == 2:
1677 uri = argv[1]
1678 else:
1679 uri = None
1680 show_table_init(uri)