396 lines
14 KiB
Python
396 lines
14 KiB
Python
import uuid
|
|
from pathlib import Path
|
|
from typing import Iterable, Sequence
|
|
|
|
from sqlalchemy import delete, or_, select, update
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.acknowledgement import Acknowledgement
|
|
from app.models.ae import AdverseEvent
|
|
from app.models.attachment import Attachment
|
|
from app.models.contract_fee import ContractFee
|
|
from app.models.contract_fee_payment import ContractFeePayment
|
|
from app.models.document import Document
|
|
from app.models.document_version import DocumentVersion
|
|
from app.models.distribution import Distribution
|
|
from app.models.drug_shipment import DrugShipment
|
|
from app.models.fee_attachment import FeeAttachment
|
|
from app.models.finance import FinanceItem
|
|
from app.models.finance_contract import FinanceContract
|
|
from app.models.finance_special import FinanceSpecial
|
|
from app.models.kickoff_meeting import KickoffMeeting
|
|
from app.models.knowledge_note import KnowledgeNote
|
|
from app.models.milestone import Milestone
|
|
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
|
|
from app.models.visit import Visit
|
|
from app.schemas.site import SiteCreate, SiteUpdate
|
|
|
|
ATTACHMENT_ROOT = Path(__file__).resolve().parent.parent / "uploads"
|
|
FEE_ATTACHMENT_ROOT = ATTACHMENT_ROOT / "fees"
|
|
DOCUMENT_ATTACHMENT_ROOT = ATTACHMENT_ROOT / "documents"
|
|
|
|
|
|
async def create_site(db: AsyncSession, study_id: uuid.UUID, site_in: SiteCreate) -> Site:
|
|
site = Site(
|
|
study_id=study_id,
|
|
name=site_in.name,
|
|
city=site_in.city,
|
|
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()
|
|
await db.refresh(site)
|
|
return site
|
|
|
|
|
|
async def get_site(db: AsyncSession, site_id: uuid.UUID) -> Site | None:
|
|
result = await db.execute(select(Site).where(Site.id == site_id))
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
async def update_site(db: AsyncSession, site: Site, site_in: SiteUpdate) -> Site:
|
|
update_data = site_in.model_dump(exclude_unset=True)
|
|
if update_data:
|
|
await db.execute(
|
|
update(Site)
|
|
.where(Site.id == site.id)
|
|
.values(**update_data)
|
|
)
|
|
await db.commit()
|
|
await db.refresh(site)
|
|
return site
|
|
|
|
|
|
async def list_by_study(
|
|
db: AsyncSession,
|
|
study_id: uuid.UUID,
|
|
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()
|
|
|
|
|
|
async def get_sites_by_ids(db: AsyncSession, ids: set[uuid.UUID]) -> dict[uuid.UUID, Site]:
|
|
if not ids:
|
|
return {}
|
|
result = await db.execute(select(Site).where(Site.id.in_(ids)))
|
|
sites = result.scalars().all()
|
|
return {s.id: s for s in sites}
|
|
|
|
|
|
async def list_active_names(db: AsyncSession, study_id: uuid.UUID) -> set[str]:
|
|
result = await db.execute(
|
|
select(Site.name).where(Site.study_id == study_id, Site.is_active.is_(True))
|
|
)
|
|
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
|
|
site_name = site.name
|
|
|
|
subject_ids = (
|
|
await db.execute(select(Subject.id).where(Subject.site_id == site_id))
|
|
).scalars().all()
|
|
contract_fee_ids = (
|
|
await db.execute(select(ContractFee.id).where(ContractFee.center_id == site_id))
|
|
).scalars().all()
|
|
special_expense_ids = (
|
|
await db.execute(select(SpecialExpense.id).where(SpecialExpense.center_id == site_id))
|
|
).scalars().all()
|
|
drug_shipment_ids = (
|
|
await db.execute(select(DrugShipment.id).where(DrugShipment.center_id == site_id))
|
|
).scalars().all()
|
|
kickoff_ids = (
|
|
await db.execute(select(KickoffMeeting.id).where(KickoffMeeting.site_id == site_id))
|
|
).scalars().all()
|
|
feasibility_ids = (
|
|
await db.execute(select(StartupFeasibility.id).where(StartupFeasibility.site_id == site_id))
|
|
).scalars().all()
|
|
ethics_ids = (
|
|
await db.execute(select(StartupEthics.id).where(StartupEthics.site_id == site_id))
|
|
).scalars().all()
|
|
training_ids = (
|
|
await db.execute(
|
|
select(TrainingAuthorization.id).where(
|
|
TrainingAuthorization.study_id == study_id,
|
|
TrainingAuthorization.site_name == site_name,
|
|
)
|
|
)
|
|
).scalars().all()
|
|
finance_contract_ids = (
|
|
await db.execute(
|
|
select(FinanceContract.id).where(
|
|
FinanceContract.study_id == study_id,
|
|
FinanceContract.site_name == site_name,
|
|
)
|
|
)
|
|
).scalars().all()
|
|
finance_special_ids = (
|
|
await db.execute(
|
|
select(FinanceSpecial.id).where(
|
|
FinanceSpecial.study_id == study_id,
|
|
FinanceSpecial.site_name == site_name,
|
|
)
|
|
)
|
|
).scalars().all()
|
|
knowledge_note_ids = (
|
|
await db.execute(
|
|
select(KnowledgeNote.id).where(
|
|
KnowledgeNote.study_id == study_id,
|
|
KnowledgeNote.site_name == site_name,
|
|
)
|
|
)
|
|
).scalars().all()
|
|
document_ids = (
|
|
await db.execute(select(Document.id).where(Document.site_id == site_id))
|
|
).scalars().all()
|
|
version_ids = []
|
|
if document_ids:
|
|
version_ids = (
|
|
await db.execute(select(DocumentVersion.id).where(DocumentVersion.document_id.in_(document_ids)))
|
|
).scalars().all()
|
|
|
|
distribution_conditions = []
|
|
if document_ids:
|
|
distribution_conditions.append(Distribution.document_id.in_(document_ids))
|
|
if version_ids:
|
|
distribution_conditions.append(Distribution.version_id.in_(version_ids))
|
|
distribution_conditions.append((Distribution.target_type == "SITE") & (Distribution.target_id == str(site_id)))
|
|
distribution_ids = (
|
|
await db.execute(select(Distribution.id).where(or_(*distribution_conditions)))
|
|
).scalars().all()
|
|
|
|
attachment_groups: dict[str, list[uuid.UUID]] = {
|
|
"startup_feasibility": feasibility_ids,
|
|
"startup_ethics": ethics_ids,
|
|
"startup_kickoff": kickoff_ids,
|
|
"startup_kickoff_minutes": kickoff_ids,
|
|
"startup_kickoff_signin": kickoff_ids,
|
|
"startup_kickoff_ppt": kickoff_ids,
|
|
"training_authorization": training_ids,
|
|
"finance_contract": finance_contract_ids,
|
|
"finance_special": finance_special_ids,
|
|
"knowledge_note": knowledge_note_ids,
|
|
"drug_shipment": drug_shipment_ids,
|
|
}
|
|
attachment_paths: list[str] = []
|
|
for entity_type, ids in attachment_groups.items():
|
|
if not ids:
|
|
continue
|
|
attachment_paths.extend(
|
|
(
|
|
await db.execute(
|
|
select(Attachment.file_path).where(
|
|
Attachment.study_id == study_id,
|
|
Attachment.entity_type == entity_type,
|
|
Attachment.entity_id.in_(ids),
|
|
)
|
|
)
|
|
).scalars().all()
|
|
)
|
|
await db.execute(
|
|
delete(Attachment).where(
|
|
Attachment.study_id == study_id,
|
|
Attachment.entity_type == entity_type,
|
|
Attachment.entity_id.in_(ids),
|
|
)
|
|
)
|
|
|
|
fee_attachment_paths: list[str] = []
|
|
if contract_fee_ids:
|
|
fee_attachment_paths.extend(
|
|
(
|
|
await db.execute(
|
|
select(FeeAttachment.storage_key).where(
|
|
FeeAttachment.entity_type == "contract_fee",
|
|
FeeAttachment.entity_id.in_(contract_fee_ids),
|
|
)
|
|
)
|
|
).scalars().all()
|
|
)
|
|
await db.execute(
|
|
delete(FeeAttachment).where(
|
|
FeeAttachment.entity_type == "contract_fee",
|
|
FeeAttachment.entity_id.in_(contract_fee_ids),
|
|
)
|
|
)
|
|
await db.execute(delete(ContractFeePayment).where(ContractFeePayment.contract_fee_id.in_(contract_fee_ids)))
|
|
if special_expense_ids:
|
|
fee_attachment_paths.extend(
|
|
(
|
|
await db.execute(
|
|
select(FeeAttachment.storage_key).where(
|
|
FeeAttachment.entity_type == "special_expense",
|
|
FeeAttachment.entity_id.in_(special_expense_ids),
|
|
)
|
|
)
|
|
).scalars().all()
|
|
)
|
|
await db.execute(
|
|
delete(FeeAttachment).where(
|
|
FeeAttachment.entity_type == "special_expense",
|
|
FeeAttachment.entity_id.in_(special_expense_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)))
|
|
await db.execute(delete(Acknowledgement).where(Acknowledgement.site_id == site_id))
|
|
version_file_paths: list[str] = []
|
|
if version_ids:
|
|
version_file_paths = (
|
|
await db.execute(select(DocumentVersion.file_uri).where(DocumentVersion.id.in_(version_ids)))
|
|
).scalars().all()
|
|
await db.execute(delete(DocumentVersion).where(DocumentVersion.id.in_(version_ids)))
|
|
if document_ids:
|
|
await db.execute(delete(Document).where(Document.id.in_(document_ids)))
|
|
|
|
if subject_ids:
|
|
await db.execute(delete(SubjectHistory).where(SubjectHistory.subject_id.in_(subject_ids)))
|
|
await db.execute(delete(Visit).where(Visit.subject_id.in_(subject_ids)))
|
|
await db.execute(delete(AdverseEvent).where(AdverseEvent.subject_id.in_(subject_ids)))
|
|
await db.execute(delete(FinanceItem).where(FinanceItem.subject_id.in_(subject_ids)))
|
|
|
|
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))
|
|
await db.execute(delete(StartupFeasibility).where(StartupFeasibility.site_id == site_id))
|
|
await db.execute(delete(StartupEthics).where(StartupEthics.site_id == site_id))
|
|
|
|
await db.execute(delete(ContractFee).where(ContractFee.center_id == site_id))
|
|
await db.execute(delete(SpecialExpense).where(SpecialExpense.center_id == site_id))
|
|
await db.execute(delete(DrugShipment).where(DrugShipment.center_id == site_id))
|
|
|
|
await db.execute(
|
|
delete(TrainingAuthorization).where(
|
|
TrainingAuthorization.study_id == study_id,
|
|
TrainingAuthorization.site_name == site_name,
|
|
)
|
|
)
|
|
await db.execute(
|
|
delete(FinanceContract).where(
|
|
FinanceContract.study_id == study_id,
|
|
FinanceContract.site_name == site_name,
|
|
)
|
|
)
|
|
await db.execute(
|
|
delete(FinanceSpecial).where(
|
|
FinanceSpecial.study_id == study_id,
|
|
FinanceSpecial.site_name == site_name,
|
|
)
|
|
)
|
|
await db.execute(
|
|
delete(KnowledgeNote).where(
|
|
KnowledgeNote.study_id == study_id,
|
|
KnowledgeNote.site_name == site_name,
|
|
)
|
|
)
|
|
|
|
await db.execute(delete(Site).where(Site.id == site_id))
|
|
await _remove_files(attachment_paths, ATTACHMENT_ROOT)
|
|
await _remove_files(fee_attachment_paths, FEE_ATTACHMENT_ROOT)
|
|
await _remove_files(version_file_paths, DOCUMENT_ATTACHMENT_ROOT)
|
|
await db.commit()
|
|
|
|
|
|
async def _remove_files(paths: Iterable[str | None], root: Path) -> None:
|
|
for raw in paths:
|
|
if not raw:
|
|
continue
|
|
file_path = Path(str(raw))
|
|
try:
|
|
if file_path.exists():
|
|
file_path.unlink()
|
|
except OSError:
|
|
pass
|
|
_prune_empty_parents(file_path.parent, root)
|
|
|
|
|
|
def _prune_empty_parents(start: Path, root: Path) -> None:
|
|
try:
|
|
root_resolved = root.resolve()
|
|
except OSError:
|
|
return
|
|
try:
|
|
current = start.resolve()
|
|
except OSError:
|
|
return
|
|
if root_resolved not in current.parents and current != root_resolved:
|
|
return
|
|
while True:
|
|
if current == root_resolved:
|
|
break
|
|
try:
|
|
current.rmdir()
|
|
except OSError:
|
|
break
|
|
current = current.parent
|