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
+2
View File
@@ -10,3 +10,5 @@ from app.models.log import LogEntry
from app.models.conductor import ConductorInteraction
from app.models.pipeline import PipelineStats
from app.models.bot_command import BotCommand
from app.models.workspace import Workspace, WorkspaceMembership
from app.models.collaboration import IdeaVote, IdeaComment, IdeaActivity, Notification
+106
View File
@@ -0,0 +1,106 @@
"""Collaboration models for VoIdea.
Voting, threaded comments, activity log, and notifications.
"""
from typing import Optional
from sqlalchemy import Boolean, ForeignKey, String, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base import SQLBase, TimestampMixin, UUIDMixin
class IdeaVote(SQLBase, UUIDMixin, TimestampMixin):
__tablename__ = "idea_votes"
idea_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("ideas.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
user_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
vote: Mapped[str] = mapped_column(
String(4), nullable=False
)
__table_args__ = (
UniqueConstraint("idea_id", "user_id", name="uq_idea_user_vote"),
)
class IdeaComment(SQLBase, UUIDMixin, TimestampMixin):
__tablename__ = "idea_comments"
idea_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("ideas.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
user_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
content: Mapped[str] = mapped_column(Text, nullable=False)
parent_id: Mapped[Optional[UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("idea_comments.id", ondelete="CASCADE"),
nullable=True,
)
class IdeaActivity(SQLBase, UUIDMixin, TimestampMixin):
__tablename__ = "idea_activities"
idea_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("ideas.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
user_id: Mapped[Optional[UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
action: Mapped[str] = mapped_column(
String(50), nullable=False
)
details: Mapped[Optional[dict]] = mapped_column(
JSONB, nullable=True
)
class Notification(SQLBase, UUIDMixin, TimestampMixin):
__tablename__ = "notifications"
user_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
type: Mapped[str] = mapped_column(
String(50), nullable=False, index=True
)
title: Mapped[str] = mapped_column(
String(255), nullable=False
)
body: Mapped[str] = mapped_column(Text, nullable=False)
link: Mapped[Optional[str]] = mapped_column(
String(512), nullable=True
)
is_read: Mapped[bool] = mapped_column(
Boolean, default=False, nullable=False, index=True
)
+5
View File
@@ -36,5 +36,10 @@ class Idea(SQLBase, UUIDMixin, TimestampMixin):
public_slug: Mapped[Optional[str]] = mapped_column(
String(64), unique=True, nullable=True, index=True
)
funnel_status: Mapped[Optional[str]] = mapped_column(
String(20), nullable=True, default=None, index=True
)
FUNNEL_STATES = ["raw", "validated", "backlog", "in_progress", "launched", "retrospective"]
user = relationship("User", back_populates="ideas", lazy="selectin")
+12 -1
View File
@@ -3,7 +3,7 @@
from datetime import datetime
from typing import Any, Optional
from sqlalchemy import Boolean, DateTime, String
from sqlalchemy import Boolean, DateTime, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -60,4 +60,15 @@ class User(SQLBase, UUIDMixin, TimestampMixin):
String(255), nullable=True
)
calendar_provider: Mapped[Optional[str]] = mapped_column(
String(20), nullable=True, default=None
)
calendar_credentials: Mapped[Optional[str]] = mapped_column(
Text, nullable=True, default=None
)
disk_providers: Mapped[Optional[list[dict[str, Any]]]] = mapped_column(
JSONB, nullable=True, default=None
)
ideas = relationship("Idea", back_populates="user", lazy="selectin")
+63
View File
@@ -0,0 +1,63 @@
"""Workspace models for VoIdea.
Personal workspace (1 per user, free) + Team workspaces (tariff-gated).
"""
from typing import Optional
from sqlalchemy import Boolean, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.base import SQLBase, TimestampMixin, UUIDMixin
class Workspace(SQLBase, UUIDMixin, TimestampMixin):
__tablename__ = "workspaces"
name: Mapped[str] = mapped_column(
String(255), nullable=False
)
type: Mapped[str] = mapped_column(
String(20), nullable=False, default="personal", index=True
)
owner_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
description: Mapped[Optional[str]] = mapped_column(
Text, nullable=True, default=""
)
owner = relationship("User", backref="owned_workspaces", lazy="selectin")
members = relationship(
"WorkspaceMembership", back_populates="workspace",
lazy="selectin", cascade="all, delete-orphan",
)
class WorkspaceMembership(SQLBase, UUIDMixin, TimestampMixin):
__tablename__ = "workspace_memberships"
workspace_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
user_id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
role: Mapped[str] = mapped_column(
String(20), nullable=False, default="member"
)
workspace = relationship(
"Workspace", back_populates="members", lazy="selectin"
)
user = relationship("User", backref="workspace_memberships", lazy="selectin")