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:
commit
9a1fc423ac
@ -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()
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
)
|
@ -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}
|
||||
)
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
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.get_object.object_type }}
|
||||
</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
63
mediagoblin/tools/federation.py
Normal file
63
mediagoblin/tools/federation.py
Normal 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
|
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user