- 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>
80 lines
1.8 KiB
Python
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
|