Merge branch '905-activities'

Add Activity and Generator models which allow for
    activities to be created. This now works with the
    feed API.
This commit is contained in:
Jessica Tallon 2014-10-07 10:01:38 +01:00
commit 9a1fc423ac
9 changed files with 670 additions and 42 deletions

View File

@ -34,7 +34,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
@ -583,7 +583,6 @@ PRIVILEGE_FOUNDATIONS_v0 = [{'privilege_name':u'admin'},
{'privilege_name':u'commenter'},
{'privilege_name':u'active'}]
# 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.
@ -895,3 +894,195 @@ def revert_username_index(db):
db.rollback()
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 ActivityIntermediator_R0(declarative_base()):
__tablename__ = "core__activity_intermediators"
id = Column(Integer, primary_key=True)
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=True)
title = Column(Unicode, nullable=True)
generator = Column(Integer, ForeignKey(Generator_R0.id), nullable=True)
object = Column(Integer,
ForeignKey(ActivityIntermediator_R0.id),
nullable=False)
target = Column(Integer,
ForeignKey(ActivityIntermediator_R0.id),
nullable=True)
@RegisterMigration(24, MIGRATIONS)
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__activity_intermediators.id"
ACTIVITY_COLUMN = "activity"
# Create the new tables.
ActivityIntermediator_R0.__table__.create(db.bind)
Generator_R0.__table__.create(db.bind)
Activity_R0.__table__.create(db.bind)
db.commit()
# Initiate the tables we want to use later
metadata = MetaData(bind=db.bind)
user_table = inspect_table(metadata, "core__users")
activity_table = inspect_table(metadata, "core__activities")
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")
ai_table = inspect_table(metadata, "core__activity_intermediators")
# Create the foundations for Generator
db.execute(generator_table.insert().values(
name="GNU Mediagoblin",
object_type="service",
published=datetime.datetime.now(),
updated=datetime.datetime.now()
))
db.commit()
# Get the ID of that generator
gmg_generator = db.execute(generator_table.select(
generator_table.c.name==u"GNU Mediagoblin")).first()
# Now we want to modify the tables which MAY have an activity at some point
media_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
media_col.create(media_entry_table)
user_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
user_col.create(user_table)
comments_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
comments_col.create(media_comments_table)
collection_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
collection_col.create(collection_table)
db.commit()
# Now we want to retroactively add what activities we can
# first we'll add activities when people uploaded media.
# these can't have content as it's not fesible to get the
# correct content strings.
for media in db.execute(media_entry_table.select()):
# Now we want to create the intermedaitory
db_ai = db.execute(ai_table.insert().values(
type="media",
))
db_ai = db.execute(ai_table.select(
ai_table.c.id==db_ai.inserted_primary_key[0]
)).first()
# Add the activity
activity = {
"verb": "create",
"actor": media.uploader,
"published": media.created,
"updated": media.created,
"generator": gmg_generator.id,
"object": db_ai.id
}
db.execute(activity_table.insert().values(**activity))
# Add the AI to the media.
db.execute(media_entry_table.update().values(
activity=db_ai.id
).where(media_entry_table.c.id==media.id))
# Now we want to add all the comments people made
for comment in db.execute(media_comments_table.select()):
# Get the MediaEntry for the comment
media_entry = db.execute(
media_entry_table.select(
media_entry_table.c.id==comment.media_entry
)).first()
# Create an AI for target
db_ai_media = db.execute(ai_table.select(
ai_table.c.id==media_entry.activity
)).first().id
db.execute(
media_comments_table.update().values(
activity=db_ai_media
).where(media_comments_table.c.id==media_entry.id))
# Now create the AI for the comment
db_ai_comment = db.execute(ai_table.insert().values(
type="comment"
)).inserted_primary_key[0]
activity = {
"verb": "comment",
"actor": comment.author,
"published": comment.created,
"updated": comment.created,
"generator": gmg_generator.id,
"object": db_ai_comment,
"target": db_ai_media,
}
# Now add the comment object
db.execute(activity_table.insert().values(**activity))
# Now add activity to comment
db.execute(media_comments_table.update().values(
activity=db_ai_comment
).where(media_comments_table.c.id==comment.id))
# Create 'create' activities for all collections
for collection in db.execute(collection_table.select()):
# create AI
db_ai = db.execute(ai_table.insert().values(
type="collection"
))
db_ai = db.execute(ai_table.select(
ai_table.c.id==db_ai.inserted_primary_key[0]
)).first()
# Now add link the collection to the AI
db.execute(collection_table.update().values(
activity=db_ai.id
).where(collection_table.c.id==collection.id))
activity = {
"verb": "create",
"actor": collection.creator,
"published": collection.created,
"updated": collection.created,
"generator": gmg_generator.id,
"object": db_ai.id,
}
db.execute(activity_table.insert().values(**activity))
# Now add the activity to the collection
db.execute(collection_table.update().values(
activity=db_ai.id
).where(collection_table.c.id==collection.id))
db.commit()

