aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJessica Tallon <jessica@megworld.co.uk>2014-08-22 18:53:29 +0100
committerJessica Tallon <jessica@megworld.co.uk>2014-08-22 23:18:01 +0100
commitb949201152b2ff3a5b072107ae903ddac309a530 (patch)
tree1823d3ec62b0bcdaee372bfa6ae193087912eb9b
parent51f49118555be3021127602aef78a548850b59b5 (diff)
downloadmediagoblin-b949201152b2ff3a5b072107ae903ddac309a530.tar.lz
mediagoblin-b949201152b2ff3a5b072107ae903ddac309a530.tar.xz
mediagoblin-b949201152b2ff3a5b072107ae903ddac309a530.zip
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.).
-rw-r--r--mediagoblin/db/migrations.py58
-rw-r--r--mediagoblin/db/models.py194
-rw-r--r--mediagoblin/federation/routing.py6
-rw-r--r--mediagoblin/federation/views.py52
-rw-r--r--mediagoblin/submit/lib.py8
-rw-r--r--mediagoblin/templates/mediagoblin/federation/activity.html42
-rw-r--r--mediagoblin/user_pages/views.py6
7 files changed, 343 insertions, 23 deletions
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index 04588ad1..72f85369 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -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()
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index b910e522..89dc2de7 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -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
diff --git a/mediagoblin/federation/routing.py b/mediagoblin/federation/routing.py
index c1c5a264..0b0fbaf1 100644
--- a/mediagoblin/federation/routing.py
+++ b/mediagoblin/federation/routing.py
@@ -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"
+) \ No newline at end of file
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 3d6953a7..7d02d02e 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -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}
+ )
+
+
diff --git a/mediagoblin/submit/lib.py b/mediagoblin/submit/lib.py
index aaa90ea0..af25bfb7 100644
--- a/mediagoblin/submit/lib.py
+++ b/mediagoblin/submit/lib.py
@@ -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))
diff --git a/mediagoblin/templates/mediagoblin/federation/activity.html b/mediagoblin/templates/mediagoblin/federation/activity.html
new file mode 100644
index 00000000..f380fd5f
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/federation/activity.html
@@ -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 %} \ No newline at end of file
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index 78751a28..8203cfa7 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -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))