diff options
Diffstat (limited to 'mediagoblin/db/models.py')
-rw-r--r-- | mediagoblin/db/models.py | 500 |
1 files changed, 421 insertions, 79 deletions
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 2ff30d22..e8fb17a7 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -18,13 +18,15 @@ TODO: indexes on foreignkeys, where useful. """ +from __future__ import print_function + import logging import datetime from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \ Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \ SmallInteger, Date -from sqlalchemy.orm import relationship, backref, with_polymorphic +from sqlalchemy.orm import relationship, backref, with_polymorphic, validates from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.sql.expression import desc from sqlalchemy.ext.associationproxy import association_proxy @@ -34,20 +36,90 @@ from mediagoblin.db.extratypes import (PathTupleWithSlashes, JSONEncoded, MutationDict) from mediagoblin.db.base import Base, DictReadAttrProxy from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \ - MediaCommentMixin, CollectionMixin, CollectionItemMixin + MediaCommentMixin, CollectionMixin, CollectionItemMixin, \ + ActivityMixin from mediagoblin.tools.files import delete_media_files from mediagoblin.tools.common import import_component +from mediagoblin.tools.routing import extract_url_arguments -# It's actually kind of annoying how sqlalchemy-migrate does this, if -# I understand it right, but whatever. Anyway, don't remove this :P -# -# We could do migration calls more manually instead of relying on -# this import-based meddling... -from migrate import changeset +import six +from pytz import UTC _log = logging.getLogger(__name__) +class Location(Base): + """ Represents a physical location """ + __tablename__ = "core__locations" + + id = Column(Integer, primary_key=True) + name = Column(Unicode) + + # GPS coordinates + position = Column(MutationDict.as_mutable(JSONEncoded)) + address = Column(MutationDict.as_mutable(JSONEncoded)) + + @classmethod + def create(cls, data, obj): + location = cls() + location.unserialize(data) + location.save() + obj.location = location.id + return location + + def serialize(self, request): + location = {"objectType": "place"} + + if self.name is not None: + location["displayName"] = self.name + + if self.position: + location["position"] = self.position + if self.address: + location["address"] = self.address + + return location + + def unserialize(self, data): + if "displayName" in data: + self.name = data["displayName"] + + self.position = {} + self.address = {} + + # nicer way to do this? + if "position" in data: + # TODO: deal with ISO 9709 formatted string as position + if "altitude" in data["position"]: + self.position["altitude"] = data["position"]["altitude"] + + if "direction" in data["position"]: + self.position["direction"] = data["position"]["direction"] + + if "longitude" in data["position"]: + self.position["longitude"] = data["position"]["longitude"] + + if "latitude" in data["position"]: + self.position["latitude"] = data["position"]["latitude"] + + if "address" in data: + if "formatted" in data["address"]: + self.address["formatted"] = data["address"]["formatted"] + + if "streetAddress" in data["address"]: + self.address["streetAddress"] = data["address"]["streetAddress"] + + if "locality" in data["address"]: + self.address["locality"] = data["address"]["locality"] + + if "region" in data["address"]: + self.address["region"] = data["address"]["region"] + + if "postalCode" in data["address"]: + self.address["postalCode"] = data["addresss"]["postalCode"] + + if "country" in data["address"]: + self.address["country"] = data["address"]["country"] class User(Base, UserMixin): """ @@ -64,7 +136,7 @@ class User(Base, UserMixin): # point. email = Column(Unicode, nullable=False) pw_hash = Column(Unicode) - created = Column(DateTime, nullable=False, default=datetime.datetime.now) + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) # Intented to be nullable=False, but migrations would not work for it # set to nullable=True implicitly. wants_comment_notification = Column(Boolean, default=True) @@ -74,6 +146,10 @@ class User(Base, UserMixin): bio = Column(UnicodeText) # ?? uploaded = Column(Integer, default=0) upload_limit = Column(Integer) + location = Column(Integer, ForeignKey("core__locations.id")) + get_location = relationship("Location", lazy="joined") + + activity = Column(Integer, ForeignKey("core__activity_intermediators.id")) ## TODO # plugin data would be in a separate model @@ -138,11 +214,13 @@ class User(Base, UserMixin): def serialize(self, request): + published = UTC.localize(self.created) user = { "id": "acct:{0}@{1}".format(self.username, request.host), + "published": published.isoformat(), "preferredUsername": self.username, "displayName": "{0}@{1}".format(self.username, request.host), - "objectType": "person", + "objectType": self.object_type, "pump_io": { "shared": False, "followed": False, @@ -150,21 +228,21 @@ class User(Base, UserMixin): "links": { "self": { "href": request.urlgen( - "mediagoblin.federation.user.profile", + "mediagoblin.api.user.profile", username=self.username, qualified=True ), }, "activity-inbox": { "href": request.urlgen( - "mediagoblin.federation.inbox", + "mediagoblin.api.inbox", username=self.username, qualified=True ) }, "activity-outbox": { "href": request.urlgen( - "mediagoblin.federation.feed", + "mediagoblin.api.feed", username=self.username, qualified=True ) @@ -176,9 +254,18 @@ class User(Base, UserMixin): user.update({"summary": self.bio}) if self.url: user.update({"url": self.url}) + if self.location: + user.update({"location": self.get_location.serialize(request)}) return user + def unserialize(self, data): + if "summary" in data: + self.bio = data["summary"] + + if "location" in data: + Location.create(data, self) + class Client(Base): """ Model representing a client - Used for API Auth @@ -189,8 +276,8 @@ class Client(Base): secret = Column(Unicode, nullable=False) expirey = Column(DateTime, nullable=True) application_type = Column(Unicode, nullable=False) - created = Column(DateTime, nullable=False, default=datetime.datetime.now) - updated = Column(DateTime, nullable=False, default=datetime.datetime.now) + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) # optional stuff redirect_uri = Column(JSONEncoded, nullable=True) @@ -218,8 +305,10 @@ class RequestToken(Base): authenticated = Column(Boolean, default=False) verifier = Column(Unicode, nullable=True) callback = Column(Unicode, nullable=False, default=u"oob") - created = Column(DateTime, nullable=False, default=datetime.datetime.now) - updated = Column(DateTime, nullable=False, default=datetime.datetime.now) + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + + get_client = relationship(Client) class AccessToken(Base): """ @@ -231,8 +320,10 @@ class AccessToken(Base): secret = Column(Unicode, nullable=False) user = Column(Integer, ForeignKey(User.id)) request_token = Column(Unicode, ForeignKey(RequestToken.token)) - created = Column(DateTime, nullable=False, default=datetime.datetime.now) - updated = Column(DateTime, nullable=False, default=datetime.datetime.now) + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + + get_requesttoken = relationship(RequestToken) class NonceTimestamp(Base): @@ -254,7 +345,7 @@ class MediaEntry(Base, MediaEntryMixin): uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True) title = Column(Unicode, nullable=False) slug = Column(Unicode) - created = Column(DateTime, nullable=False, default=datetime.datetime.now, + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow, index=True) description = Column(UnicodeText) # ?? media_type = Column(Unicode, nullable=False) @@ -262,6 +353,8 @@ class MediaEntry(Base, MediaEntryMixin): # or use sqlalchemy.types.Enum? license = Column(Unicode) file_size = Column(Integer, default=0) + location = Column(Integer, ForeignKey("core__locations.id")) + get_location = relationship("Location", lazy="joined") fail_error = Column(Unicode) fail_metadata = Column(JSONEncoded) @@ -309,6 +402,8 @@ class MediaEntry(Base, MediaEntryMixin): media_metadata = Column(MutationDict.as_mutable(JSONEncoded), default=MutationDict()) + activity = Column(Integer, ForeignKey("core__activity_intermediators.id")) + ## TODO # fail_error @@ -344,7 +439,7 @@ class MediaEntry(Base, MediaEntryMixin): return the value of the key. """ media_file = MediaFile.query.filter_by(media_entry=self.id, - name=unicode(file_key)).first() + name=six.text_type(file_key)).first() if media_file: if metadata_key: @@ -357,11 +452,11 @@ class MediaEntry(Base, MediaEntryMixin): Update the file_metadata of a MediaFile. """ media_file = MediaFile.query.filter_by(media_entry=self.id, - name=unicode(file_key)).first() + name=six.text_type(file_key)).first() file_metadata = media_file.file_metadata or {} - for key, value in kwargs.iteritems(): + for key, value in six.iteritems(kwargs): file_metadata[key] = value media_file.file_metadata = file_metadata @@ -386,7 +481,7 @@ class MediaEntry(Base, MediaEntryMixin): media_data.get_media_entry = self else: # Update old media data - for field, value in kwargs.iteritems(): + for field, value in six.iteritems(kwargs): setattr(media_data, field, value) @memoized_property @@ -394,7 +489,11 @@ class MediaEntry(Base, MediaEntryMixin): return import_component(self.media_type + '.models:BACKREF_NAME') def __repr__(self): - safe_title = self.title.encode('ascii', 'replace') + if six.PY2: + # obj.__repr__() should return a str on Python 2 + safe_title = self.title.encode('utf-8', 'replace') + else: + safe_title = self.title return '<{classname} {id}: {title}>'.format( classname=self.__class__.__name__, @@ -415,7 +514,7 @@ class MediaEntry(Base, MediaEntryMixin): # Delete all related files/attachments try: delete_media_files(self) - except OSError, error: + except OSError as error: # Returns list of files we failed to delete _log.error('No such files from the user "{1}" to delete: ' '{0}'.format(str(error), self.get_uploader)) @@ -430,38 +529,36 @@ class MediaEntry(Base, MediaEntryMixin): # pass through commit=False/True in kwargs super(MediaEntry, self).delete(**kwargs) - @property - def objectType(self): - """ Converts media_type to pump-like type - don't use internally """ - return self.media_type.split(".")[-1] - def serialize(self, request, show_comments=True): """ Unserialize MediaEntry to object """ + href = request.urlgen( + "mediagoblin.api.object", + object_type=self.object_type, + id=self.id, + qualified=True + ) author = self.get_uploader + published = UTC.localize(self.created) + updated = UTC.localize(self.created) context = { - "id": self.id, + "id": href, "author": author.serialize(request), - "objectType": self.objectType, - "url": self.url_for_self(request.urlgen), + "objectType": self.object_type, + "url": self.url_for_self(request.urlgen, qualified=True), "image": { "url": request.host_url + self.thumb_url[1:], }, "fullImage":{ "url": request.host_url + self.original_url[1:], }, - "published": self.created.isoformat(), - "updated": self.created.isoformat(), + "published": published.isoformat(), + "updated": updated.isoformat(), "pump_io": { "shared": False, }, "links": { "self": { - "href": request.urlgen( - "mediagoblin.federation.object", - objectType=self.objectType, - id=self.id, - qualified=True - ), + "href": href, }, } @@ -476,20 +573,40 @@ class MediaEntry(Base, MediaEntryMixin): if self.license: context["license"] = self.license + if self.location: + context["location"] = self.get_location.serialize(request) + if show_comments: - comments = [comment.serialize(request) for comment in self.get_comments()] + comments = [ + comment.serialize(request) for comment in self.get_comments()] total = len(comments) context["replies"] = { "totalItems": total, "items": comments, "url": request.urlgen( - "mediagoblin.federation.object.comments", - objectType=self.objectType, + "mediagoblin.api.object.comments", + object_type=self.object_type, id=self.id, qualified=True ), } + # Add image height and width if possible. We didn't use to store this + # data and we're not able (and maybe not willing) to re-process all + # images so it's possible this might not exist. + if self.get_file_metadata("thumb", "height"): + height = self.get_file_metadata("thumb", "height") + context["image"]["height"] = height + if self.get_file_metadata("thumb", "width"): + width = self.get_file_metadata("thumb", "width") + context["image"]["width"] = width + if self.get_file_metadata("original", "height"): + height = self.get_file_metadata("original", "height") + context["fullImage"]["height"] = height + if self.get_file_metadata("original", "height"): + width = self.get_file_metadata("original", "width") + context["fullImage"]["width"] = width + return context def unserialize(self, data): @@ -503,6 +620,9 @@ class MediaEntry(Base, MediaEntryMixin): if "license" in data: self.license = data["license"] + if "location" in data: + Licence.create(data["location"], self) + return True class FileKeynames(Base): @@ -561,7 +681,7 @@ class MediaAttachmentFile(Base): nullable=False) name = Column(Unicode, nullable=False) filepath = Column(PathTupleWithSlashes) - created = Column(DateTime, nullable=False, default=datetime.datetime.now) + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) @property def dict_view(self): @@ -595,7 +715,7 @@ class MediaTag(Base): nullable=False, index=True) tag = Column(Integer, ForeignKey(Tag.id), nullable=False, index=True) name = Column(Unicode) - # created = Column(DateTime, nullable=False, default=datetime.datetime.now) + # created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) __table_args__ = ( UniqueConstraint('tag', 'media_entry'), @@ -626,8 +746,10 @@ class MediaComment(Base, MediaCommentMixin): media_entry = Column( Integer, ForeignKey(MediaEntry.id), nullable=False, index=True) author = Column(Integer, ForeignKey(User.id), nullable=False) - created = Column(DateTime, nullable=False, default=datetime.datetime.now) + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) content = Column(UnicodeText, nullable=False) + location = Column(Integer, ForeignKey("core__locations.id")) + get_location = relationship("Location", lazy="joined") # Cascade: Comments are owned by their creator. So do the full thing. # lazy=dynamic: People might post a *lot* of comments, @@ -650,44 +772,60 @@ class MediaComment(Base, MediaCommentMixin): lazy="dynamic", cascade="all, delete-orphan")) + + activity = Column(Integer, ForeignKey("core__activity_intermediators.id")) + def serialize(self, request): """ Unserialize to python dictionary for API """ + href = request.urlgen( + "mediagoblin.api.object", + object_type=self.object_type, + id=self.id, + qualified=True + ) media = MediaEntry.query.filter_by(id=self.media_entry).first() author = self.get_author + published = UTC.localize(self.created) context = { - "id": self.id, - "objectType": "comment", + "id": href, + "objectType": self.object_type, "content": self.content, "inReplyTo": media.serialize(request, show_comments=False), - "author": author.serialize(request) + "author": author.serialize(request), + "published": published.isoformat(), + "updated": published.isoformat(), } + if self.location: + context["location"] = self.get_location.seralize(request) + return context - def unserialize(self, data): + def unserialize(self, data, request): """ Takes API objects and unserializes on existing comment """ - # Do initial checks to verify the object is correct - required_attributes = ["content", "inReplyTo"] - for attr in required_attributes: - if attr not in data: + # Handle changing the reply ID + if "inReplyTo" in data: + # Validate that the ID is correct + try: + media_id = int(extract_url_arguments( + url=data["inReplyTo"]["id"], + urlmap=request.app.url_map + )["id"]) + except ValueError: return False - # Validate inReplyTo has ID - if "id" not in data["inReplyTo"]: - return False + media = MediaEntry.query.filter_by(id=media_id).first() + if media is None: + return False - # Validate that the ID is correct - try: - media_id = int(data["inReplyTo"]["id"]) - except ValueError: - return False + self.media_entry = media.id - media = MediaEntry.query.filter_by(id=media_id).first() - if media is None: - return False + if "content" in data: + self.content = data["content"] + + if "location" in data: + Location.create(data["location"], self) - self.media_entry = media.id - self.content = data["content"] return True @@ -702,10 +840,13 @@ class Collection(Base, CollectionMixin): id = Column(Integer, primary_key=True) title = Column(Unicode, nullable=False) slug = Column(Unicode) - created = Column(DateTime, nullable=False, default=datetime.datetime.now, + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow, index=True) description = Column(UnicodeText) creator = Column(Integer, ForeignKey(User.id), nullable=False) + location = Column(Integer, ForeignKey("core__locations.id")) + get_location = relationship("Location", lazy="joined") + # TODO: No of items in Collection. Badly named, can we migrate to num_items? items = Column(Integer, default=0) @@ -714,6 +855,8 @@ class Collection(Base, CollectionMixin): backref=backref("collections", cascade="all, delete-orphan")) + activity = Column(Integer, ForeignKey("core__activity_intermediators.id")) + __table_args__ = ( UniqueConstraint('creator', 'slug'), {}) @@ -734,6 +877,18 @@ class Collection(Base, CollectionMixin): creator=self.creator, title=safe_title) + def serialize(self, request): + # Get all serialized output in a list + items = [] + for item in self.get_collection_items(): + items.append(item.serialize(request)) + + return { + "totalItems": self.items, + "url": self.url_for_self(request.urlgen, qualified=True), + "items": items, + } + class CollectionItem(Base, CollectionItemMixin): __tablename__ = "core__collection_items" @@ -743,7 +898,7 @@ class CollectionItem(Base, CollectionItemMixin): Integer, ForeignKey(MediaEntry.id), nullable=False, index=True) collection = Column(Integer, ForeignKey(Collection.id), nullable=False) note = Column(UnicodeText, nullable=True) - added = Column(DateTime, nullable=False, default=datetime.datetime.now) + added = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) position = Column(Integer) # Cascade: CollectionItems are owned by their Collection. So do the full thing. @@ -770,6 +925,9 @@ class CollectionItem(Base, CollectionItemMixin): collection=self.collection, entry=self.media_entry) + def serialize(self, request): + return self.get_media_entry.serialize(request) + class ProcessingMetaData(Base): __tablename__ = 'core__processing_metadata' @@ -792,7 +950,7 @@ class CommentSubscription(Base): __tablename__ = 'core__comment_subscriptions' id = Column(Integer, primary_key=True) - created = Column(DateTime, nullable=False, default=datetime.datetime.now) + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False) media_entry = relationship(MediaEntry, @@ -823,7 +981,7 @@ class Notification(Base): id = Column(Integer, primary_key=True) type = Column(Unicode) - created = Column(DateTime, nullable=False, default=datetime.datetime.now) + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False, index=True) @@ -883,9 +1041,8 @@ class ProcessingNotification(Notification): 'polymorphic_identity': 'processing_notification' } -with_polymorphic( - Notification, - [ProcessingNotification, CommentNotification]) +# the with_polymorphic call has been moved to the bottom above MODELS +# this is because it causes conflicts with relationship calls. class ReportBase(Base): """ @@ -930,7 +1087,7 @@ class ReportBase(Base): lazy="dynamic", cascade="all, delete-orphan"), primaryjoin="User.id==ReportBase.reported_user_id") - created = Column(DateTime, nullable=False, default=datetime.datetime.now()) + created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) discriminator = Column('type', Unicode(50)) resolver_id = Column(Integer, ForeignKey(User.id)) resolver = relationship( @@ -1068,13 +1225,198 @@ class PrivilegeUserAssociation(Base): ForeignKey(Privilege.id), primary_key=True) +class Generator(Base): + """ Information about what created an activity """ + __tablename__ = "core__generators" + + id = Column(Integer, primary_key=True) + name = Column(Unicode, nullable=False) + published = Column(DateTime, default=datetime.datetime.utcnow) + updated = Column(DateTime, default=datetime.datetime.utcnow) + object_type = Column(Unicode, nullable=False) + + def __repr__(self): + return "<{klass} {name}>".format( + klass=self.__class__.__name__, + name=self.name + ) + + def serialize(self, request): + href = request.urlgen( + "mediagoblin.api.object", + object_type=self.object_type, + id=self.id, + qualified=True + ) + published = UTC.localize(self.published) + updated = UTC.localize(self.updated) + return { + "id": href, + "displayName": self.name, + "published": published.isoformat(), + "updated": updated.isoformat(), + "objectType": self.object_type, + } + + def unserialize(self, data): + if "displayName" in data: + self.name = data["displayName"] + + +class ActivityIntermediator(Base): + """ + This is used so that objects/targets can have a foreign key back to this + object and activities can a foreign key to this object. This objects to be + used multiple times for the activity object or target and also allows for + different types of objects to be used as an Activity. + """ + __tablename__ = "core__activity_intermediators" + + id = Column(Integer, primary_key=True) + type = Column(Unicode, nullable=False) + + TYPES = { + "user": User, + "media": MediaEntry, + "comment": MediaComment, + "collection": Collection, + } + + def _find_model(self, obj): + """ Finds the model for a given object """ + for key, model in self.TYPES.items(): + if isinstance(obj, model): + return key, model + + return None, None + + def set(self, obj): + """ This sets itself as the activity """ + key, model = self._find_model(obj) + if key is None: + raise ValueError("Invalid type of object given") + + self.type = key + + # We need to populate the self.id so we need to save but, we don't + # want to save this AI in the database (yet) so commit=False. + self.save(commit=False) + obj.activity = self.id + obj.save() + + def get(self): + """ Finds the object for an activity """ + if self.type is None: + return None + + model = self.TYPES[self.type] + return model.query.filter_by(activity=self.id).first() + + @validates("type") + def validate_type(self, key, value): + """ Validate that the type set is a valid type """ + assert value in self.TYPES + return value + +class Activity(Base, ActivityMixin): + """ + This holds all the metadata about an activity such as uploading an image, + posting a comment, etc. + """ + __tablename__ = "core__activities" + + id = Column(Integer, primary_key=True) + actor = Column(Integer, + ForeignKey("core__users.id"), + nullable=False) + published = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) + verb = Column(Unicode, nullable=False) + content = Column(Unicode, nullable=True) + title = Column(Unicode, nullable=True) + generator = Column(Integer, + ForeignKey("core__generators.id"), + nullable=True) + object = Column(Integer, + ForeignKey("core__activity_intermediators.id"), + nullable=False) + target = Column(Integer, + ForeignKey("core__activity_intermediators.id"), + nullable=True) + + get_actor = relationship(User, + backref=backref("activities", + cascade="all, delete-orphan")) + get_generator = relationship(Generator) + + def __repr__(self): + if self.content is None: + return "<{klass} verb:{verb}>".format( + klass=self.__class__.__name__, + verb=self.verb + ) + else: + return "<{klass} {content}>".format( + klass=self.__class__.__name__, + content=self.content + ) + + @property + def get_object(self): + if self.object is None: + return None + + ai = ActivityIntermediator.query.filter_by(id=self.object).first() + return ai.get() + + def set_object(self, obj): + self.object = self._set_model(obj) + + @property + def get_target(self): + if self.target is None: + return None + + ai = ActivityIntermediator.query.filter_by(id=self.target).first() + return ai.get() + + def set_target(self, obj): + self.target = self._set_model(obj) + + def _set_model(self, obj): + # Firstly can we set obj + if not hasattr(obj, "activity"): + raise ValueError( + "{0!r} is unable to be set on activity".format(obj)) + + if obj.activity is None: + # We need to create a new AI + ai = ActivityIntermediator() + ai.set(obj) + ai.save() + return ai.id + + # Okay we should have an existing AI + return ActivityIntermediator.query.filter_by(id=obj.activity).first().id + + def save(self, set_updated=True, *args, **kwargs): + if set_updated: + self.updated = datetime.datetime.now() + super(Activity, self).save(*args, **kwargs) + +with_polymorphic( + Notification, + [ProcessingNotification, CommentNotification]) + MODELS = [ User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData, Notification, CommentNotification, ProcessingNotification, Client, CommentSubscription, ReportBase, CommentReport, MediaReport, UserBan, Privilege, PrivilegeUserAssociation, - RequestToken, AccessToken, NonceTimestamp] + RequestToken, AccessToken, NonceTimestamp, + Activity, ActivityIntermediator, Generator, + Location] """ Foundations are the default rows that are created immediately after the tables @@ -1125,7 +1467,7 @@ def show_table_init(engine_uri): if __name__ == '__main__': from sys import argv - print repr(argv) + print(repr(argv)) if len(argv) == 2: uri = argv[1] else: |