Add ActivityIntermediator table and refactor some of Activity model

- This has introduced a intermediatory table between object/target and
  the activity. This allows for multiple activities to be associated
  with one object/target.
- This moves some of the methods off Activity model into a mixin which
  didn't need to interact with database things.
- This also cleaned up the migrations as well as adding retroactive
  creation of activities for collection creation.
This commit is contained in:
Jessica Tallon 2014-08-27 14:34:07 +01:00
parent 1c15126819
commit ce46470c02
3 changed files with 366 additions and 222 deletions

View File

@ -29,7 +29,7 @@ from mediagoblin.db.extratypes import JSONEncoded, MutationDict
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
@ -578,30 +578,6 @@ PRIVILEGE_FOUNDATIONS_v0 = [{'privilege_name':u'admin'},
{'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.
@ -914,17 +890,91 @@ def revert_username_index(db):
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():
@ -932,19 +982,40 @@ def create_activity_table(db):
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()
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(set_updated=False)
activity.set_object(collection)
collection.save()
db.commit()

View File

@ -39,6 +39,7 @@ from mediagoblin.tools import common, licenses
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):
@ -208,7 +209,7 @@ class MediaEntryMixin(GenerateSlugMixin):
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"]
)
@ -363,3 +364,109 @@ class CollectionItemMixin(object):
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"]

View File

@ -35,9 +35,9 @@ 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.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
@ -77,6 +77,11 @@ class User(Base, UserMixin):
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
@ -313,6 +318,11 @@ class MediaEntry(Base, MediaEntryMixin):
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
@ -656,8 +666,14 @@ class MediaComment(Base, MediaCommentMixin):
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()
@ -722,6 +738,11 @@ class Collection(Base, CollectionMixin):
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'),
{})
@ -1069,13 +1090,13 @@ class Generator(Base):
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,
@ -1084,199 +1105,144 @@ class Generator(Base):
"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 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
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
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:
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
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)
self.object.set_object(*args, **kwargs)
self.object.save()
@property
def get_object(self):
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):
if self.target is None:
return None
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,
@ -1285,7 +1251,7 @@ MODELS = [
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