Skip to content

Commit

Permalink
Merge pull request #349 from ynput/348-activity-feed-reactions
Browse files Browse the repository at this point in the history
Activity feed:  Reactions
  • Loading branch information
martastain authored Sep 13, 2024
2 parents db2dd1e + dea406d commit 75f6e0f
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 4 deletions.
4 changes: 2 additions & 2 deletions api/activities/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__all__ = ["activity", "suggest", "router", "watchers"]
__all__ = ["activity", "suggest", "reactions", "router", "watchers"]

from . import activity, suggest, watchers
from . import activity, reactions, suggest, watchers
from .router import router
5 changes: 3 additions & 2 deletions api/activities/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from ayon_server.activities.watchers.set_watchers import ensure_watching
from ayon_server.api.dependencies import (
ActivityID,
CurrentUser,
PathEntityID,
PathProjectLevelEntityType,
Expand Down Expand Up @@ -91,7 +92,7 @@ async def post_project_activity(
@router.delete("/activities/{activity_id}")
async def delete_project_activity(
project_name: ProjectName,
activity_id: str,
activity_id: ActivityID,
user: CurrentUser,
background_tasks: BackgroundTasks,
x_sender: str | None = Header(default=None),
Expand Down Expand Up @@ -127,7 +128,7 @@ class ActivityPatchModel(OPModel):
@router.patch("/activities/{activity_id}")
async def patch_project_activity(
project_name: ProjectName,
activity_id: str,
activity_id: ActivityID,
user: CurrentUser,
activity: ActivityPatchModel,
background_tasks: BackgroundTasks,
Expand Down
159 changes: 159 additions & 0 deletions api/activities/reactions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
from datetime import datetime
from typing import Literal

from fastapi import Header, Path

from ayon_server.api.dependencies import (
ActivityID,
CurrentUser,
ProjectName,
)
from ayon_server.entities import UserEntity
from ayon_server.events.eventstream import EventStream
from ayon_server.exceptions import BadRequestException, NotFoundException
from ayon_server.lib.postgres import Postgres
from ayon_server.types import NAME_REGEX, Field, OPModel

from .router import router


async def modify_reactions(
project_name: str,
activity_id: str,
user: UserEntity,
reaction: str,
action: Literal["add", "remove"],
sender: str | None = None,
):
"""
Reactions are stored in the activity data as a list of dicts
with the following structure:
{
"reaction": "like",
"userName": "user1",
"fullName": "User One",
"timestamp": "2024-10-01T12:00:00Z"
}
"""
async with Postgres.acquire() as conn, conn.transaction():
res = await conn.fetch(
f"""
SELECT activity_type, data
FROM project_{project_name}.activities
WHERE id = $1
""",
activity_id,
)

if not res:
raise NotFoundException("Activity not found")
activity_type = res[0]["activity_type"]

# modify the reactions

activity_data = res[0]["data"]
reactions = activity_data.get("reactions", [])

if action == "add":
# check if the user has already reacted
for r in reactions:
if r["reaction"] == reaction and r["userName"] == user.name:
raise BadRequestException("Already reacted")

reactions.append(
{
"reaction": reaction,
"userName": user.name,
"fullName": user.attrib.fullName or None,
"timestamp": datetime.now().isoformat(),
}
)
elif action == "remove":
reactions = [
r
for r in reactions
if r["reaction"] != reaction or r["userName"] != user.name
]

activity_data["reactions"] = reactions

await conn.execute(
f"""
UPDATE project_{project_name}.activities
SET data = $1
WHERE id = $2
""",
activity_data,
activity_id,
)

# load activity references (used to generate the event summary)

references = await conn.fetch(
f"""
SELECT entity_id, entity_type, reference_type
FROM project_{project_name}.activity_references
WHERE activity_id = $1
""",
activity_id,
)

summary = {
"activity_id": activity_id,
"activity_type": activity_type,
"references": [dict(r) for r in references],
}

await EventStream.dispatch(
"activity.updated",
project=project_name,
description="",
summary=summary,
store=False,
user=user.name,
sender=sender,
)


#
# REST API
#


class CreateReactionModel(OPModel):
reaction: str = Field(..., description="The reaction to be created", example="like")


@router.post("/activities/{activity_id}/reactions", status_code=201)
async def create_reaction_to_activity(
user: CurrentUser,
project_name: ProjectName,
activity_id: ActivityID,
request: CreateReactionModel,
x_sender: str | None = Header(None, description="The sender of the request"),
):
await modify_reactions(
project_name, activity_id, user, request.reaction, "add", x_sender
)


@router.delete("/activities/{activity_id}/reactions/{reaction}", status_code=204)
async def delete_reaction_to_activity(
user: CurrentUser,
project_name: ProjectName,
activity_id: ActivityID,
reaction: str = Path(
...,
description="The reaction to be deleted",
example="like",
regex=NAME_REGEX,
),
x_sender: str | None = Header(None, description="The sender of the request"),
):
await modify_reactions(
project_name, activity_id, user, reaction, "remove", x_sender
)
22 changes: 22 additions & 0 deletions ayon_server/graphql/nodes/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ class ActivityFileNode:
updated_at: datetime = strawberry.field()


@strawberry.type
class ActivityReactionNode:
user_name: str = strawberry.field()
full_name: str | None = strawberry.field()
reaction: str = strawberry.field()
timestamp: datetime = strawberry.field()


@strawberry.type
class ActivityNode:
project_name: str = strawberry.field()
Expand All @@ -69,6 +77,7 @@ class ActivityNode:

origin: ActivityOriginNode | None = strawberry.field()
parents: list[ActivityOriginNode] = strawberry.field()
reactions: list[ActivityReactionNode] = strawberry.field()

@strawberry.field
async def author(self, info: Info) -> Optional[UserNode]:
Expand Down Expand Up @@ -192,13 +201,26 @@ def activity_from_record(
else:
parents = []

reactions: list[ActivityReactionNode] = []
if reactions_data := activity_data.get("reactions"):
for reaction in reactions_data:
reactions.append(
ActivityReactionNode(
user_name=reaction["userName"],
full_name=reaction["fullName"],
reaction=reaction["reaction"],
timestamp=datetime.fromisoformat(reaction["timestamp"]),
)
)

node = ActivityNode(
project_name=project_name,
activity_data=json_dumps(activity_data),
reference_data=json_dumps(reference_data),
origin=origin,
parents=parents,
read=reference_data.pop("read", False),
reactions=reactions,
**record,
)
# probably won't be used
Expand Down

0 comments on commit 75f6e0f

Please sign in to comment.