from mediagoblin.db.migration_tools import (
RegisterMigration, inspect_table, replace_table_hack)
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
- Privilege)
+ Privilege, Generator)
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
{'privilege_name':u'commenter'},
{'privilege_name':u'active'}]
-
-class Activity_R0(declarative_base()):
- __tablename__ = "core__activities"
- id = Column(Integer, primary_key=True)
- actor = Column(Integer, ForeignKey(User.id), nullable=False)
- published = Column(DateTime, nullable=False, default=datetime.datetime.now)
- updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
- verb = Column(Unicode, nullable=False)
- content = Column(Unicode, nullable=False)
- title = Column(Unicode, nullable=True)
- target = Column(Integer, ForeignKey(User.id), nullable=True)
- object_comment = Column(Integer, ForeignKey(MediaComment.id), nullable=True)
- object_collection = Column(Integer, ForeignKey(Collection.id), nullable=True)
- object_media = Column(Integer, ForeignKey(MediaEntry.id), nullable=True)
- object_user = Column(Integer, ForeignKey(User.id), nullable=True)
-
-class Generator(declarative_base()):
- __tablename__ = "core__generators"
- id = Column(Integer, primary_key=True)
- name = Column(Unicode, nullable=False)
- published = Column(DateTime, nullable=False, default=datetime.datetime.now)
- updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
- object_type = Column(Unicode, nullable=False)
-
# vR1 stands for "version Rename 1". This only exists because we need
# to deal with dropping some booleans and it's otherwise impossible
# with sqlite.
db.commit()
+class Generator_R0(declarative_base()):
+ __tablename__ = "core__generators"
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode, nullable=False)
+ published = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ object_type = Column(Unicode, nullable=False)
+
+class Activity_R0(declarative_base()):
+ __tablename__ = "core__activities"
+ id = Column(Integer, primary_key=True)
+ actor = Column(Integer, ForeignKey(User.id), nullable=False)
+ published = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ verb = Column(Unicode, nullable=False)
+ content = Column(Unicode, nullable=False)
+ title = Column(Unicode, nullable=True)
+ target = Column(Integer, ForeignKey(User.id), nullable=True)
+ generator = Column(Integer, ForeignKey(Generator.id), nullable=True)
+
+class ActivityIntermediator_R0(declarative_base()):
+ __tablename__ = "core__acitivity_intermediators"
+ id = Column(Integer, primary_key=True)
+ type = Column(Integer, nullable=False)
+
@RegisterMigration(24, MIGRATIONS)
-def create_activity_table(db):
- """ This will create the activity table """
+def activity_migration(db):
+ """
+ Creates everything to create activities in GMG
+ - Adds Activity, ActivityIntermediator and Generator table
+ - Creates GMG service generator for activities produced by the server
+ - Adds the activity_as_object and activity_as_target to objects/targets
+ - Retroactively adds activities for what we can acurately work out
+ """
+ # Set constants we'll use later
+ FOREIGN_KEY = "core__acitivity_intermediators.id"
+
+
+ # Create the new tables.
Activity_R0.__table__.create(db.bind)
Generator_R0.__table__.create(db.bind)
+ ActivityIntermediator_R0.__table__.create(db.bind)
db.commit()
-
- # Create the GNU MediaGoblin generator
- gmg_generator = Generator(name="GNU MediaGoblin", object_type="service")
- gmg_generator.save()
-
+
+
+ # Initiate the tables we want to use later
+ metadata = MetaData(bind=db.bind)
+ user_table = inspect_table(metadata, "core__users")
+ generator_table = inspect_table(metadata, "core__generators")
+ collection_table = inspect_table(metadata, "core__collections")
+ media_entry_table = inspect_table(metadata, "core__media_entries")
+ media_comments_table = inspect_table(metadata, "core__media_comments")
+
+
+ # Create the foundations for Generator
+ db.execute(generator_table.insert().values(
+ name="GNU Mediagoblin",
+ object_type="service"
+ ))
+ db.commit()
+
+
+ # Now we want to modify the tables which MAY have an activity at some point
+ as_object = Column("activity_as_object", Integer, ForeignKey(FOREIGN_KEY))
+ as_object.create(media_entry_table)
+ as_target = Column("activity_as_target", Integer, ForeignKey(FOREIGN_KEY))
+ as_target.create(media_entry_table)
+
+ as_object = Column("activity_as_object", Integer, ForeignKey(FOREIGN_KEY))
+ as_object.create(user_table)
+ as_target = Column("activity_as_target", Integer, ForeignKey(FOREIGN_KEY))
+ as_target.create(user_table)
+
+ as_object = Column("activity_as_object", Integer, ForeignKey(FOREIGN_KEY))
+ as_object.create(media_comments_table)
+ as_target = Column("activity_as_target", Integer, ForeignKey(FOREIGN_KEY))
+ as_target.create(media_comments_table)
+
+ as_object = Column("activity_as_object", Integer, ForeignKey(FOREIGN_KEY))
+ as_object.create(collection_table)
+ as_target = Column("activity_as_target", Integer, ForeignKey(FOREIGN_KEY))
+ as_target.create(collection_table)
+ db.commit()
+
+
# Now we want to retroactively add what activities we can
# first we'll add activities when people uploaded media.
for media in MediaEntry.query.all():
verb="create",
actor=media.uploader,
published=media.created,
- object_media=media.id,
+ updated=media.created,
+ generator=gmg_generator.id
)
activity.generate_content()
- activity.save()
-
+ activity.save(set_updated=False)
+ activity.set_object(media)
+ media.save()
+
# Now we want to add all the comments people made
for comment in MediaComment.query.all():
activity = Activity_R0(
verb="comment",
actor=comment.author,
published=comment.created,
+ updated=comment.created,
+ generator=gmg_generator.id
+ )
+ activity.generate_content()
+ activity.save(set_updated=False)
+ activity.set_object(comment)
+ comment.save()
+
+ # Create 'create' activities for all collections
+ for collection in Collection.query.all():
+ activity = Activity_R0(
+ verb="create",
+ actor=collection.creator,
+ published=collection.created,
+ updated=collection.created,
+ generator=gmg_generator.id
)
activity.generate_content()
- activity.save()
-
+ activity.save(set_updated=False)
+ activity.set_object(collection)
+ collection.save()
+
db.commit()
from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin.tools.text import cleaned_markdown_conversion
from mediagoblin.tools.url import slugify
+from mediagoblin.tools.translate import pass_to_ugettext as _
class UserMixin(object):
will return self.thumb_url if original url doesn't exist"""
if u"original" not in self.media_files:
return self.thumb_url
-
+
return mg_globals.app.public_store.file_url(
self.media_files[u"original"]
)
Run through Markdown and the HTML cleaner.
"""
return cleaned_markdown_conversion(self.note)
+
+class ActivityMixin(object):
+
+ VALID_VERBS = ["add", "author", "create", "delete", "dislike", "favorite",
+ "follow", "like", "post", "share", "unfavorite", "unfollow",
+ "unlike", "unshare", "update", "tag"]
+
+ def get_url(self, request):
+ return request.urlgen(
+ "mediagoblin.federation.activity_view",
+ username=self.get_actor.username,
+ id=self.id,
+ qualified=True
+ )
+
+ def generate_content(self):
+ """ Produces a HTML content for object """
+ # some of these have simple and targetted. If self.target it set
+ # it will pick the targetted. If they DON'T have a targetted version
+ # the information in targetted won't be added to the content.
+ verb_to_content = {
+ "add": {
+ "simple" : _("{username} added {object}"),
+ "targetted": _("{username} added {object} to {target}"),
+ },
+ "author": {"simple": _("{username} authored {object}")},
+ "create": {"simple": _("{username} created {object}")},
+ "delete": {"simple": _("{username} deleted {object}")},
+ "dislike": {"simple": _("{username} disliked {object}")},
+ "favorite": {"simple": _("{username} favorited {object}")},
+ "follow": {"simple": _("{username} followed {object}")},
+ "like": {"simple": _("{username} liked {object}")},
+ "post": {
+ "simple": _("{username} posted {object}"),
+ "targetted": _("{username} posted {object} to {targetted}"),
+ },
+ "share": {"simple": _("{username} shared {object}")},
+ "unfavorite": {"simple": _("{username} unfavorited {object}")},
+ "unfollow": {"simple": _("{username} stopped following {object}")},
+ "unlike": {"simple": _("{username} unliked {object}")},
+ "unshare": {"simple": _("{username} unshared {object}")},
+ "update": {"simple": _("{username} updated {object}")},
+ "tag": {"simple": _("{username} tagged {object}")},
+ }
+
+ obj = self.get_object()
+ target = self.get_target()
+ actor = self.get_actor
+ content = verb_to_content.get(self.verb, None)
+
+ if content is None or obj is None:
+ return
+
+ if target is None or "targetted" not in content:
+ self.content = content["simple"].format(
+ username=actor.username,
+ object=obj.objectType
+ )
+ else:
+ self.content = content["targetted"].format(
+ username=actor.username,
+ object=obj.objectType,
+ target=target.objectType,
+ )
+
+ return self.content
+
+ def serialize(self, request):
+ obj = {
+ "id": self.id,
+ "actor": self.get_actor.serialize(request),
+ "verb": self.verb,
+ "published": self.published.isoformat(),
+ "updated": self.updated.isoformat(),
+ "content": self.content,
+ "url": self.get_url(request),
+ "object": self.get_object().serialize(request)
+ }
+
+ if self.generator:
+ obj["generator"] = self.get_generator.seralize(request)
+
+ if self.title:
+ obj["title"] = self.title
+
+ target = self.get_target()
+ if target is not None:
+ obj["target"] = target.seralize(request)
+
+ return obj
+
+ def unseralize(self, data):
+ """
+ Takes data given and set it on this activity.
+
+ Several pieces of data are not written on because of security
+ reasons. For example changing the author or id of an activity.
+ """
+ if "verb" in data:
+ self.verb = data["verb"]
+
+ if "title" in data:
+ self.title = data["title"]
+
+ if "content" in data:
+ self.content = data["content"]
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.translate import pass_to_ugettext as _
from mediagoblin.tools.common import import_component
# It's actually kind of annoying how sqlalchemy-migrate does this, if
uploaded = Column(Integer, default=0)
upload_limit = Column(Integer)
+ activity_as_object = Column(Integer,
+ ForeignKey("core__acitivity_intermediators.id"))
+ activity_as_target = Column(Integer,
+ ForeignKey("core__acitivity_intermediators.id"))
+
## TODO
# plugin data would be in a separate model
media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
default=MutationDict())
+ activity_as_object = Column(Integer,
+ ForeignKey("core__acitivity_intermediators.id"))
+ activity_as_target = Column(Integer,
+ ForeignKey("core__acitivity_intermediators.id"))
+
## TODO
# fail_error
lazy="dynamic",
cascade="all, delete-orphan"))
+
+ activity_as_object = Column(Integer,
+ ForeignKey("core__acitivity_intermediators.id"))
+ activity_as_target = Column(Integer,
+ ForeignKey("core__acitivity_intermediators.id"))
+
objectType = "comment"
-
+
def serialize(self, request):
""" Unserialize to python dictionary for API """
media = MediaEntry.query.filter_by(id=self.media_entry).first()
backref=backref("collections",
cascade="all, delete-orphan"))
+ activity_as_object = Column(Integer,
+ ForeignKey("core__acitivity_intermediators.id"))
+ activity_as_target = Column(Integer,
+ ForeignKey("core__acitivity_intermediators.id"))
+
__table_args__ = (
UniqueConstraint('creator', 'slug'),
{})
objects for the pump.io APIs.
"""
__tablename__ = "core__generators"
-
+
id = Column(Integer, primary_key=True)
name = Column(Unicode, nullable=False)
- published = Column(DateTime, nullable=False, default=datetime.datetime.now)
- updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ published = Column(DateTime, default=datetime.datetime.now)
+ updated = Column(DateTime, default=datetime.datetime.now)
object_type = Column(Unicode, nullable=False)
-
+
def serialize(self, request):
return {
"id": self.id,
"updated": self.updated.isoformat(),
"objectType": self.object_type,
}
-
+
def unserialize(self, data):
if "displayName" in data:
self.name = data["displayName"]
-
-
-class Activity(Base):
+
+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__acitivity_intermediators"
+
+ id = Column(Integer, primary_key=True)
+ type = Column(Integer, nullable=False)
+
+ TYPES = {
+ 0: User,
+ 1: MediaEntry,
+ 2: MediaComment,
+ 3: 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_object(self, obj):
+ """ This sets itself as the object for an activity """
+ key, model = self._find_model(obj)
+ if key is None:
+ raise ValueError("Invalid type of object given")
+
+ # First set self as activity
+ obj.activity_as_object = self.id
+ self.type = key
+
+ @property
+ def get_object(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_as_object=self.id).first()
+
+ def set_target(self, obj):
+ """ This sets itself as the target for an activity """
+ key, model = self._find_model(obj)
+ if key is None:
+ raise ValueError("Invalid type of object given")
+
+ obj.activity_as_target = self.id
+ self.type = key
+
+ @property
+ def get_target(self):
+ """ Gets the target for an activity """
+ if self.type is None:
+ return None
+
+ model = self.TYPES[self.type]
+ return model.query.filter_by(activity_as_target=self.id).first()
+
+ def save(self, *args, **kwargs):
+ if self.type not in self.TYPES.keys():
+ raise ValueError("Invalid type set")
+ Base.save(self, *args, **kwargs)
+
+class Activity(Base, ActivityMixin):
"""
This holds all the metadata about an activity such as uploading an image,
- posting a comment, etc.
+ posting a comment, etc.
"""
__tablename__ = "core__activities"
-
+
id = Column(Integer, primary_key=True)
- actor = Column(Integer, ForeignKey(User.id), nullable=False)
+ actor = Column(Integer,
+ ForeignKey(User.id, use_alter=True, name="actor"),
+ nullable=False)
published = Column(DateTime, nullable=False, default=datetime.datetime.now)
updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
verb = Column(Unicode, nullable=False)
content = Column(Unicode, nullable=True)
title = Column(Unicode, nullable=True)
- target = Column(Integer, ForeignKey(User.id), nullable=True)
generator = Column(Integer, ForeignKey(Generator.id), nullable=True)
-
-
- # Links to other models (only one of these should have a value).
- object_comment = Column(Integer, ForeignKey(MediaComment.id), nullable=True)
- object_collection = Column(Integer, ForeignKey(Collection.id), nullable=True)
- object_media = Column(Integer, ForeignKey(MediaEntry.id), nullable=True)
- object_user = Column(Integer, ForeignKey(User.id), nullable=True)
-
- # The target could also be several things
- target_comment = Column(Integer, ForeignKey(MediaComment.id), nullable=True)
- target_collection = Column(Integer, ForeignKey(Collection.id), nullable=True)
- target_media = Column(Integer, ForeignKey(MediaEntry.id), nullable=True)
- target_user = Column(Integer, ForeignKey(User.id), nullable=True)
-
- get_actor = relationship(User, foreign_keys="Activity.actor")
+ object = Column(Integer,
+ ForeignKey(ActivityIntermediator.id), nullable=False)
+ target = Column(Integer,
+ ForeignKey(ActivityIntermediator.id), nullable=True)
+
+ get_actor = relationship(User,
+ foreign_keys="Activity.actor", post_update=True)
get_generator = relationship(Generator)
-
- VALID_VERBS = ["add", "author", "create", "delete", "dislike", "favorite",
- "follow", "like", "post", "share", "unfavorite", "unfollow",
- "unlike", "unshare", "update", "tag"]
-
+
+ def set_object(self, *args, **kwargs):
+ if self.object is None:
+ ai = ActivityIntermediator()
+ ai.set_object(*args, **kwargs)
+ ai.save()
+ self.object = ai.id
+ return
+
+ self.object.set_object(*args, **kwargs)
+ self.object.save()
+
+ @property
def get_object(self):
- """ This represents the object that is given to the activity """
- # Do we have a cached version
- if getattr(self, "_cached_object", None) is not None:
- return self._cached_object
-
- if self.object_comment is not None:
- obj = MediaComment.query.filter_by(id=self.object_comment).first()
- elif self.object_collection is not None:
- obj = Collection.query.filter_by(id=self.object_collection).first()
- elif self.object_media is not None:
- obj = MediaEntry.query.filter_by(id=self.object_media).first()
- elif self.object_user is not None:
- obj = User.query.filter_by(id=self.object_user).first()
- else:
- # Shouldn't happen but in case it does
- return None
-
- self._cached_object = obj
- return obj
-
+ return self.object.get_object
+
+ def set_target(self, *args, **kwargs):
+ if self.target is None:
+ ai = ActivityIntermediator()
+ ai.set_target(*args, **kwargs)
+ ai.save()
+ self.object = ai.id
+ return
+
+ self.target.set_object(*args, **kwargs)
+ self.targt.save()
+
+ @property
def get_target(self):
- """ This represents the target given on the activity (if any) """
- if getattr(self, "_cached_target", None) is not None:
- return self._cached_target
-
- if self.target_comment is not None:
- target = MediaComment.query.filter_by(id=self.target_comment).first()
- elif self.target_collection is not None:
- target = Collection.query.filter_by(id=self.target_collection).first()
- elif self.target_media is not None:
- target = MediaEntry.query.filter_by(id=self.target_media).first()
- elif self.target_user is not None:
- target = User.query.filter_by(id=self.target_user).first()
- else:
- # Shouldn't happen but in case it does
+ if self.target is None:
return None
-
- self._cached_target = target
- return self._cached_target
-
-
- def url(self, request):
- actor = User.query.filter_by(id=self.actor).first()
- return request.urlgen(
- "mediagoblin.federation.activity_view",
- username=actor.username,
- id=self.id,
- qualified=True
- )
-
- def generate_content(self):
- """
- Produces a HTML content for object
- TODO: Can this be moved to a mixin?
- """
- # some of these have simple and targetted. If self.target it set
- # it will pick the targetted. If they DON'T have a targetted version
- # the information in targetted won't be added to the content.
- verb_to_content = {
- "add": {
- "simple" : _("{username} added {object}"),
- "targetted": _("{username} added {object} to {target}"),
- },
- "author": {"simple": _("{username} authored {object}")},
- "create": {"simple": _("{username} created {object}")},
- "delete": {"simple": _("{username} deleted {object}")},
- "dislike": {"simple": _("{username} disliked {object}")},
- "favorite": {"simple": _("{username} favorited {object}")},
- "follow": {"simple": _("{username} followed {object}")},
- "like": {"simple": _("{username} liked {object}")},
- "post": {
- "simple": _("{username} posted {object}"),
- "targetted": _("{username} posted {object} to {targetted}"),
- },
- "share": {"simple": _("{username} shared {object}")},
- "unfavorite": {"simple": _("{username} unfavorited {object}")},
- "unfollow": {"simple": _("{username} stopped following {object}")},
- "unlike": {"simple": _("{username} unliked {object}")},
- "unshare": {"simple": _("{username} unshared {object}")},
- "update": {"simple": _("{username} updated {object}")},
- "tag": {"simple": _("{username} tagged {object}")},
- }
-
- obj = self.get_object()
- target = self.get_target()
- actor = self.get_actor
- content = verb_to_content.get(self.verb, None)
-
- if content is None or obj is None:
- return
-
- if target is None or "targetted" not in content:
- self.content = content["simple"].format(
- username=actor.username,
- object=obj.objectType
- )
- else:
- self.content = content["targetted"].format(
- username=actor.username,
- object=obj.objectType,
- target=target.objectType,
- )
-
- return self.content
-
- def serialize(self, request):
- obj = {
- "id": self.id,
- "actor": self.get_actor.serialize(request),
- "verb": self.verb,
- "published": self.published.isoformat(),
- "updated": self.updated.isoformat(),
- "content": self.content,
- "url": self.url(request),
- "object": self.get_object().serialize(request)
- }
-
- if self.generator:
- obj["generator"] = generator.seralize(request)
-
- if self.title:
- obj["title"] = self.title
-
- target = self.get_target()
- if target is not None:
- obj["target"] = target.seralize(request)
-
- return obj
-
- def unseralize(self, data):
- """
- Takes data given and set it on this activity.
-
- Several pieces of data are not written on because of security
- reasons. For example changing the author or id of an activity.
- """
- if "verb" in data:
- self.verb = data["verb"]
-
- if "title" in data:
- self.title = data["title"]
-
- if "content" in data:
- self.content = data["content"]
-
- def save(self, *args, **kwargs):
- self.updated = datetime.datetime.now()
- super(Activity, self).save(*args, **kwargs)
+
+ return self.target.get_target
+
+ def save(self, set_updated=True, *args, **kwargs):
+ if set_updated:
+ self.updated = datetime.datetime.now()
+ super(Activity, self).save(*args, **kwargs)
MODELS = [
User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
CommentSubscription, ReportBase, CommentReport, MediaReport, UserBan,
Privilege, PrivilegeUserAssociation,
RequestToken, AccessToken, NonceTimestamp,
- Activity, Generator]
+ Activity, ActivityIntermediator, Generator]
"""
Foundations are the default rows that are created immediately after the tables