feat: deploy infrastructure + disk/drive, calendar, presentation, workspaces, onboarding, demo user

This commit is contained in:
2026-05-19 16:26:26 +03:00
parent 688d043dad
commit 3529c39b22
304 changed files with 18826 additions and 1176 deletions
+222
View File
@@ -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