Create activity model and add activity creation

This creates the Activity and Genrator models from the Activity
Streams spec and. I then created a migration which retro-actively
create activities for media uploaded and comments created. Through
out the code I've added so automatically activties are created when
a user peforms an action (uploading media, commenting, etc.).
This commit is contained in:
Jessica Tallon 2014-08-22 18:53:29 +01:00
parent 51f4911855
commit b949201152
7 changed files with 343 additions and 23 deletions

View File

@ -579,6 +579,29 @@ PRIVILEGE_FOUNDATIONS_v0 = [{'privilege_name':u'admin'},
{'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.
@ -890,3 +913,38 @@ def revert_username_index(db):
db.rollback()
db.commit()
@RegisterMigration(24, MIGRATIONS)
def create_activity_table(db):
""" This will create the activity table """
Activity_R0.__table__.create(db.bind)
Generator_R0.__table__.create(db.bind)
db.commit()
# Create the GNU MediaGoblin generator
gmg_generator = Generator(name="GNU MediaGoblin", object_type="service")
gmg_generator.save()
# 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():
activity = Activity_R0(
verb="create",
actor=media.uploader,
published=media.created,
object_media=media.id,
)
activity.generate_content()
activity.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,
)
activity.generate_content()
activity.save()
db.commit()

View File

@ -37,6 +37,7 @@ from mediagoblin.db.base import Base, DictReadAttrProxy
from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
MediaCommentMixin, CollectionMixin, CollectionItemMixin
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
@ -79,6 +80,8 @@ class User(Base, UserMixin):
## TODO
# plugin data would be in a separate model
objectType = "person"
def __repr__(self):
return '<{0} #{1} {2} {3} "{4}">'.format(
self.__class__.__name__,
@ -143,7 +146,7 @@ class User(Base, UserMixin):
"id": "acct:{0}@{1}".format(self.username, request.host),
"preferredUsername": self.username,
"displayName": "{0}@{1}".format(self.username, request.host),
"objectType": "person",
"objectType": self.objectType,
"pump_io": {
"shared": False,
"followed": False,
@ -651,13 +654,15 @@ class MediaComment(Base, MediaCommentMixin):
lazy="dynamic",
cascade="all, delete-orphan"))
objectType = "comment"
def serialize(self, request):
""" Unserialize to python dictionary for API """
media = MediaEntry.query.filter_by(id=self.media_entry).first()
author = self.get_author
context = {
"id": self.id,
"objectType": "comment",
"objectType": self.objectType,
"content": self.content,
"inReplyTo": media.serialize(request, show_comments=False),
"author": author.serialize(request)
@ -1054,13 +1059,196 @@ class PrivilegeUserAssociation(Base):
ForeignKey(Privilege.id),
primary_key=True)
class Generator(Base):
"""
This holds the information about the software used to create
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)
object_type = Column(Unicode, nullable=False)
def serialize(self, request):
return {
"id": self.id,
"displayName": self.name,
"published": self.published.isoformat(),
"updated": self.updated.isoformat(),
"objectType": self.object_type,
}
def unserialize(self, data):
if "displayName" in data:
self.name = data["displayName"]
class Activity(Base):
"""
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(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)
# 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)
VALID_VERBS = ["add", "author", "create", "delete", "dislike", "favorite",
"follow", "like", "post", "share", "unfavorite", "unfollow",
"unlike", "unshare", "update", "tag"]
@property
def 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 incase it does
return None
self._cached_object = obj
return obj
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?
"""
verb_to_content = {
"add": _("{username} added {object} to {destination}"),
"author": _("{username} authored {object}"),
"create": _("{username} created {object}"),
"delete": _("{username} deleted {object}"),
"dislike": _("{username} disliked {object}"),
"favorite": _("{username} favorited {object}"),
"follow": _("{username} followed {object}"),
"like": _("{username} liked {object}"),
"post": _("{username} posted {object}"),
"share": _("{username} shared {object}"),
"unfavorite": _("{username} unfavorited {object}"),
"unfollow": _("{username} stopped following {object}"),
"unlike": _("{username} unliked {object}"),
"unshare": _("{username} unshared {object}"),
"update": _("{username} updated {object}"),
"tag": _("{username} tagged {object}"),
}
actor = User.query.filter_by(id=self.actor).first()
if self.verb == "add" and self.object.objectType == "collection":
media = MediaEntry.query.filter_by(id=self.object.media_entry)
content = verb_to_content[self.verb]
self.content = content.format(
username=actor.username,
object=media.objectType,
destination=self.object.objectType,
)
elif self.verb in verb_to_content:
content = verb_to_content[self.verb]
self.content = content.format(
username=actor.username,
object=self.object.objectType
)
else:
return
return self.content
def serialize(self, request):
# Lookup models
actor = User.query.filter_by(id=self.actor).first()
generator = Generator.query.filter_by(id=self.generator).first()
obj = {
"id": self.id,
"actor": actor.serialize(request),
"verb": self.verb,
"published": self.published.isoformat(),
"updated": self.updated.isoformat(),
"content": self.content,
"url": self.url(request),
"object": self.object.serialize(request)
}
if self.generator:
obj["generator"] = generator.seralize(request)
if self.title:
obj["title"] = self.title
if self.target:
target = User.query.filter_by(id=self.target).first()
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()
if self.content is None:
self.generate_content()
super(Activity, self).save(*args, **kwargs)
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, Generator]
"""
Foundations are the default rows that are created immediately after the tables

View File

@ -77,3 +77,9 @@ add_route(
"/api/whoami",
"mediagoblin.federation.views:whoami"
)
add_route(
"mediagoblin.federation.activity_view",
"/<string:username>/activity/<string:id>",
"mediagoblin.federation.views:activity_view"
)

View File

@ -20,10 +20,11 @@ import mimetypes
from werkzeug.datastructures import FileStorage
from mediagoblin.decorators import oauth_required
from mediagoblin.decorators import oauth_required, require_active_login
from mediagoblin.federation.decorators import user_has_privilege
from mediagoblin.db.models import User, MediaEntry, MediaComment
from mediagoblin.tools.response import redirect, json_response, json_error
from mediagoblin.db.models import User, MediaEntry, MediaComment, Activity
from mediagoblin.tools.response import redirect, json_response, json_error, \
render_404, render_to_response
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.submit.lib import new_upload_entry, api_upload_request, \
api_add_to_feed
@ -340,21 +341,8 @@ def feed_endpoint(request):
"items": [],
}
# Look up all the media to put in the feed (this will be changed
# when we get real feeds/inboxes/outboxes/activites)
for media in MediaEntry.query.all():
item = {
"verb": "post",
"object": media.serialize(request),
"actor": media.get_uploader.serialize(request),
"content": "{0} posted a picture".format(request.user.username),
"id": media.id,
}
item["updated"] = item["object"]["updated"]
item["published"] = item["object"]["published"]
item["url"] = item["object"]["url"]
feed["items"].append(item)
for activity in Activity.query.filter_by(actor=request.user.id):
feed["items"].append(activity.serialize(request))
feed["totalItems"] = len(feed["items"])
return json_response(feed)
@ -467,3 +455,31 @@ def whoami(request):
)
return redirect(request, location=profile)
@require_active_login
def activity_view(request):
""" /<username>/activity/<id> - Display activity
This should display a HTML presentation of the activity
this is NOT an API endpoint.
"""
# Get the user object.
username = request.matchdict["username"]
user = User.query.filter_by(username=username).first()
activity_id = request.matchdict["id"]
if request.user is None:
return render_404(request)
activity = Activity.query.filter_by(id=activity_id).first()
if activity is None:
return render_404(request)
return render_to_response(
request,
"mediagoblin/federation/activity.html",
{"activity": activity}
)

