Files
ctms/backend/app/schemas/monitoring_visit_issue.py
Cheng Zhou 917ab7ccf1 feat: refine subject visits and project workflows
Add early termination visit workflow with ordering, non-applicable visit handling, visit window display, and medication adherence support.

Extend monitoring visit issue template fields, site scoping, setup draft project info handling, login security UI, attachment behavior, and related tests/migrations.
2026-05-09 17:10:34 +08:00

214 lines
6.2 KiB
Python

import uuid
from datetime import date, datetime, timezone
from pydantic import BaseModel, ConfigDict, field_validator
class MonitoringVisitIssueCreate(BaseModel):
issue_no: str | None = None
site_id: uuid.UUID | None = None
category: str
severity: str | None = None
mark: str | None = None
visit_cycle: str | None = None
subject_code: str | None = None
monitor_item: str | None = None
monitor_type: str | None = None
status: str = "OPEN"
source: str | None = None
creator_name: str | None = None
recommendation: str | None = None
subject_name: str | None = None
description: str | None = None
action_taken: str | None = None
follow_up_progress: str | None = None
center_query: str | None = None
center_latest_reply: str | None = None
rectification_completed: bool = False
found_date: date | None = None
due_at: datetime | None = None
actual_resolve_date: date | None = None
closed_at: datetime | None = None
responsible_name: str | None = None
@field_validator("category", mode="before")
@classmethod
def _strip_required(cls, value: str) -> str:
text = (value or "").strip()
if not text:
raise ValueError("不能为空")
return text
@field_validator("issue_no", mode="before")
@classmethod
def _strip_issue_no(cls, value: str | None) -> str | None:
if value is None:
return None
text = value.strip()
return text or None
@field_validator(
"subject_code",
"severity",
"mark",
"visit_cycle",
"monitor_item",
"monitor_type",
"source",
"creator_name",
"recommendation",
"subject_name",
"description",
"action_taken",
"follow_up_progress",
"center_query",
"center_latest_reply",
"responsible_name",
mode="before",
)
@classmethod
def _strip_optional(cls, value: str | None) -> str | None:
if value is None:
return None
text = value.strip()
return text or None
@field_validator("status", mode="before")
@classmethod
def _normalize_status(cls, value: str | None) -> str:
normalized = (value or "OPEN").strip().upper()
if normalized not in {"OPEN", "CLOSED"}:
raise ValueError("状态必须为 OPEN 或 CLOSED")
return normalized
@field_validator("due_at", "closed_at")
@classmethod
def _normalize_due_at(cls, value: datetime | None) -> datetime | None:
if value is None:
return None
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
class MonitoringVisitIssueRead(BaseModel):
id: uuid.UUID
study_id: uuid.UUID
site_id: uuid.UUID | None
issue_no: str
open_duration: str
category: str
severity: str | None
mark: str | None
visit_cycle: str | None
subject_code: str | None
monitor_item: str | None
monitor_type: str | None
progress: str
status: str
source: str | None
creator_name: str | None
recommendation: str | None
subject_name: str | None
created_at: datetime
description: str | None
action_taken: str | None
follow_up_progress: str | None
center_query: str | None
center_latest_reply: str | None
rectification_completed: bool
found_date: date | None
overdue: bool
due_at: datetime | None
actual_resolve_date: date | None
closed_at: datetime | None
responsible_name: str | None
model_config = ConfigDict(from_attributes=True)
class MonitoringVisitIssueImportSummary(BaseModel):
created_count: int
updated_count: int
skipped_count: int
skipped_rows: list[str]
class MonitoringVisitIssueUpdate(BaseModel):
issue_no: str | None = None
site_id: uuid.UUID | None = None
category: str | None = None
severity: str | None = None
mark: str | None = None
visit_cycle: str | None = None
subject_code: str | None = None
monitor_item: str | None = None
monitor_type: str | None = None
status: str | None = None
source: str | None = None
creator_name: str | None = None
recommendation: str | None = None
subject_name: str | None = None
description: str | None = None
action_taken: str | None = None
follow_up_progress: str | None = None
center_query: str | None = None
center_latest_reply: str | None = None
rectification_completed: bool | None = None
found_date: date | None = None
due_at: datetime | None = None
actual_resolve_date: date | None = None
closed_at: datetime | None = None
responsible_name: str | None = None
@field_validator("issue_no", "category", mode="before")
@classmethod
def _strip_required(cls, value: str | None) -> str | None:
if value is None:
return None
text = value.strip()
if not text:
raise ValueError("不能为空")
return text
@field_validator(
"subject_code",
"severity",
"mark",
"visit_cycle",
"monitor_item",
"monitor_type",
"source",
"creator_name",
"recommendation",
"subject_name",
"description",
"action_taken",
"follow_up_progress",
"center_query",
"center_latest_reply",
"responsible_name",
mode="before",
)
@classmethod
def _strip_optional(cls, value: str | None) -> str | None:
if value is None:
return None
text = value.strip()
return text or None
@field_validator("status", mode="before")
@classmethod
def _normalize_status(cls, value: str | None) -> str | None:
if value is None:
return None
normalized = value.strip().upper()
if normalized not in {"OPEN", "CLOSED"}:
raise ValueError("状态必须为 OPEN 或 CLOSED")
return normalized
@field_validator("due_at", "closed_at")
@classmethod
def _normalize_due_at(cls, value: datetime | None) -> datetime | None:
if value is None:
return None
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)