feat: deploy infrastructure + disk/drive, calendar, presentation, workspaces, onboarding, demo user
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user