View File

@ -39,9 +39,12 @@ 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):
object_type = "person"
@property
def bio_html(self):
return cleaned_markdown_conversion(self.bio)
@ -130,6 +133,11 @@ class MediaEntryMixin(GenerateSlugMixin):
return check_media_slug_used(self.uploader, slug, self.id)
@property
def object_type(self):
""" Converts media_type to pump-like type - don't use internally """
return self.media_type.split(".")[-1]
@property
def description_html(self):
"""
@ -208,7 +216,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"]
)
@ -297,6 +305,8 @@ class MediaEntryMixin(GenerateSlugMixin):
class MediaCommentMixin(object):
object_type = "comment"
@property
def content_html(self):
"""
@ -321,6 +331,8 @@ class MediaCommentMixin(object):
class CollectionMixin(GenerateSlugMixin):
object_type = "collection"
def check_slug_used(self, slug):
# import this here due to a cyclic import issue
# (db.models -> db.mixin -> db.util -> db.models)
@ -363,3 +375,111 @@ class CollectionItemMixin(object):
Run through Markdown and the HTML cleaner.
"""
return cleaned_markdown_conversion(self.note)
class ActivityMixin(object):
object_type = "activity"
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 {target}"),
},
"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.object_type
)
else:
self.content = content["targetted"].format(
username=actor.username,
object=obj.object_type,
target=target.object_type,
)
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),
"objectType": self.object_type,
}
if self.generator:
obj["generator"] = self.get_generator.serialize(request)
if self.title:
obj["title"] = self.title
target = self.get_target
if target is not None:
obj["target"] = target.serialize(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

@ -36,7 +36,8 @@ 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.common import import_component
@ -71,6 +72,8 @@ class User(Base, UserMixin):
uploaded = Column(Integer, default=0)
upload_limit = Column(Integer)
activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
## TODO
# plugin data would be in a separate model
@ -138,7 +141,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.object_type,
"pump_io": {
"shared": False,
"followed": False,
@ -309,6 +312,8 @@ class MediaEntry(Base, MediaEntryMixin):
media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
default=MutationDict())
activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
## TODO
# fail_error
@ -430,18 +435,13 @@ class MediaEntry(Base, MediaEntryMixin):
# pass through commit=False/True in kwargs
super(MediaEntry, self).delete(**kwargs)
@property
def objectType(self):
""" Converts media_type to pump-like type - don't use internally """
return self.media_type.split(".")[-1]
def serialize(self, request, show_comments=True):
""" Unserialize MediaEntry to object """
author = self.get_uploader
context = {
"id": self.id,
"author": author.serialize(request),
"objectType": self.objectType,
"objectType": self.object_type,
"url": self.url_for_self(request.urlgen),
"image": {
"url": request.host_url + self.thumb_url[1:],
@ -458,7 +458,7 @@ class MediaEntry(Base, MediaEntryMixin):
"self": {
"href": request.urlgen(
"mediagoblin.federation.object",
objectType=self.objectType,
object_type=self.object_type,
id=self.id,
qualified=True
),
@ -477,14 +477,15 @@ class MediaEntry(Base, MediaEntryMixin):
context["license"] = self.license
if show_comments:
comments = [comment.serialize(request) for comment in self.get_comments()]
comments = [
comment.serialize(request) for comment in self.get_comments()]
total = len(comments)
context["replies"] = {
"totalItems": total,
"items": comments,
"url": request.urlgen(
"mediagoblin.federation.object.comments",
objectType=self.objectType,
object_type=self.object_type,
id=self.id,
qualified=True
),
@ -650,13 +651,16 @@ class MediaComment(Base, MediaCommentMixin):
lazy="dynamic",
cascade="all, delete-orphan"))
activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
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.object_type,
"content": self.content,
"inReplyTo": media.serialize(request, show_comments=False),
"author": author.serialize(request)
@ -714,6 +718,8 @@ class Collection(Base, CollectionMixin):
backref=backref("collections",
cascade="all, delete-orphan"))
activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
__table_args__ = (
UniqueConstraint('creator', 'slug'),
{})
@ -1068,13 +1074,183 @@ class PrivilegeUserAssociation(Base):
ForeignKey(Privilege.id),
primary_key=True)
class Generator(Base):
""" Information about what created an activity """
__tablename__ = "core__generators"
id = Column(Integer, primary_key=True)
name = Column(Unicode, nullable=False)
published = Column(DateTime, default=datetime.datetime.now)
updated = Column(DateTime, default=datetime.datetime.now)
object_type = Column(Unicode, nullable=False)
def __repr__(self):
return "<{klass} {name}>".format(
klass=self.__class__.__name__,
name=self.name
)
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 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__activity_intermediators"
id = Column(Integer, primary_key=True)
type = Column(Unicode, nullable=False)
TYPES = {
"user": User,
"media": MediaEntry,
"comment": MediaComment,
"collection": 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(self, obj):
""" This sets itself as the activity """
key, model = self._find_model(obj)
if key is None:
raise ValueError("Invalid type of object given")
# We need to save so that self.id is populated
self.type = key
self.save()
# First set self as activity
obj.activity = self.id
obj.save()
def get(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=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.
"""
__tablename__ = "core__activities"
id = Column(Integer, primary_key=True)
actor = Column(Integer,
ForeignKey("core__users.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=True)
title = Column(Unicode, nullable=True)
generator = Column(Integer,
ForeignKey("core__generators.id"),
nullable=True)
object = Column(Integer,
ForeignKey("core__activity_intermediators.id"),
nullable=False)
target = Column(Integer,
ForeignKey("core__activity_intermediators.id"),
nullable=True)
get_actor = relationship(User,
foreign_keys="Activity.actor", post_update=True)
get_generator = relationship(Generator)
def __repr__(self):
if self.content is None:
return "<{klass} verb:{verb}>".format(
klass=self.__class__.__name__,
verb=self.verb
)
else:
return "<{klass} {content}>".format(
klass=self.__class__.__name__,
content=self.content
)
@property
def get_object(self):
if self.object is None:
return None
ai = ActivityIntermediator.query.filter_by(id=self.object).first()
return ai.get()
def set_object(self, obj):
self.object = self._set_model(obj)
@property
def get_target(self):
if self.target is None:
return None
ai = ActivityIntermediator.query.filter_by(id=self.target).first()
return ai.get()
def set_target(self, obj):
self.target = self._set_model(obj)
def _set_model(self, obj):
# Firstly can we set obj
if not hasattr(obj, "activity"):
raise ValueError(
"{0!r} is unable to be set on activity".format(obj))
if obj.activity is None:
# We need to create a new AI
ai = ActivityIntermediator()
ai.set(obj)
ai.save()
return ai.id
# Okay we should have an existing AI
return ActivityIntermediator.query.filter_by(id=obj.activity).first().id
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,
MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
Notification, CommentNotification, ProcessingNotification, Client,
CommentSubscription, ReportBase, CommentReport, MediaReport, UserBan,
Privilege, PrivilegeUserAssociation,
RequestToken, AccessToken, NonceTimestamp]
RequestToken, AccessToken, NonceTimestamp,
Activity, ActivityIntermediator, Generator]
"""
Foundations are the default rows that are created immediately after the tables

View File

@ -51,12 +51,12 @@ add_route(
# object endpoints
add_route(
"mediagoblin.federation.object",
"/api/<string:objectType>/<string:id>",
"/api/<string:object_type>/<string:id>",
"mediagoblin.federation.views:object_endpoint"
)
add_route(
"mediagoblin.federation.object.comments",
"/api/<string:objectType>/<string:id>/comments",
"/api/<string:object_type>/<string:id>/comments",
"mediagoblin.federation.views:object_comments"
)
@ -82,4 +82,10 @@ add_route(
"mediagoblin.webfinger.whoami",
"/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,11 +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.db.models import User, MediaEntry, MediaComment, Activity
from mediagoblin.tools.response import redirect, json_response, json_error, \
render_to_response
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
@ -341,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)
@ -363,7 +350,7 @@ def feed_endpoint(request):
@oauth_required
def object_endpoint(request):
""" Lookup for a object type """
object_type = request.matchdict["objectType"]
object_type = request.matchdict["object_type"]
try:
object_id = int(request.matchdict["id"])
except ValueError:
@ -395,17 +382,17 @@ def object_comments(request):
media = MediaEntry.query.filter_by(id=request.matchdict["id"]).first()
if media is None:
return json_error("Can't find '{0}' with ID '{1}'".format(
request.matchdict["objectType"],
request.matchdict["object_type"],
request.matchdict["id"]
), 404)
comments = response.serialize(request)
comments = media.serialize(request)
comments = comments.get("replies", {
"totalItems": 0,
"items": [],
"url": request.urlgen(
"mediagoblin.federation.object.comments",
objectType=media.objectType,
object_type=media.object_type,
id=media.id,
qualified=True
)
@ -555,3 +542,34 @@ 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,
author=user.id
).first()
if activity is None:
return render_404(request)
return render_to_response(
request,
"mediagoblin/federation/activity.html",
{"activity": activity}
)

View File

@ -26,6 +26,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
@ -202,6 +203,10 @@ def submit_media(mg_app, user, submitted_file, filename,
add_comment_subscription(user, entry)
# Create activity
entry.activity = create_activity("post", entry, entry.uploader).id
entry.save()
return entry
@ -291,4 +296,9 @@ def api_add_to_feed(request, entry):
run_process_media(entry, feed_url)
add_comment_subscription(request.user, entry)
# Create activity
entry.activity = create_activity("post", entry, entry.uploader).id
entry.save()
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.get_object.object_type }}
</a>
{% endblock %}
</div>
{% endblock %}

View File

@ -0,0 +1,63 @@
# 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/>.
from mediagoblin.db.models import Activity, Generator, User
def create_activity(verb, obj, actor, target=None):
"""
This will create an Activity object which for the obj if possible
and save it. The verb should be one of the following:
add, author, create, delete, dislike, favorite, follow
like, post, share, unfollow, unfavorite, unlike, unshare,
update, tag.
If none of those fit you might not want/need to create an activity for
the object. The list is in mediagoblin.db.models.Activity.VALID_VERBS
"""
# exception when we try and generate an activity with an unknow verb
# could change later to allow arbitrary verbs but at the moment we'll play
# it safe.
if verb not in Activity.VALID_VERBS:
raise ValueError("A invalid verb type has been supplied.")
# This should exist as we're creating it by the migration for Generator
generator = Generator.query.filter_by(name="GNU MediaGoblin").first()
if generator is None:
generator = Generator(
name="GNU MediaGoblin",
object_type="service"
)
generator.save()
activity = Activity(verb=verb)
activity.set_object(obj)
if target is not None:
activity.set_target(target)
# If they've set it override the actor from the obj.
activity.actor = actor.id if isinstance(actor, User) else actor
activity.generator = generator.id
activity.save()
# Sigh want to do this prior to save but I can't figure a way to get
# around relationship() not looking up object when model isn't saved.
if activity.generate_content():
activity.save()
return activity

View File

@ -28,6 +28,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)
@ -201,7 +202,7 @@ def media_post_comment(request, media):
_('Your comment has been posted!'))
trigger_notification(comment, media, request)
create_activity("post", comment, comment.author, target=media)
add_comment_subscription(request.user, media)
return redirect_obj(request, media)
@ -263,6 +264,7 @@ def media_collect(request, media):
collection.creator = request.user.id
collection.generate_slug()
collection.save()
create_activity("create", collection, collection.creator)
# Otherwise, use the collection selected from the drop-down
else:
@ -289,7 +291,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, request.user, target=collection)
messages.add_message(request, messages.SUCCESS,
_('"%s" added to collection "%s"')
% (media.title, collection.title))