video-accessibility/backend/app/models/client.py
Vadym Samoilenko a168af1aa7 feat: two-stage QC (linguist→reviewer), project picker, comments, email notifications, deadlines
- Two-stage QC workflow: linguist edits + submits → reviewer approves/rejects per language.
  New statuses: in_progress, pending_review, in_review. New service functions: submit_for_review,
  open_review, assign_reviewer, reassign_reviewer, add_comment. Linguist and reviewer deadlines.
- Reject now resets language to in_progress so linguist can iterate without full re-assignment.
- QC comment threads per language (append-only), visible to all assignees.
- Email notifications via Mailgun on: assignment, submit-for-review, comment, approve, reject.
  Best-effort (failures do not roll back QC actions). asyncio.gather for parallel fan-out.
- New audit actions: LANGUAGE_QC_REVIEWER_ASSIGN/REASSIGN, LANGUAGE_QC_SUBMIT,
  LANGUAGE_QC_OPEN_REVIEW, LANGUAGE_QC_COMMENT.
- Inline project picker in NewJob: "+ Create new project…" option with name, default
  languages, default linguist, default reviewer. Pre-fills languages on the new job.
- Project model extended with default_languages, default_linguist_id, default_reviewer_id.
- RBAC: CLIENT org-members can now create projects (backend guard relaxed).
- LinguistQueue: role toggle "As linguist / As reviewer" + new status tabs.
- QCDetail: two-slot assignment cards (linguist + reviewer), deadline display, role-aware
  action buttons, comments panel with optimistic insert and 15s refetch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:59:40 +01:00

80 lines
1.8 KiB
Python

from datetime import datetime
from typing import Optional, Annotated
from bson import ObjectId
from pydantic import BaseModel, BeforeValidator
def validate_object_id(v) -> str:
if isinstance(v, ObjectId):
return str(v)
if isinstance(v, str):
return v
raise ValueError("Invalid ObjectId")
PyObjectId = Annotated[str, BeforeValidator(validate_object_id)]
class Client(BaseModel):
id: Optional[str] = None
name: str
slug: str
is_active: bool = True
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class ClientCreate(BaseModel):
name: str
slug: str
class ClientUpdate(BaseModel):
name: Optional[str] = None
slug: Optional[str] = None
is_active: Optional[bool] = None
class Team(BaseModel):
id: Optional[str] = None
name: str
client_id: str
member_user_ids: list[str] = []
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class TeamCreate(BaseModel):
name: str
class TeamUpdate(BaseModel):
name: Optional[str] = None
class Project(BaseModel):
id: Optional[str] = None
name: str
client_id: str
is_active: bool = True
default_languages: list[str] = []
default_linguist_id: Optional[str] = None
default_reviewer_id: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class ProjectCreate(BaseModel):
name: str
default_languages: list[str] = []
default_linguist_id: Optional[str] = None
default_reviewer_id: Optional[str] = None
class ProjectUpdate(BaseModel):
name: Optional[str] = None
is_active: Optional[bool] = None
default_languages: Optional[list[str]] = None
default_linguist_id: Optional[str] = None
default_reviewer_id: Optional[str] = None