diff options
Diffstat (limited to 'mediagoblin')
-rw-r--r-- | mediagoblin/db/migrations.py | 195 | ||||
-rw-r--r-- | mediagoblin/db/mixin.py | 122 | ||||
-rw-r--r-- | mediagoblin/db/models.py | 202 | ||||
-rw-r--r-- | mediagoblin/federation/routing.py | 10 | ||||
-rw-r--r-- | mediagoblin/federation/views.py | 62 | ||||
-rw-r--r-- | mediagoblin/submit/lib.py | 10 | ||||
-rw-r--r-- | mediagoblin/templates/mediagoblin/federation/activity.html | 42 | ||||
-rw-r--r-- | mediagoblin/tools/federation.py | 63 | ||||
-rw-r--r-- | mediagoblin/user_pages/views.py | 6 |
9 files changed, 670 insertions, 42 deletions
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index b5b5a026..31b8333e 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -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() diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index 1f2e7ec3..39690cfc 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -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) @@ -131,6 +134,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): """ Rendered version of the description, run through @@ -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"] diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 1b700dce..0069c85a 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -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 diff --git a/mediagoblin/federation/routing.py b/mediagoblin/federation/routing.py index e9fa6252..2f8ed799 100644 --- a/mediagoblin/federation/routing.py +++ b/mediagoblin/federation/routing.py @@ -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" )
\ No newline at end of file diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py index 55d14e30..370ec8c3 100644 --- a/mediagoblin/federation/views.py +++ b/mediagoblin/federation/views.py @@ -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} + ) + + diff --git a/mediagoblin/submit/lib.py b/mediagoblin/submit/lib.py index 637d5038..1813aa82 100644 --- a/mediagoblin/submit/lib.py +++ b/mediagoblin/submit/lib.py @@ -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)) diff --git a/mediagoblin/templates/mediagoblin/federation/activity.html b/mediagoblin/templates/mediagoblin/federation/activity.html new file mode 100644 index 00000000..14377a48 --- /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.get_object.object_type }} + </a> + {% endblock %} + </div> +{% endblock %}
\ No newline at end of file diff --git a/mediagoblin/tools/federation.py b/mediagoblin/tools/federation.py new file mode 100644 index 00000000..890e8801 --- /dev/null +++ b/mediagoblin/tools/federation.py @@ -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 diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py index 1f0b9dcd..b6cbcabd 100644 --- a/mediagoblin/user_pages/views.py +++ b/mediagoblin/user_pages/views.py @@ -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)) |