新增电子试验主文件目录与文件归档
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.schemas.document import DocumentCreate, DocumentSummary
|
||||
from app.schemas.etmf import EtmfNodeCreate, EtmfNodeRead, EtmfNodeUpdate, EtmfTreeNode
|
||||
from app.services import etmf_service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/tree", response_model=list[EtmfTreeNode])
|
||||
async def get_etmf_tree(
|
||||
study_id: uuid.UUID = Query(...),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[EtmfTreeNode]:
|
||||
return await etmf_service.list_etmf_tree(db, study_id=study_id, current_user=current_user)
|
||||
|
||||
|
||||
@router.post("/nodes", response_model=EtmfNodeRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_etmf_node(
|
||||
payload: EtmfNodeCreate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> EtmfNodeRead:
|
||||
node = await etmf_service.create_node(db, payload, current_user)
|
||||
return EtmfNodeRead.model_validate(node)
|
||||
|
||||
|
||||
@router.patch("/nodes/{node_id}", response_model=EtmfNodeRead)
|
||||
async def update_etmf_node(
|
||||
node_id: uuid.UUID,
|
||||
payload: EtmfNodeUpdate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> EtmfNodeRead:
|
||||
node = await etmf_service.update_node(db, node_id, payload, current_user)
|
||||
return EtmfNodeRead.model_validate(node)
|
||||
|
||||
|
||||
@router.get("/nodes/{node_id}/documents", response_model=list[DocumentSummary])
|
||||
async def list_etmf_node_documents(
|
||||
node_id: uuid.UUID,
|
||||
site_id: uuid.UUID | None = Query(None),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[DocumentSummary]:
|
||||
return await etmf_service.list_node_documents(db, node_id=node_id, site_id=site_id, current_user=current_user)
|
||||
|
||||
|
||||
@router.post("/nodes/{node_id}/documents", response_model=DocumentSummary, status_code=status.HTTP_201_CREATED)
|
||||
async def create_etmf_node_document(
|
||||
node_id: uuid.UUID,
|
||||
payload: DocumentCreate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> DocumentSummary:
|
||||
return await etmf_service.create_node_document(db, node_id=node_id, payload=payload, current_user=current_user)
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
@@ -33,6 +35,7 @@ async def list_documents(
|
||||
doc_type: str | None = None,
|
||||
status: str | None = None,
|
||||
scope_type: str | None = None,
|
||||
etmf_node_id: uuid.UUID | None = None,
|
||||
cra_site_ids: set[uuid.UUID] | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
@@ -51,6 +54,8 @@ async def list_documents(
|
||||
stmt = stmt.where(Document.status == status)
|
||||
if site_id:
|
||||
stmt = stmt.where(Document.site_id == site_id)
|
||||
if etmf_node_id:
|
||||
stmt = stmt.where(Document.etmf_node_id == etmf_node_id)
|
||||
stmt = stmt.order_by(Document.updated_at.desc()).offset(skip).limit(limit)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, update as sa_update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.etmf import EtmfNode
|
||||
|
||||
|
||||
async def get_node(db: AsyncSession, node_id: uuid.UUID) -> EtmfNode | None:
|
||||
result = await db.execute(select(EtmfNode).where(EtmfNode.id == node_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_nodes_by_study(db: AsyncSession, study_id: uuid.UUID, *, include_inactive: bool = True) -> Sequence[EtmfNode]:
|
||||
stmt = select(EtmfNode).where(EtmfNode.study_id == study_id)
|
||||
if not include_inactive:
|
||||
stmt = stmt.where(EtmfNode.is_active.is_(True))
|
||||
stmt = stmt.order_by(EtmfNode.sort_order.asc(), EtmfNode.code.asc(), EtmfNode.name.asc())
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def create_node(db: AsyncSession, node: EtmfNode, *, commit: bool = True) -> EtmfNode:
|
||||
db.add(node)
|
||||
if commit:
|
||||
await db.commit()
|
||||
await db.refresh(node)
|
||||
return node
|
||||
|
||||
|
||||
async def update_node(db: AsyncSession, node_id: uuid.UUID, values: dict, *, commit: bool = True) -> None:
|
||||
if values:
|
||||
await db.execute(sa_update(EtmfNode).where(EtmfNode.id == node_id).values(**values))
|
||||
if commit:
|
||||
await db.commit()
|
||||
@@ -31,6 +31,7 @@ class Document(Base):
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
trial_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=False)
|
||||
site_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), nullable=True)
|
||||
etmf_node_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("etmf_nodes.id"), nullable=True)
|
||||
doc_no: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
doc_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, Text, UniqueConstraint, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db.base_class import Base
|
||||
from app.models.document import DocumentScopeType
|
||||
|
||||
|
||||
class EtmfNode(Base):
|
||||
__tablename__ = "etmf_nodes"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("study_id", "parent_id", "code", name="uq_etmf_nodes_study_parent_code"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=False)
|
||||
parent_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("etmf_nodes.id"), nullable=True)
|
||||
code: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
scope_type: Mapped[DocumentScopeType] = mapped_column(
|
||||
Enum(DocumentScopeType, name="document_scope_type"),
|
||||
nullable=False,
|
||||
server_default=DocumentScopeType.GLOBAL.value,
|
||||
)
|
||||
required: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false")
|
||||
expected_doc_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
)
|
||||
|
||||
parent = relationship("EtmfNode", remote_side=[id], foreign_keys=[parent_id])
|
||||
@@ -25,6 +25,7 @@ class DocumentStatus(str, enum.Enum):
|
||||
class DocumentCreate(BaseModel):
|
||||
trial_id: uuid.UUID
|
||||
site_id: Optional[uuid.UUID] = None
|
||||
etmf_node_id: Optional[uuid.UUID] = None
|
||||
doc_no: str
|
||||
doc_type: str
|
||||
title: str
|
||||
@@ -39,6 +40,7 @@ class DocumentUpdate(BaseModel):
|
||||
scope_type: Optional[DocumentScopeType] = None
|
||||
owner_id: Optional[uuid.UUID] = None
|
||||
site_id: Optional[uuid.UUID] = None
|
||||
etmf_node_id: Optional[uuid.UUID] = None
|
||||
status: Optional[DocumentStatus] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
@@ -47,6 +49,7 @@ class DocumentSummary(BaseModel):
|
||||
id: uuid.UUID
|
||||
trial_id: uuid.UUID
|
||||
site_id: Optional[uuid.UUID] = None
|
||||
etmf_node_id: Optional[uuid.UUID] = None
|
||||
doc_type: str
|
||||
title: str
|
||||
scope_type: DocumentScopeType
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import enum
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.schemas.document import DocumentScopeType, DocumentSummary
|
||||
|
||||
|
||||
class EtmfNodeStatus(str, enum.Enum):
|
||||
NOT_REQUIRED = "NOT_REQUIRED"
|
||||
MISSING = "MISSING"
|
||||
UPLOADED = "UPLOADED"
|
||||
EFFECTIVE = "EFFECTIVE"
|
||||
INACTIVE = "INACTIVE"
|
||||
|
||||
|
||||
class EtmfNodeBase(BaseModel):
|
||||
parent_id: Optional[uuid.UUID] = None
|
||||
code: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
scope_type: DocumentScopeType = DocumentScopeType.GLOBAL
|
||||
required: bool = False
|
||||
expected_doc_type: Optional[str] = None
|
||||
sort_order: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class EtmfNodeCreate(EtmfNodeBase):
|
||||
study_id: uuid.UUID
|
||||
|
||||
|
||||
class EtmfNodeUpdate(BaseModel):
|
||||
parent_id: Optional[uuid.UUID] = None
|
||||
code: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
scope_type: Optional[DocumentScopeType] = None
|
||||
required: Optional[bool] = None
|
||||
expected_doc_type: Optional[str] = None
|
||||
sort_order: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class EtmfNodeRead(EtmfNodeBase):
|
||||
id: uuid.UUID
|
||||
study_id: uuid.UUID
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class EtmfNodeStatusSummary(BaseModel):
|
||||
status: EtmfNodeStatus
|
||||
document_count: int = 0
|
||||
effective_document_count: int = 0
|
||||
missing_count: int = 0
|
||||
|
||||
|
||||
class EtmfTreeNode(EtmfNodeRead):
|
||||
status: EtmfNodeStatus = EtmfNodeStatus.NOT_REQUIRED
|
||||
document_count: int = 0
|
||||
effective_document_count: int = 0
|
||||
children: list["EtmfTreeNode"] = Field(default_factory=list)
|
||||
|
||||
|
||||
class EtmfNodeDocuments(BaseModel):
|
||||
node: EtmfNodeRead
|
||||
documents: list[DocumentSummary] = Field(default_factory=list)
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import uuid
|
||||
@@ -16,6 +18,7 @@ from app.core.project_permissions import role_has_api_permission
|
||||
from app.crud import acknowledgement as acknowledgement_crud
|
||||
from app.crud import distribution as distribution_crud
|
||||
from app.crud import document as document_crud
|
||||
from app.crud import etmf as etmf_crud
|
||||
from app.crud import document_version as version_crud
|
||||
from app.crud import member as member_crud
|
||||
from app.crud import site as site_crud
|
||||
@@ -58,6 +61,7 @@ def _doc_snapshot(doc: Document) -> dict:
|
||||
"id": str(doc.id),
|
||||
"trial_id": str(doc.trial_id),
|
||||
"site_id": str(doc.site_id) if doc.site_id else None,
|
||||
"etmf_node_id": str(doc.etmf_node_id) if doc.etmf_node_id else None,
|
||||
"doc_no": doc.doc_no,
|
||||
"status": str(doc.status),
|
||||
"current_effective_version_id": str(doc.current_effective_version_id) if doc.current_effective_version_id else None,
|
||||
@@ -108,9 +112,20 @@ async def create_document(
|
||||
current_user,
|
||||
) -> Document:
|
||||
await _ensure_study_access(db, doc_in.trial_id, current_user, action="create_document")
|
||||
if doc_in.scope_type == DocumentScopeType.SITE and not doc_in.site_id:
|
||||
etmf_node = None
|
||||
if doc_in.etmf_node_id:
|
||||
etmf_node = await etmf_crud.get_node(db, doc_in.etmf_node_id)
|
||||
if not etmf_node:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="eTMF目录不存在")
|
||||
if etmf_node.study_id != doc_in.trial_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="eTMF目录不属于当前项目")
|
||||
if etmf_node.scope_type == DocumentScopeType.SITE and not doc_in.site_id:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="中心级eTMF目录需指定中心")
|
||||
|
||||
scope_type = etmf_node.scope_type if etmf_node else doc_in.scope_type
|
||||
if scope_type == DocumentScopeType.SITE and not doc_in.site_id:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="分中心文件需指定分中心")
|
||||
site_id = doc_in.site_id if doc_in.scope_type == DocumentScopeType.SITE else None
|
||||
site_id = doc_in.site_id if scope_type == DocumentScopeType.SITE else None
|
||||
if site_id:
|
||||
site = await site_crud.get_site(db, site_id)
|
||||
if not site or not site.is_active:
|
||||
@@ -122,10 +137,11 @@ async def create_document(
|
||||
doc = Document(
|
||||
trial_id=doc_in.trial_id,
|
||||
site_id=site_id,
|
||||
etmf_node_id=doc_in.etmf_node_id,
|
||||
doc_no=doc_in.doc_no,
|
||||
doc_type=doc_in.doc_type,
|
||||
title=doc_in.title,
|
||||
scope_type=doc_in.scope_type,
|
||||
scope_type=scope_type,
|
||||
owner_id=doc_in.owner_id,
|
||||
description=doc_in.description,
|
||||
)
|
||||
@@ -154,6 +170,7 @@ async def list_documents(
|
||||
doc_type: str | None,
|
||||
status: str | None,
|
||||
scope_type: str | None,
|
||||
etmf_node_id: uuid.UUID | None = None,
|
||||
skip: int,
|
||||
limit: int,
|
||||
current_user,
|
||||
@@ -168,6 +185,7 @@ async def list_documents(
|
||||
doc_type=doc_type,
|
||||
status=status,
|
||||
scope_type=scope_type,
|
||||
etmf_node_id=etmf_node_id,
|
||||
cra_site_ids=cra_site_ids,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
@@ -185,6 +203,7 @@ async def list_documents(
|
||||
id=doc.id,
|
||||
trial_id=doc.trial_id,
|
||||
site_id=doc.site_id,
|
||||
etmf_node_id=doc.etmf_node_id,
|
||||
doc_type=doc.doc_type,
|
||||
title=doc.title,
|
||||
scope_type=doc.scope_type,
|
||||
@@ -228,6 +247,7 @@ async def get_document_detail(
|
||||
id=doc.id,
|
||||
trial_id=doc.trial_id,
|
||||
site_id=doc.site_id,
|
||||
etmf_node_id=doc.etmf_node_id,
|
||||
doc_type=doc.doc_type,
|
||||
title=doc.title,
|
||||
scope_type=doc.scope_type,
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from typing import Iterable
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.crud import document as document_crud
|
||||
from app.crud import etmf as etmf_crud
|
||||
from app.models.document import Document, DocumentStatus
|
||||
from app.models.etmf import EtmfNode
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.schemas.document import DocumentCreate, DocumentSummary
|
||||
from app.schemas.etmf import EtmfNodeCreate, EtmfNodeRead, EtmfNodeStatus, EtmfTreeNode, EtmfNodeUpdate
|
||||
|
||||
|
||||
def _role_value(user) -> str:
|
||||
if user is None:
|
||||
return "SYSTEM"
|
||||
return user.role.value if hasattr(user.role, "value") else str(user.role)
|
||||
|
||||
|
||||
def calculate_node_status(node: EtmfNode, documents: Iterable[Document]) -> EtmfNodeStatus:
|
||||
docs = list(documents)
|
||||
if not node.is_active:
|
||||
return EtmfNodeStatus.INACTIVE
|
||||
if any(doc.current_effective_version_id for doc in docs):
|
||||
return EtmfNodeStatus.EFFECTIVE
|
||||
if docs:
|
||||
return EtmfNodeStatus.UPLOADED
|
||||
if node.required:
|
||||
return EtmfNodeStatus.MISSING
|
||||
return EtmfNodeStatus.NOT_REQUIRED
|
||||
|
||||
|
||||
def _tree_node(node: EtmfNode, documents: list[Document], children: list[EtmfTreeNode]) -> EtmfTreeNode:
|
||||
return EtmfTreeNode(
|
||||
id=node.id,
|
||||
study_id=node.study_id,
|
||||
parent_id=node.parent_id,
|
||||
code=node.code,
|
||||
name=node.name,
|
||||
description=node.description,
|
||||
scope_type=node.scope_type,
|
||||
required=node.required,
|
||||
expected_doc_type=node.expected_doc_type,
|
||||
sort_order=node.sort_order,
|
||||
is_active=node.is_active,
|
||||
created_at=node.created_at,
|
||||
updated_at=node.updated_at,
|
||||
status=calculate_node_status(node, documents),
|
||||
document_count=len(documents),
|
||||
effective_document_count=sum(1 for doc in documents if doc.current_effective_version_id),
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
async def _documents_by_node(db: AsyncSession, study_id: uuid.UUID) -> dict[uuid.UUID, list[Document]]:
|
||||
result = await db.execute(
|
||||
select(Document).where(
|
||||
Document.trial_id == study_id,
|
||||
Document.etmf_node_id.is_not(None),
|
||||
Document.status != DocumentStatus.ARCHIVED,
|
||||
)
|
||||
)
|
||||
grouped: dict[uuid.UUID, list[Document]] = defaultdict(list)
|
||||
for document in result.scalars().all():
|
||||
if document.etmf_node_id:
|
||||
grouped[document.etmf_node_id].append(document)
|
||||
return grouped
|
||||
|
||||
|
||||
async def list_etmf_tree(db: AsyncSession, *, study_id: uuid.UUID, current_user) -> list[EtmfTreeNode]:
|
||||
if current_user is not None:
|
||||
from app.services import document_service
|
||||
|
||||
await document_service._ensure_study_access(db, study_id, current_user, action="view")
|
||||
nodes = list(await etmf_crud.list_nodes_by_study(db, study_id))
|
||||
documents = await _documents_by_node(db, study_id)
|
||||
children_by_parent: dict[uuid.UUID | None, list[EtmfNode]] = defaultdict(list)
|
||||
for node in nodes:
|
||||
children_by_parent[node.parent_id].append(node)
|
||||
|
||||
def build(node: EtmfNode) -> EtmfTreeNode:
|
||||
children = [build(child) for child in children_by_parent.get(node.id, [])]
|
||||
return _tree_node(node, documents.get(node.id, []), children)
|
||||
|
||||
return [build(node) for node in children_by_parent.get(None, [])]
|
||||
|
||||
|
||||
async def create_node(db: AsyncSession, payload: EtmfNodeCreate, current_user) -> EtmfNode:
|
||||
from app.services import document_service
|
||||
|
||||
await document_service._ensure_study_access(db, payload.study_id, current_user, action="create_document")
|
||||
if payload.parent_id:
|
||||
parent = await etmf_crud.get_node(db, payload.parent_id)
|
||||
if not parent or parent.study_id != payload.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="父级eTMF目录不存在或不属于当前项目")
|
||||
node = EtmfNode(**payload.model_dump())
|
||||
db.add(node)
|
||||
db.add(
|
||||
AuditLog(
|
||||
study_id=payload.study_id,
|
||||
entity_type="ETMF_NODE",
|
||||
entity_id=node.id,
|
||||
action="ETMF_NODE_CREATED",
|
||||
detail=f'{{"code":"{node.code}","name":"{node.name}"}}',
|
||||
operator_id=current_user.id,
|
||||
operator_role=_role_value(current_user),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(node)
|
||||
return node
|
||||
|
||||
|
||||
async def update_node(db: AsyncSession, node_id: uuid.UUID, payload: EtmfNodeUpdate, current_user) -> EtmfNode:
|
||||
from app.services import document_service
|
||||
|
||||
node = await etmf_crud.get_node(db, node_id)
|
||||
if not node:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="eTMF目录不存在")
|
||||
await document_service._ensure_study_access(db, node.study_id, current_user, action="create_version")
|
||||
values = payload.model_dump(exclude_unset=True)
|
||||
if "parent_id" in values and values["parent_id"]:
|
||||
parent = await etmf_crud.get_node(db, values["parent_id"])
|
||||
if not parent or parent.study_id != node.study_id or parent.id == node.id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="父级eTMF目录不合法")
|
||||
await etmf_crud.update_node(db, node_id, values, commit=False)
|
||||
db.add(
|
||||
AuditLog(
|
||||
study_id=node.study_id,
|
||||
entity_type="ETMF_NODE",
|
||||
entity_id=node.id,
|
||||
action="ETMF_NODE_UPDATED",
|
||||
detail="{}",
|
||||
operator_id=current_user.id,
|
||||
operator_role=_role_value(current_user),
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
refreshed = await etmf_crud.get_node(db, node_id)
|
||||
if not refreshed:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="eTMF目录不存在")
|
||||
return refreshed
|
||||
|
||||
|
||||
async def list_node_documents(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
node_id: uuid.UUID,
|
||||
site_id: uuid.UUID | None,
|
||||
current_user,
|
||||
) -> list[DocumentSummary]:
|
||||
from app.services import document_service
|
||||
|
||||
node = await etmf_crud.get_node(db, node_id)
|
||||
if not node:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="eTMF目录不存在")
|
||||
return await document_service.list_documents(
|
||||
db,
|
||||
trial_id=node.study_id,
|
||||
site_id=site_id,
|
||||
doc_type=None,
|
||||
status=None,
|
||||
scope_type=None,
|
||||
etmf_node_id=node_id,
|
||||
skip=0,
|
||||
limit=500,
|
||||
current_user=current_user,
|
||||
)
|
||||
|
||||
|
||||
async def create_node_document(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
node_id: uuid.UUID,
|
||||
payload: DocumentCreate,
|
||||
current_user,
|
||||
) -> DocumentSummary:
|
||||
from app.services import document_service
|
||||
|
||||
node = await etmf_crud.get_node(db, node_id)
|
||||
if not node:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="eTMF目录不存在")
|
||||
doc_payload = payload.model_copy(update={"trial_id": node.study_id, "etmf_node_id": node.id, "scope_type": node.scope_type})
|
||||
document = await document_service.create_document(db, doc_payload, current_user)
|
||||
return DocumentSummary.model_validate(document)
|
||||
Reference in New Issue
Block a user