View File

@ -24,6 +24,7 @@ from werkzeug.datastructures import FileStorage
from mediagoblin import mg_globals
from mediagoblin.tools.response import json_response
from mediagoblin.tools.text import convert_to_tag_list_of_dicts
from mediagoblin.tools.federation import create_activity
from mediagoblin.db.models import MediaEntry, ProcessingMetaData
from mediagoblin.processing import mark_entry_failed
from mediagoblin.processing.task import ProcessMedia
@ -199,6 +200,9 @@ def submit_media(mg_app, user, submitted_file, filename,
run_process_media(entry, feed_url)
add_comment_subscription(user, entry)
# Create activity
create_activity("post", entry)
return entry
@ -289,4 +293,8 @@ def api_add_to_feed(request, entry):
run_process_media(entry, feed_url)
add_comment_subscription(request.user, entry)
# Create activity
create_activity("post", entry)
return json_response(entry.serialize(request))

View File

@ -0,0 +1,42 @@
{#
# GNU MediaGoblin -- federated, autonomous media hosting
# Copyright (C) 2014 MediaGoblin contributors. See AUTHORS.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#}
{%- extends "mediagoblin/base.html" %}
{% block mediagoblin_head %}
{% template_hook("media_head") %}
{% endblock mediagoblin_head %}
{% block mediagoblin_content %}
<div class="media_pane eleven columns">
<h2 class="media_title">
{% if activity.title %}{{ activity.title }}{% endif %}
</h2>
{% autoescape False %}
<p> {{ activity.content }} </p>
{% endautoescape %}
<div class="media_sidebar">
{% block mediagoblin_after_added_sidebar %}
<a href="{{ activity.url(request) }}"
class="button_action"
id="button_reportmedia">
View {{ activity.object.objectType }}
</a>
{% endblock %}
</div>
{% endblock %}

View File

@ -26,6 +26,7 @@ from mediagoblin.tools.response import render_to_response, render_404, \
from mediagoblin.tools.text import cleaned_markdown_conversion
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.pagination import Pagination
from mediagoblin.tools.federation import create_activity
from mediagoblin.user_pages import forms as user_forms
from mediagoblin.user_pages.lib import (send_comment_email,
add_media_to_collection, build_report_object)
@ -199,7 +200,7 @@ def media_post_comment(request, media):
_('Your comment has been posted!'))
trigger_notification(comment, media, request)
create_activity("post", comment)
add_comment_subscription(request.user, media)
return redirect_obj(request, media)
@ -261,6 +262,7 @@ def media_collect(request, media):
collection.creator = request.user.id
collection.generate_slug()
collection.save()
create_activity("create", collection)
# Otherwise, use the collection selected from the drop-down
else:
@ -287,7 +289,7 @@ def media_collect(request, media):
% (media.title, collection.title))
else: # Add item to collection
add_media_to_collection(collection, media, form.note.data)
create_activity("add", media)
messages.add_message(request, messages.SUCCESS,
_('"%s" added to collection "%s"')
% (media.title, collection.title))