未知(继上次中断)
This commit is contained in:
+27
-4
@@ -2,7 +2,7 @@ import uuid
|
||||
from datetime import date, timedelta
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, update as sa_update
|
||||
from sqlalchemy import or_, select, update as sa_update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.ae import AdverseEvent
|
||||
@@ -11,10 +11,20 @@ from app.models.subject import Subject
|
||||
from app.schemas.ae import AECreate, AEUpdate
|
||||
|
||||
|
||||
def _seriousness_rank(value: str | None) -> int | None:
|
||||
if not value:
|
||||
return None
|
||||
normalized = str(value).strip().upper()
|
||||
roman_map = {"I": 1, "II": 2, "III": 3, "IV": 4, "V": 5}
|
||||
legacy_map = {"SERIOUS": 5, "NON_SERIOUS": 1}
|
||||
return roman_map.get(normalized) or legacy_map.get(normalized)
|
||||
|
||||
|
||||
def _calc_due_date(onset: date | None, seriousness: str) -> date | None:
|
||||
if onset is None:
|
||||
return None
|
||||
delta = 1 if seriousness == "SERIOUS" else 7
|
||||
rank = _seriousness_rank(seriousness)
|
||||
delta = 1 if rank and rank >= 3 else 7
|
||||
return onset + timedelta(days=delta)
|
||||
|
||||
|
||||
@@ -54,8 +64,8 @@ async def create_ae(
|
||||
term=ae_in.term,
|
||||
onset_date=ae_in.onset_date,
|
||||
resolution_date=None,
|
||||
seriousness=ae_in.seriousness,
|
||||
severity=ae_in.severity,
|
||||
seriousness=str(ae_in.seriousness),
|
||||
severity=str(ae_in.seriousness),
|
||||
causality=ae_in.causality,
|
||||
action_taken=None,
|
||||
outcome=ae_in.outcome,
|
||||
@@ -84,8 +94,19 @@ async def list_ae(
|
||||
site_id: uuid.UUID | None = None,
|
||||
subject_id: uuid.UUID | None = None,
|
||||
overdue: bool | None = None,
|
||||
site_ids: set[uuid.UUID] | None = None,
|
||||
) -> Sequence[AdverseEvent]:
|
||||
stmt = select(AdverseEvent).where(AdverseEvent.study_id == study_id)
|
||||
if site_ids is not None:
|
||||
if not site_ids:
|
||||
return []
|
||||
subject_site_ids = select(Subject.id).where(Subject.site_id.in_(site_ids))
|
||||
stmt = stmt.where(
|
||||
or_(
|
||||
AdverseEvent.site_id.in_(site_ids),
|
||||
AdverseEvent.subject_id.in_(subject_site_ids),
|
||||
)
|
||||
)
|
||||
if status:
|
||||
stmt = stmt.where(AdverseEvent.status == status)
|
||||
if seriousness:
|
||||
@@ -104,6 +125,8 @@ 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 "seriousness" in update_data:
|
||||
update_data["seriousness"] = str(update_data["seriousness"])
|
||||
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:
|
||||
|
||||
@@ -48,6 +48,7 @@ async def list_contract_fees(
|
||||
db: AsyncSession,
|
||||
project_id: uuid.UUID,
|
||||
center_id: uuid.UUID | None = None,
|
||||
center_ids: set[uuid.UUID] | None = None,
|
||||
q: str | None = None,
|
||||
) -> Sequence[tuple[ContractFee, str, float, float, date | None, date | None]]:
|
||||
paid_total = func.coalesce(
|
||||
@@ -82,6 +83,10 @@ async def list_contract_fees(
|
||||
|
||||
if center_id:
|
||||
stmt = stmt.where(ContractFee.center_id == center_id)
|
||||
if center_ids is not None:
|
||||
if not center_ids:
|
||||
return []
|
||||
stmt = stmt.where(ContractFee.center_id.in_(center_ids))
|
||||
|
||||
if q:
|
||||
conditions = [Site.name.ilike(f"%{q}%")]
|
||||
|
||||
@@ -33,10 +33,16 @@ async def list_documents(
|
||||
doc_type: str | None = None,
|
||||
status: str | None = None,
|
||||
scope_type: str | None = None,
|
||||
cra_site_ids: set[uuid.UUID] | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> Sequence[Document]:
|
||||
stmt = select(Document).where(Document.trial_id == trial_id)
|
||||
if cra_site_ids is not None:
|
||||
if not cra_site_ids:
|
||||
stmt = stmt.where(Document.scope_type == "GLOBAL")
|
||||
else:
|
||||
stmt = stmt.where((Document.scope_type == "GLOBAL") | (Document.site_id.in_(cra_site_ids)))
|
||||
if scope_type:
|
||||
stmt = stmt.where(Document.scope_type == scope_type)
|
||||
if doc_type:
|
||||
|
||||
@@ -44,6 +44,7 @@ async def list_shipments(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
center_id: uuid.UUID | None = None,
|
||||
center_ids: set[uuid.UUID] | None = None,
|
||||
site_name: str | None = None,
|
||||
tracking_no: str | None = None,
|
||||
direction: str | None = None,
|
||||
@@ -52,6 +53,10 @@ async def list_shipments(
|
||||
limit: int = 100,
|
||||
) -> Sequence[DrugShipment]:
|
||||
stmt = select(DrugShipment).where(DrugShipment.study_id == study_id)
|
||||
if center_ids is not None:
|
||||
if not center_ids:
|
||||
return []
|
||||
stmt = stmt.where(DrugShipment.center_id.in_(center_ids))
|
||||
if center_id:
|
||||
stmt = stmt.where(DrugShipment.center_id == center_id)
|
||||
if site_name:
|
||||
|
||||
@@ -39,6 +39,7 @@ async def list_contracts(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
site_name: str | None = None,
|
||||
site_names: set[str] | None = None,
|
||||
contract_no: str | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
@@ -47,6 +48,10 @@ async def list_contracts(
|
||||
select(FinanceContract)
|
||||
.where(FinanceContract.study_id == study_id)
|
||||
)
|
||||
if site_names is not None:
|
||||
if not site_names:
|
||||
return []
|
||||
stmt = stmt.where(FinanceContract.site_name.in_(site_names))
|
||||
if site_name:
|
||||
stmt = stmt.where(FinanceContract.site_name.ilike(f"%{site_name}%"))
|
||||
if contract_no:
|
||||
|
||||
@@ -39,6 +39,7 @@ async def list_specials(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
site_name: str | None = None,
|
||||
site_names: set[str] | None = None,
|
||||
fee_type: str | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
@@ -47,6 +48,10 @@ async def list_specials(
|
||||
select(FinanceSpecial)
|
||||
.where(FinanceSpecial.study_id == study_id)
|
||||
)
|
||||
if site_names is not None:
|
||||
if not site_names:
|
||||
return []
|
||||
stmt = stmt.where(FinanceSpecial.site_name.in_(site_names))
|
||||
if site_name:
|
||||
stmt = stmt.where(FinanceSpecial.site_name.ilike(f"%{site_name}%"))
|
||||
if fee_type:
|
||||
|
||||
+44
-16
@@ -28,9 +28,7 @@ from app.models.startup_feasibility import StartupFeasibility
|
||||
from app.models.subject import Subject
|
||||
from app.models.subject_history import SubjectHistory
|
||||
from app.models.training_authorization import TrainingAuthorization
|
||||
from app.models.version_workflow import VersionWorkflow
|
||||
from app.models.visit import Visit
|
||||
from app.models.workflow_action import WorkflowAction
|
||||
from app.schemas.site import SiteCreate, SiteUpdate
|
||||
|
||||
ATTACHMENT_ROOT = Path(__file__).resolve().parent.parent / "uploads"
|
||||
@@ -77,10 +75,15 @@ async def list_by_study(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
include_inactive: bool = False,
|
||||
site_ids: set[uuid.UUID] | None = None,
|
||||
) -> Sequence[Site]:
|
||||
if site_ids is not None and not site_ids:
|
||||
return []
|
||||
stmt = select(Site).where(Site.study_id == study_id)
|
||||
if not include_inactive:
|
||||
stmt = stmt.where(Site.is_active.is_(True))
|
||||
if site_ids is not None:
|
||||
stmt = stmt.where(Site.id.in_(site_ids))
|
||||
result = await db.execute(stmt.offset(skip).limit(limit))
|
||||
return result.scalars().all()
|
||||
|
||||
@@ -100,6 +103,45 @@ async def list_active_names(db: AsyncSession, study_id: uuid.UUID) -> set[str]:
|
||||
return {row[0] for row in result.all() if row[0]}
|
||||
|
||||
|
||||
def _contact_filter(user_id: str):
|
||||
return or_(
|
||||
Site.contact == user_id,
|
||||
Site.contact.like(f"{user_id},%"),
|
||||
Site.contact.like(f"%,{user_id}"),
|
||||
Site.contact.like(f"%,{user_id},%"),
|
||||
)
|
||||
|
||||
|
||||
async def list_ids_by_contact_user(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
*,
|
||||
include_inactive: bool = False,
|
||||
) -> set[uuid.UUID]:
|
||||
user_id_str = str(user_id)
|
||||
stmt = select(Site.id).where(Site.study_id == study_id, _contact_filter(user_id_str))
|
||||
if not include_inactive:
|
||||
stmt = stmt.where(Site.is_active.is_(True))
|
||||
result = await db.execute(stmt)
|
||||
return {row[0] for row in result.all() if row[0]}
|
||||
|
||||
|
||||
async def list_names_by_contact_user(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
*,
|
||||
include_inactive: bool = False,
|
||||
) -> set[str]:
|
||||
user_id_str = str(user_id)
|
||||
stmt = select(Site.name).where(Site.study_id == study_id, _contact_filter(user_id_str))
|
||||
if not include_inactive:
|
||||
stmt = stmt.where(Site.is_active.is_(True))
|
||||
result = await db.execute(stmt)
|
||||
return {row[0] for row in result.all() if row[0]}
|
||||
|
||||
|
||||
async def delete_site_and_related(db: AsyncSession, site: Site) -> None:
|
||||
site_id = site.id
|
||||
study_id = site.study_id
|
||||
@@ -167,17 +209,6 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None:
|
||||
await db.execute(select(DocumentVersion.id).where(DocumentVersion.document_id.in_(document_ids)))
|
||||
).scalars().all()
|
||||
|
||||
workflow_ids = []
|
||||
workflow_conditions = []
|
||||
if document_ids:
|
||||
workflow_conditions.append(VersionWorkflow.document_id.in_(document_ids))
|
||||
if version_ids:
|
||||
workflow_conditions.append(VersionWorkflow.version_id.in_(version_ids))
|
||||
if workflow_conditions:
|
||||
workflow_ids = (
|
||||
await db.execute(select(VersionWorkflow.id).where(or_(*workflow_conditions)))
|
||||
).scalars().all()
|
||||
|
||||
distribution_conditions = []
|
||||
if document_ids:
|
||||
distribution_conditions.append(Distribution.document_id.in_(document_ids))
|
||||
@@ -261,9 +292,6 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None:
|
||||
)
|
||||
)
|
||||
|
||||
if workflow_ids:
|
||||
await db.execute(delete(WorkflowAction).where(WorkflowAction.workflow_id.in_(workflow_ids)))
|
||||
await db.execute(delete(VersionWorkflow).where(VersionWorkflow.id.in_(workflow_ids)))
|
||||
if distribution_ids:
|
||||
await db.execute(delete(Acknowledgement).where(Acknowledgement.distribution_id.in_(distribution_ids)))
|
||||
await db.execute(delete(Distribution).where(Distribution.id.in_(distribution_ids)))
|
||||
|
||||
@@ -44,6 +44,7 @@ async def list_special_expenses(
|
||||
db: AsyncSession,
|
||||
project_id: uuid.UUID,
|
||||
center_id: uuid.UUID | None = None,
|
||||
center_ids: set[uuid.UUID] | None = None,
|
||||
category: str | None = None,
|
||||
date_from: date | None = None,
|
||||
date_to: date | None = None,
|
||||
@@ -72,6 +73,10 @@ async def list_special_expenses(
|
||||
|
||||
if center_id:
|
||||
stmt = stmt.where(SpecialExpense.center_id == center_id)
|
||||
if center_ids is not None:
|
||||
if not center_ids:
|
||||
return []
|
||||
stmt = stmt.where(SpecialExpense.center_id.in_(center_ids))
|
||||
if category:
|
||||
stmt = stmt.where(SpecialExpense.category == category)
|
||||
if date_from:
|
||||
|
||||
@@ -51,7 +51,10 @@ async def list_feasibilities(
|
||||
study_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
site_ids: set[uuid.UUID] | None = None,
|
||||
) -> Sequence[StartupFeasibility]:
|
||||
if site_ids is not None and not site_ids:
|
||||
return []
|
||||
stmt = (
|
||||
select(StartupFeasibility)
|
||||
.where(StartupFeasibility.study_id == study_id)
|
||||
@@ -59,6 +62,8 @@ async def list_feasibilities(
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
if site_ids is not None:
|
||||
stmt = stmt.where(StartupFeasibility.site_id.in_(site_ids))
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@@ -111,7 +116,10 @@ async def list_ethics(
|
||||
study_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
site_ids: set[uuid.UUID] | None = None,
|
||||
) -> Sequence[StartupEthics]:
|
||||
if site_ids is not None and not site_ids:
|
||||
return []
|
||||
stmt = (
|
||||
select(StartupEthics)
|
||||
.where(StartupEthics.study_id == study_id)
|
||||
@@ -119,6 +127,8 @@ async def list_ethics(
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
if site_ids is not None:
|
||||
stmt = stmt.where(StartupEthics.site_id.in_(site_ids))
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@@ -168,7 +178,10 @@ async def list_kickoffs(
|
||||
study_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
site_ids: set[uuid.UUID] | None = None,
|
||||
) -> Sequence[KickoffMeeting]:
|
||||
if site_ids is not None and not site_ids:
|
||||
return []
|
||||
stmt = (
|
||||
select(KickoffMeeting)
|
||||
.where(KickoffMeeting.study_id == study_id)
|
||||
@@ -176,6 +189,8 @@ async def list_kickoffs(
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
if site_ids is not None:
|
||||
stmt = stmt.where(KickoffMeeting.site_id.in_(site_ids))
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@@ -227,7 +242,10 @@ async def list_training_authorizations(
|
||||
study_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 200,
|
||||
site_names: set[str] | None = None,
|
||||
) -> Sequence[TrainingAuthorization]:
|
||||
if site_names is not None and not site_names:
|
||||
return []
|
||||
stmt = (
|
||||
select(TrainingAuthorization)
|
||||
.where(TrainingAuthorization.study_id == study_id)
|
||||
@@ -235,6 +253,8 @@ async def list_training_authorizations(
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
if site_names is not None:
|
||||
stmt = stmt.where(TrainingAuthorization.site_name.in_(site_names))
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@@ -114,7 +114,6 @@ 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.workflow_template import WorkflowTemplate
|
||||
|
||||
# 按依赖关系顺序删除关联数据
|
||||
# 1. 删除审计日志
|
||||
@@ -128,10 +127,7 @@ async def delete(db: AsyncSession, study_id: uuid.UUID) -> None:
|
||||
# 3. 删除知识库
|
||||
await db.execute(sa_delete(KnowledgeNote).where(KnowledgeNote.study_id == study_id))
|
||||
|
||||
# 4. 删除工作流模板
|
||||
await db.execute(sa_delete(WorkflowTemplate).where(WorkflowTemplate.trial_id == study_id))
|
||||
|
||||
# 5. 删除启动相关
|
||||
# 4. 删除启动相关
|
||||
await db.execute(sa_delete(KickoffMeeting).where(KickoffMeeting.study_id == study_id))
|
||||
await db.execute(sa_delete(StartupEthics).where(StartupEthics.study_id == study_id))
|
||||
await db.execute(sa_delete(StartupFeasibility).where(StartupFeasibility.study_id == study_id))
|
||||
|
||||
@@ -2,11 +2,14 @@ import uuid
|
||||
from datetime import date, timedelta
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, update as sa_update
|
||||
from sqlalchemy import delete as sa_delete, select, update as sa_update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.crud import visit as visit_crud
|
||||
from app.models.ae import AdverseEvent
|
||||
from app.models.finance import FinanceItem
|
||||
from app.models.study import Study
|
||||
from app.models.subject_history import SubjectHistory
|
||||
from app.models.visit import Visit
|
||||
from app.models.site import Site
|
||||
from app.models.subject import Subject
|
||||
@@ -60,8 +63,13 @@ async def list_subjects(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
site_id: uuid.UUID | None = None,
|
||||
site_ids: set[uuid.UUID] | None = None,
|
||||
) -> Sequence[Subject]:
|
||||
stmt = select(Subject).where(Subject.study_id == study_id)
|
||||
if site_ids is not None:
|
||||
if not site_ids:
|
||||
return []
|
||||
stmt = stmt.where(Subject.site_id.in_(site_ids))
|
||||
if site_id:
|
||||
stmt = stmt.where(Subject.site_id == site_id)
|
||||
result = await db.execute(stmt)
|
||||
@@ -125,5 +133,10 @@ async def update_subject(db: AsyncSession, subject: Subject, subject_in: Subject
|
||||
|
||||
|
||||
async def delete_subject(db: AsyncSession, subject: Subject) -> None:
|
||||
subject_id = subject.id
|
||||
await db.execute(sa_delete(Visit).where(Visit.subject_id == subject_id))
|
||||
await db.execute(sa_delete(SubjectHistory).where(SubjectHistory.subject_id == subject_id))
|
||||
await db.execute(sa_delete(AdverseEvent).where(AdverseEvent.subject_id == subject_id))
|
||||
await db.execute(sa_delete(FinanceItem).where(FinanceItem.subject_id == subject_id))
|
||||
await db.delete(subject)
|
||||
await db.commit()
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.version_workflow import VersionWorkflow
|
||||
|
||||
|
||||
async def create(db: AsyncSession, workflow: VersionWorkflow, *, commit: bool = True) -> VersionWorkflow:
|
||||
db.add(workflow)
|
||||
if commit:
|
||||
await db.commit()
|
||||
await db.refresh(workflow)
|
||||
return workflow
|
||||
|
||||
|
||||
async def get(db: AsyncSession, workflow_id: uuid.UUID) -> VersionWorkflow | None:
|
||||
result = await db.execute(select(VersionWorkflow).where(VersionWorkflow.id == workflow_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_by_version(db: AsyncSession, version_id: uuid.UUID) -> VersionWorkflow | None:
|
||||
result = await db.execute(select(VersionWorkflow).where(VersionWorkflow.version_id == version_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_by_document(db: AsyncSession, document_id: uuid.UUID) -> Sequence[VersionWorkflow]:
|
||||
result = await db.execute(select(VersionWorkflow).where(VersionWorkflow.document_id == document_id))
|
||||
return result.scalars().all()
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
from datetime import date, timedelta
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, update as sa_update
|
||||
from sqlalchemy import and_, func, or_, select, update as sa_update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.subject import Subject
|
||||
@@ -43,6 +43,54 @@ async def list_visits(db: AsyncSession, subject_id: uuid.UUID) -> Sequence[Visit
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def mark_overdue_as_lost(db: AsyncSession, subject_id: uuid.UUID) -> None:
|
||||
today = func.current_date()
|
||||
deadline = func.coalesce(Visit.window_end, Visit.planned_date)
|
||||
await db.execute(
|
||||
sa_update(Visit)
|
||||
.where(
|
||||
Visit.subject_id == subject_id,
|
||||
Visit.actual_date.is_(None),
|
||||
deadline.is_not(None),
|
||||
deadline < today,
|
||||
Visit.status.not_in(["DONE", "CANCELLED", "LOST"]),
|
||||
)
|
||||
.values(status="LOST")
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def mark_overdue_as_lost_global(db: AsyncSession) -> None:
|
||||
today = func.current_date()
|
||||
deadline = func.coalesce(Visit.window_end, Visit.planned_date)
|
||||
await db.execute(
|
||||
sa_update(Visit)
|
||||
.where(
|
||||
Visit.actual_date.is_(None),
|
||||
deadline.is_not(None),
|
||||
deadline < today,
|
||||
Visit.status.not_in(["DONE", "CANCELLED", "LOST"]),
|
||||
)
|
||||
.values(status="LOST")
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_next_visit_code(db: AsyncSession, subject_id: uuid.UUID) -> str:
|
||||
result = await db.execute(select(Visit.visit_code).where(Visit.subject_id == subject_id))
|
||||
used: set[int] = set()
|
||||
for (code,) in result.all():
|
||||
if not code or not code.startswith("V"):
|
||||
continue
|
||||
num = code[1:]
|
||||
if num.isdigit():
|
||||
used.add(int(num))
|
||||
next_num = 1
|
||||
while next_num in used:
|
||||
next_num += 1
|
||||
return f"V{next_num}"
|
||||
|
||||
|
||||
async def get_visit(db: AsyncSession, visit_id: uuid.UUID) -> Visit | None:
|
||||
result = await db.execute(select(Visit).where(Visit.id == visit_id))
|
||||
return result.scalar_one_or_none()
|
||||
@@ -72,6 +120,36 @@ async def delete_visit(db: AsyncSession, visit: Visit) -> None:
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def list_lost_visits(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
*,
|
||||
site_ids: set[uuid.UUID] | None = None,
|
||||
limit: int = 50,
|
||||
) -> Sequence[tuple[Visit, str, uuid.UUID | None]]:
|
||||
if site_ids is not None and not site_ids:
|
||||
return []
|
||||
today = func.current_date()
|
||||
deadline = func.coalesce(Visit.window_end, Visit.planned_date)
|
||||
lost_condition = and_(
|
||||
Visit.actual_date.is_(None),
|
||||
deadline.is_not(None),
|
||||
deadline < today,
|
||||
Visit.status.not_in(["DONE", "CANCELLED"]),
|
||||
)
|
||||
stmt = (
|
||||
select(Visit, Subject.subject_no, Subject.site_id)
|
||||
.join(Subject, Subject.id == Visit.subject_id)
|
||||
.where(Visit.study_id == study_id, lost_condition)
|
||||
.order_by(Visit.updated_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
if site_ids is not None:
|
||||
stmt = stmt.where(Subject.site_id.in_(site_ids))
|
||||
result = await db.execute(stmt)
|
||||
return result.all()
|
||||
|
||||
|
||||
async def create_followup_visits(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.workflow_action import WorkflowAction
|
||||
|
||||
|
||||
async def create(db: AsyncSession, action: WorkflowAction, *, commit: bool = True) -> WorkflowAction:
|
||||
db.add(action)
|
||||
if commit:
|
||||
await db.commit()
|
||||
await db.refresh(action)
|
||||
return action
|
||||
@@ -1,26 +0,0 @@
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.workflow_template import WorkflowTemplate
|
||||
|
||||
|
||||
async def get(db: AsyncSession, template_id: uuid.UUID) -> WorkflowTemplate | None:
|
||||
result = await db.execute(select(WorkflowTemplate).where(WorkflowTemplate.id == template_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_active(db: AsyncSession, trial_id: uuid.UUID | None = None) -> Sequence[WorkflowTemplate]:
|
||||
stmt = (
|
||||
select(WorkflowTemplate)
|
||||
.where(WorkflowTemplate.is_active.is_(True))
|
||||
.options(selectinload(WorkflowTemplate.nodes))
|
||||
)
|
||||
if trial_id:
|
||||
stmt = stmt.where(WorkflowTemplate.trial_id == trial_id)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
Reference in New Issue
Block a user