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