917ab7ccf1
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.
214 lines
6.2 KiB
Python
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)
|