223 lines
7.2 KiB
Python
223 lines
7.2 KiB
Python
"""Collaboration service for VoIdea.
|
|
|
|
Voting, threaded comments, activity logging, and notifications.
|
|
"""
|
|
|
|
from typing import Any, Optional
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import select, func, delete
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.collaboration import IdeaActivity, IdeaComment, IdeaVote, Notification
|
|
from app.models.user import User
|
|
|
|
|
|
class CollaborationService:
|
|
def __init__(self, db: AsyncSession):
|
|
self.db = db
|
|
|
|
# ── Voting ──
|
|
|
|
async def toggle_vote(self, idea_id: UUID, user_id: UUID, vote: str) -> dict[str, Any]:
|
|
"""Toggle vote. Returns current vote state."""
|
|
result = await self.db.execute(
|
|
select(IdeaVote).where(
|
|
IdeaVote.idea_id == idea_id,
|
|
IdeaVote.user_id == user_id,
|
|
)
|
|
)
|
|
existing = result.scalar_one_or_none()
|
|
|
|
if existing:
|
|
if existing.vote == vote:
|
|
await self.db.delete(existing)
|
|
await self.db.commit()
|
|
return {"vote": None}
|
|
existing.vote = vote
|
|
await self.db.commit()
|
|
return {"vote": vote}
|
|
else:
|
|
v = IdeaVote(idea_id=idea_id, user_id=user_id, vote=vote)
|
|
self.db.add(v)
|
|
await self.db.commit()
|
|
return {"vote": vote}
|
|
|
|
async def get_vote_counts(self, idea_id: UUID) -> dict[str, Any]:
|
|
"""Get vote counts and current user's vote."""
|
|
up = await self.db.execute(
|
|
select(func.count(IdeaVote.id)).where(
|
|
IdeaVote.idea_id == idea_id, IdeaVote.vote == "up"
|
|
)
|
|
)
|
|
down = await self.db.execute(
|
|
select(func.count(IdeaVote.id)).where(
|
|
IdeaVote.idea_id == idea_id, IdeaVote.vote == "down"
|
|
)
|
|
)
|
|
return {
|
|
"up": up.scalar() or 0,
|
|
"down": down.scalar() or 0,
|
|
}
|
|
|
|
async def get_user_vote(self, idea_id: UUID, user_id: UUID) -> Optional[str]:
|
|
result = await self.db.execute(
|
|
select(IdeaVote).where(
|
|
IdeaVote.idea_id == idea_id, IdeaVote.user_id == user_id
|
|
)
|
|
)
|
|
v = result.scalar_one_or_none()
|
|
return v.vote if v else None
|
|
|
|
# ── Comments ──
|
|
|
|
async def create_comment(
|
|
self, idea_id: UUID, user_id: UUID, content: str, parent_id: Optional[UUID] = None
|
|
) -> IdeaComment:
|
|
c = IdeaComment(
|
|
idea_id=idea_id, user_id=user_id, content=content, parent_id=parent_id
|
|
)
|
|
self.db.add(c)
|
|
await self.db.commit()
|
|
await self.db.refresh(c)
|
|
return c
|
|
|
|
async def list_comments(self, idea_id: UUID) -> list[dict[str, Any]]:
|
|
result = await self.db.execute(
|
|
select(IdeaComment, User)
|
|
.join(User, IdeaComment.user_id == User.id)
|
|
.where(IdeaComment.idea_id == idea_id)
|
|
.order_by(IdeaComment.created_at.asc())
|
|
)
|
|
rows = result.all()
|
|
comments = []
|
|
for c, u in rows:
|
|
comments.append({
|
|
"id": str(c.id),
|
|
"idea_id": str(c.idea_id),
|
|
"user_id": str(c.user_id),
|
|
"content": c.content,
|
|
"parent_id": str(c.parent_id) if c.parent_id else None,
|
|
"display_name": u.display_name,
|
|
"avatar_url": u.avatar_url,
|
|
"created_at": c.created_at,
|
|
"updated_at": c.updated_at,
|
|
})
|
|
return comments
|
|
|
|
async def delete_comment(self, comment_id: UUID, user_id: UUID) -> bool:
|
|
result = await self.db.execute(
|
|
select(IdeaComment).where(IdeaComment.id == comment_id)
|
|
)
|
|
c = result.scalar_one_or_none()
|
|
if not c or str(c.user_id) != str(user_id):
|
|
return False
|
|
await self.db.delete(c)
|
|
await self.db.commit()
|
|
return True
|
|
|
|
# ── Activity ──
|
|
|
|
async def log_activity(
|
|
self, idea_id: UUID, user_id: Optional[UUID],
|
|
action: str, details: Optional[dict] = None
|
|
) -> IdeaActivity:
|
|
a = IdeaActivity(
|
|
idea_id=idea_id, user_id=user_id, action=action, details=details
|
|
)
|
|
self.db.add(a)
|
|
await self.db.commit()
|
|
await self.db.refresh(a)
|
|
return a
|
|
|
|
async def list_activities(self, idea_id: UUID, limit: int = 50) -> list[dict[str, Any]]:
|
|
result = await self.db.execute(
|
|
select(IdeaActivity, User)
|
|
.join(User, IdeaActivity.user_id == User.id, isouter=True)
|
|
.where(IdeaActivity.idea_id == idea_id)
|
|
.order_by(IdeaActivity.created_at.desc())
|
|
.limit(limit)
|
|
)
|
|
rows = result.all()
|
|
return [
|
|
{
|
|
"id": str(a.id),
|
|
"idea_id": str(a.idea_id),
|
|
"user_id": str(a.user_id) if a.user_id else None,
|
|
"action": a.action,
|
|
"details": a.details,
|
|
"display_name": u.display_name if u else "System",
|
|
"created_at": a.created_at,
|
|
}
|
|
for a, u in rows
|
|
]
|
|
|
|
# ── Notifications ──
|
|
|
|
async def create_notification(
|
|
self, user_id: UUID, type: str, title: str,
|
|
body: str, link: Optional[str] = None
|
|
) -> Notification:
|
|
n = Notification(
|
|
user_id=user_id, type=type, title=title,
|
|
body=body, link=link,
|
|
)
|
|
self.db.add(n)
|
|
await self.db.commit()
|
|
await self.db.refresh(n)
|
|
return n
|
|
|
|
async def list_notifications(
|
|
self, user_id: UUID, unread_only: bool = False, limit: int = 50
|
|
) -> list[Notification]:
|
|
query = (
|
|
select(Notification)
|
|
.where(Notification.user_id == user_id)
|
|
.order_by(Notification.created_at.desc())
|
|
.limit(limit)
|
|
)
|
|
if unread_only:
|
|
query = query.where(Notification.is_read == False)
|
|
result = await self.db.execute(query)
|
|
return list(result.scalars().all())
|
|
|
|
async def mark_notification_read(self, notification_id: UUID, user_id: UUID) -> bool:
|
|
result = await self.db.execute(
|
|
select(Notification).where(
|
|
Notification.id == notification_id,
|
|
Notification.user_id == user_id,
|
|
)
|
|
)
|
|
n = result.scalar_one_or_none()
|
|
if not n:
|
|
return False
|
|
n.is_read = True
|
|
await self.db.commit()
|
|
return True
|
|
|
|
async def mark_all_read(self, user_id: UUID) -> int:
|
|
result = await self.db.execute(
|
|
select(func.count(Notification.id)).where(
|
|
Notification.user_id == user_id,
|
|
Notification.is_read == False,
|
|
)
|
|
)
|
|
count = result.scalar() or 0
|
|
await self.db.execute(
|
|
delete(Notification).where(
|
|
Notification.user_id == user_id,
|
|
Notification.is_read == False,
|
|
)
|
|
)
|
|
await self.db.commit()
|
|
return count
|
|
|
|
async def get_unread_count(self, user_id: UUID) -> int:
|
|
result = await self.db.execute(
|
|
select(func.count(Notification.id)).where(
|
|
Notification.user_id == user_id,
|
|
Notification.is_read == False,
|
|
)
|
|
)
|
|
return result.scalar() or 0
|