立项配置页初步优化
This commit is contained in:
+11
-3
@@ -64,12 +64,14 @@ async def create_ae(
|
||||
term=ae_in.term,
|
||||
onset_date=ae_in.onset_date,
|
||||
resolution_date=None,
|
||||
seriousness=str(ae_in.seriousness),
|
||||
severity=str(ae_in.seriousness),
|
||||
seriousness=ae_in.seriousness.value,
|
||||
severity=ae_in.seriousness.value,
|
||||
causality=ae_in.causality,
|
||||
action_taken=None,
|
||||
outcome=ae_in.outcome,
|
||||
reported_to_sponsor=False,
|
||||
is_sae=bool(ae_in.is_sae or ae_in.is_susar),
|
||||
is_susar=bool(ae_in.is_susar),
|
||||
report_due_date=due_date,
|
||||
status="NEW",
|
||||
description=ae_in.description,
|
||||
@@ -125,8 +127,14 @@ async def list_ae(
|
||||
|
||||
async def update_ae(db: AsyncSession, study_id: uuid.UUID, ae: AdverseEvent, ae_in: AEUpdate) -> AdverseEvent:
|
||||
update_data = ae_in.model_dump(exclude_unset=True)
|
||||
if "is_susar" in update_data and update_data["is_susar"]:
|
||||
update_data["is_sae"] = True
|
||||
if "is_sae" in update_data and update_data["is_sae"] is False:
|
||||
update_data["is_susar"] = False
|
||||
if "seriousness" in update_data:
|
||||
update_data["seriousness"] = str(update_data["seriousness"])
|
||||
severity_value = update_data["seriousness"].value if hasattr(update_data["seriousness"], "value") else str(update_data["seriousness"])
|
||||
update_data["seriousness"] = severity_value
|
||||
update_data["severity"] = severity_value
|
||||
if "subject_id" in update_data:
|
||||
await _validate_site_subject(db, study_id, None, update_data["subject_id"])
|
||||
if "onset_date" in update_data or "seriousness" in update_data:
|
||||
|
||||
@@ -17,6 +17,7 @@ async def log_action(
|
||||
detail: str | None,
|
||||
operator_id: uuid.UUID,
|
||||
operator_role: str,
|
||||
auto_commit: bool = True,
|
||||
) -> AuditLog:
|
||||
log = AuditLog(
|
||||
study_id=study_id,
|
||||
@@ -28,8 +29,11 @@ async def log_action(
|
||||
operator_role=operator_role,
|
||||
)
|
||||
db.add(log)
|
||||
await db.commit()
|
||||
await db.refresh(log)
|
||||
if auto_commit:
|
||||
await db.commit()
|
||||
await db.refresh(log)
|
||||
else:
|
||||
await db.flush()
|
||||
return log
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from app.models.site import Site
|
||||
from app.models.special_expense import SpecialExpense
|
||||
from app.models.startup_ethics import StartupEthics
|
||||
from app.models.startup_feasibility import StartupFeasibility
|
||||
from app.models.study_center_confirm import StudyCenterConfirm
|
||||
from app.models.subject import Subject
|
||||
from app.models.subject_history import SubjectHistory
|
||||
from app.models.training_authorization import TrainingAuthorization
|
||||
@@ -44,6 +45,9 @@ async def create_site(db: AsyncSession, study_id: uuid.UUID, site_in: SiteCreate
|
||||
pi_name=site_in.pi_name,
|
||||
contact=site_in.contact,
|
||||
is_active=site_in.is_active,
|
||||
enrollment_plan_start_date=site_in.enrollment_plan_start_date,
|
||||
enrollment_plan_end_date=site_in.enrollment_plan_end_date,
|
||||
enrollment_plan_note=site_in.enrollment_plan_note,
|
||||
)
|
||||
db.add(site)
|
||||
await db.commit()
|
||||
@@ -314,6 +318,7 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None:
|
||||
await db.execute(delete(AdverseEvent).where(AdverseEvent.site_id == site_id))
|
||||
await db.execute(delete(FinanceItem).where(FinanceItem.site_id == site_id))
|
||||
await db.execute(delete(Subject).where(Subject.site_id == site_id))
|
||||
await db.execute(delete(StudyCenterConfirm).where(StudyCenterConfirm.site_id == site_id))
|
||||
|
||||
await db.execute(delete(Milestone).where(Milestone.site_id == site_id))
|
||||
await db.execute(delete(KickoffMeeting).where(KickoffMeeting.site_id == site_id))
|
||||
|
||||
+37
-19
@@ -9,23 +9,29 @@ from app.schemas.study import StudyCreate, StudyUpdate
|
||||
|
||||
|
||||
async def create(db: AsyncSession, study_in: StudyCreate, *, created_by: uuid.UUID | None = None) -> Study:
|
||||
import uuid as uuid_lib # Import uuid to generate unique code if needed
|
||||
|
||||
code = study_in.code
|
||||
if not code:
|
||||
# Generate a unique code if not provided
|
||||
# Use first 8 chars of a new UUID, uppercase
|
||||
code = f"P-{str(uuid_lib.uuid4())[:8].upper()}"
|
||||
|
||||
study = Study(
|
||||
code=code,
|
||||
code=study_in.code.strip(),
|
||||
name=study_in.name,
|
||||
# sponsor is removed from schema, but model has it. Set to None/empty or keep existing value if schema had it (which it doesn't now)
|
||||
# If we removed it from schema, study_in.sponsor will fail if we try to access it via .sponsor attribute directly if it's not in the pydantic model anymore.
|
||||
# Since we removed `sponsor` field from StudyCreate, `study_in` object won't have `sponsor` attribute unless we access dict or use default.
|
||||
# However, the Study MODEL still has sponsor. We should set it to None.
|
||||
sponsor=None,
|
||||
project_full_name=study_in.project_full_name,
|
||||
sponsor=study_in.sponsor,
|
||||
protocol_no=study_in.protocol_no,
|
||||
lead_unit=study_in.lead_unit,
|
||||
principal_investigator=study_in.principal_investigator,
|
||||
main_pm=study_in.main_pm,
|
||||
research_analysis=study_in.research_analysis,
|
||||
research_product=study_in.research_product,
|
||||
control_product=study_in.control_product,
|
||||
indication=study_in.indication,
|
||||
research_population=study_in.research_population,
|
||||
research_design=study_in.research_design,
|
||||
plan_start_date=study_in.plan_start_date,
|
||||
plan_end_date=study_in.plan_end_date,
|
||||
planned_site_count=study_in.planned_site_count,
|
||||
planned_enrollment_count=study_in.planned_enrollment_count,
|
||||
enrollment_monthly_goal_note=study_in.enrollment_monthly_goal_note,
|
||||
enrollment_stage_breakdown=study_in.enrollment_stage_breakdown,
|
||||
summary_note=study_in.summary_note,
|
||||
objective_note=study_in.objective_note,
|
||||
phase=study_in.phase,
|
||||
status=study_in.status,
|
||||
visit_interval_days=study_in.visit_interval_days,
|
||||
@@ -114,6 +120,10 @@ async def delete(db: AsyncSession, study_id: uuid.UUID) -> None:
|
||||
from app.models.faq_category import FaqCategory
|
||||
from app.models.faq_item import FaqItem
|
||||
from app.models.faq_reply import FaqReply
|
||||
from app.models.study_setup_config import StudySetupConfig
|
||||
from app.models.study_setup_config_version import StudySetupConfigVersion
|
||||
from app.models.study_monitoring_strategy import StudyMonitoringStrategy
|
||||
from app.models.study_center_confirm import StudyCenterConfirm
|
||||
|
||||
# 按依赖关系顺序删除关联数据
|
||||
# 1. 删除审计日志
|
||||
@@ -160,13 +170,21 @@ async def delete(db: AsyncSession, study_id: uuid.UUID) -> None:
|
||||
await db.execute(sa_delete(SubjectHistory).where(SubjectHistory.study_id == study_id))
|
||||
await db.execute(sa_delete(Subject).where(Subject.study_id == study_id))
|
||||
|
||||
# 13. 删除站点
|
||||
# 13. 删除立项联动实体(需先于站点删除,避免外键约束)
|
||||
await db.execute(sa_delete(StudyMonitoringStrategy).where(StudyMonitoringStrategy.study_id == study_id))
|
||||
await db.execute(sa_delete(StudyCenterConfirm).where(StudyCenterConfirm.study_id == study_id))
|
||||
|
||||
# 14. 删除站点
|
||||
await db.execute(sa_delete(Site).where(Site.study_id == study_id))
|
||||
|
||||
# 14. 删除成员
|
||||
|
||||
# 15. 删除成员
|
||||
await db.execute(sa_delete(StudyMember).where(StudyMember.study_id == study_id))
|
||||
|
||||
# 15. 最后删除项目本身
|
||||
|
||||
# 16. 删除立项配置
|
||||
await db.execute(sa_delete(StudySetupConfigVersion).where(StudySetupConfigVersion.study_id == study_id))
|
||||
await db.execute(sa_delete(StudySetupConfig).where(StudySetupConfig.study_id == study_id))
|
||||
|
||||
# 17. 最后删除项目本身
|
||||
await db.execute(sa_delete(Study).where(Study.id == study_id))
|
||||
|
||||
await db.commit()
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import delete as sa_delete, select
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.study_setup_config import StudySetupConfig
|
||||
from app.models.study_setup_config_version import StudySetupConfigVersion
|
||||
from app.schemas.study_setup_config import StudySetupConfigData
|
||||
|
||||
|
||||
def _to_storage(data: StudySetupConfigData | dict) -> dict:
|
||||
if isinstance(data, StudySetupConfigData):
|
||||
return data.model_dump(mode="json")
|
||||
return data
|
||||
|
||||
|
||||
async def get_by_study(db: AsyncSession, study_id: uuid.UUID) -> StudySetupConfig | None:
|
||||
result = await db.execute(select(StudySetupConfig).where(StudySetupConfig.study_id == study_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def upsert(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
*,
|
||||
expected_version: int | None,
|
||||
data: StudySetupConfigData | dict,
|
||||
saved_by: uuid.UUID | None,
|
||||
) -> tuple[StudySetupConfig | None, bool]:
|
||||
existing = await get_by_study(db, study_id)
|
||||
payload = _to_storage(data)
|
||||
if existing:
|
||||
if expected_version is not None and expected_version != existing.version:
|
||||
return None, True
|
||||
existing.version = existing.version + 1
|
||||
existing.config = payload
|
||||
existing.saved_by = saved_by
|
||||
existing.publish_status = "PUBLISHED" if existing.published_config == payload else "DRAFT"
|
||||
existing.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await db.refresh(existing)
|
||||
return existing, False
|
||||
|
||||
record = StudySetupConfig(
|
||||
study_id=study_id,
|
||||
version=1,
|
||||
config=payload,
|
||||
publish_status="DRAFT",
|
||||
saved_by=saved_by,
|
||||
)
|
||||
db.add(record)
|
||||
await db.commit()
|
||||
await db.refresh(record)
|
||||
return record, False
|
||||
|
||||
|
||||
async def publish(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
*,
|
||||
expected_version: int | None,
|
||||
published_by: uuid.UUID | None,
|
||||
auto_commit: bool = True,
|
||||
) -> tuple[StudySetupConfig | None, bool]:
|
||||
existing = await get_by_study(db, study_id)
|
||||
if not existing:
|
||||
return None, False
|
||||
if expected_version is not None and expected_version != existing.version:
|
||||
return None, True
|
||||
next_version = existing.version + 1
|
||||
next_display_version_stmt = select(func.max(StudySetupConfigVersion.display_version)).where(
|
||||
StudySetupConfigVersion.study_id == study_id
|
||||
)
|
||||
display_version_result = await db.execute(next_display_version_stmt)
|
||||
current_max_display_version = display_version_result.scalar_one_or_none() or 0
|
||||
next_display_version = current_max_display_version + 1
|
||||
existing.version = next_version
|
||||
existing.publish_status = "PUBLISHED"
|
||||
existing.published_config = existing.config
|
||||
existing.published_by = published_by
|
||||
existing.published_at = datetime.utcnow()
|
||||
existing.saved_by = published_by
|
||||
existing.updated_at = datetime.utcnow()
|
||||
snapshot = StudySetupConfigVersion(
|
||||
study_setup_config_id=existing.id,
|
||||
study_id=study_id,
|
||||
version=next_version,
|
||||
display_version=next_display_version,
|
||||
source_version=next_version - 1,
|
||||
config=existing.config,
|
||||
published_by=published_by,
|
||||
published_at=existing.published_at,
|
||||
)
|
||||
db.add(snapshot)
|
||||
if auto_commit:
|
||||
await db.commit()
|
||||
await db.refresh(existing)
|
||||
else:
|
||||
await db.flush()
|
||||
return existing, False
|
||||
|
||||
|
||||
async def list_versions(db: AsyncSession, study_id: uuid.UUID) -> list[StudySetupConfigVersion]:
|
||||
result = await db.execute(
|
||||
select(StudySetupConfigVersion)
|
||||
.where(StudySetupConfigVersion.study_id == study_id)
|
||||
.order_by(StudySetupConfigVersion.version.desc(), StudySetupConfigVersion.published_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def rollback_to_version(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
*,
|
||||
expected_version: int | None,
|
||||
target_version: int,
|
||||
saved_by: uuid.UUID | None,
|
||||
) -> tuple[StudySetupConfig | None, bool, bool]:
|
||||
existing = await get_by_study(db, study_id)
|
||||
if not existing:
|
||||
return None, False, False
|
||||
if expected_version is not None and expected_version != existing.version:
|
||||
return None, True, False
|
||||
|
||||
result = await db.execute(
|
||||
select(StudySetupConfigVersion).where(
|
||||
StudySetupConfigVersion.study_id == study_id,
|
||||
StudySetupConfigVersion.version == target_version,
|
||||
)
|
||||
)
|
||||
target = result.scalar_one_or_none()
|
||||
if not target:
|
||||
return None, False, True
|
||||
|
||||
existing.version = existing.version + 1
|
||||
existing.config = target.config
|
||||
existing.saved_by = saved_by
|
||||
existing.publish_status = "DRAFT"
|
||||
existing.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await db.refresh(existing)
|
||||
return existing, False, False
|
||||
|
||||
|
||||
async def delete_version(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
*,
|
||||
target_version: int,
|
||||
) -> bool:
|
||||
result = await db.execute(
|
||||
select(StudySetupConfigVersion).where(
|
||||
StudySetupConfigVersion.study_id == study_id,
|
||||
StudySetupConfigVersion.version == target_version,
|
||||
)
|
||||
)
|
||||
target = result.scalar_one_or_none()
|
||||
if not target:
|
||||
return False
|
||||
await db.delete(target)
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
|
||||
async def delete_by_study(db: AsyncSession, study_id: uuid.UUID) -> None:
|
||||
await db.execute(sa_delete(StudySetupConfigVersion).where(StudySetupConfigVersion.study_id == study_id))
|
||||
await db.execute(sa_delete(StudySetupConfig).where(StudySetupConfig.study_id == study_id))
|
||||
await db.commit()
|
||||
@@ -16,6 +16,27 @@ from app.models.subject import Subject
|
||||
from app.schemas.subject import SubjectCreate, SubjectUpdate
|
||||
|
||||
|
||||
def _validate_subject_date_chain(
|
||||
*,
|
||||
screening_date: date | None,
|
||||
consent_date: date | None,
|
||||
enrollment_date: date | None,
|
||||
completion_date: date | None,
|
||||
) -> None:
|
||||
if screening_date and consent_date and consent_date < screening_date:
|
||||
raise ValueError("知情同意日期不能早于筛选日期")
|
||||
if screening_date and enrollment_date and enrollment_date < screening_date:
|
||||
raise ValueError("入组日期不能早于筛选日期")
|
||||
if consent_date and enrollment_date and enrollment_date < consent_date:
|
||||
raise ValueError("入组日期不能早于知情同意日期")
|
||||
if screening_date and completion_date and completion_date < screening_date:
|
||||
raise ValueError("完成日期不能早于筛选日期")
|
||||
if consent_date and completion_date and completion_date < consent_date:
|
||||
raise ValueError("完成日期不能早于知情同意日期")
|
||||
if enrollment_date and completion_date and completion_date < enrollment_date:
|
||||
raise ValueError("完成日期不能早于入组日期")
|
||||
|
||||
|
||||
async def _validate_site(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UUID) -> None:
|
||||
result = await db.execute(select(Site).where(Site.id == site_id))
|
||||
site = result.scalar_one_or_none()
|
||||
@@ -27,6 +48,12 @@ async def _validate_site(db: AsyncSession, study_id: uuid.UUID, site_id: uuid.UU
|
||||
|
||||
async def create_subject(db: AsyncSession, study_id: uuid.UUID, subject_in: SubjectCreate) -> Subject:
|
||||
await _validate_site(db, study_id, subject_in.site_id)
|
||||
_validate_subject_date_chain(
|
||||
screening_date=subject_in.screening_date,
|
||||
consent_date=subject_in.consent_date,
|
||||
enrollment_date=None,
|
||||
completion_date=None,
|
||||
)
|
||||
subject = Subject(
|
||||
study_id=study_id,
|
||||
site_id=subject_in.site_id,
|
||||
@@ -121,6 +148,16 @@ async def generate_default_visits(db: AsyncSession, subject: Subject) -> None:
|
||||
|
||||
async def update_subject(db: AsyncSession, subject: Subject, subject_in: SubjectUpdate) -> Subject:
|
||||
update_data = subject_in.model_dump(exclude_unset=True)
|
||||
next_screening_date = subject.screening_date
|
||||
next_consent_date = update_data.get("consent_date", subject.consent_date)
|
||||
next_enrollment_date = update_data.get("enrollment_date", subject.enrollment_date)
|
||||
next_completion_date = update_data.get("completion_date", subject.completion_date)
|
||||
_validate_subject_date_chain(
|
||||
screening_date=next_screening_date,
|
||||
consent_date=next_consent_date,
|
||||
enrollment_date=next_enrollment_date,
|
||||
completion_date=next_completion_date,
|
||||
)
|
||||
if update_data:
|
||||
await db.execute(
|
||||
sa_update(Subject)
|
||||
|
||||
Reference in New Issue
Block a user