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:
parent
51f4911855
commit
b949201152
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
)
|
@ -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}
|
||||
)
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
42
mediagoblin/templates/mediagoblin/federation/activity.html
Normal file
42
mediagoblin/templates/mediagoblin/federation/activity.html
Normal 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 %}
|
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user