细节优化——1
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
@@ -46,6 +47,8 @@ async def create_ae(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> AERead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
if ae_in.onset_date and ae_in.onset_date > date.today():
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Onset date cannot be in the future")
|
||||
member_role = await _get_member_role(db, study_id, current_user.id)
|
||||
if current_user.role != "ADMIN" and member_role not in ALLOWED_CREATE_ROLES:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
@@ -124,6 +127,8 @@ async def update_ae(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> AERead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
if ae_in.onset_date and ae_in.onset_date > date.today():
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Onset date cannot be in the future")
|
||||
ae = await ae_crud.get_ae(db, ae_id)
|
||||
if not ae or ae.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="AE not found")
|
||||
@@ -142,12 +147,24 @@ async def update_ae(
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
|
||||
old_status = ae.status
|
||||
updated = await ae_crud.update_ae(db, ae, ae_in)
|
||||
detail_before = {
|
||||
"event": ae.term,
|
||||
"onset_date": ae.onset_date,
|
||||
"severity": ae.severity,
|
||||
"status": ae.status,
|
||||
"description": ae.description,
|
||||
}
|
||||
updated = await ae_crud.update_ae(db, study_id, ae, ae_in)
|
||||
detail_after = {
|
||||
"event": updated.term,
|
||||
"onset_date": updated.onset_date,
|
||||
"severity": updated.severity,
|
||||
"status": updated.status,
|
||||
"description": updated.description,
|
||||
}
|
||||
|
||||
detail = None
|
||||
action = "UPDATE_AE"
|
||||
if ae_in.status and ae_in.status != old_status:
|
||||
detail = f"AE {ae_id} status {old_status} -> {ae_in.status}"
|
||||
action = "AE_STATUS_CHANGE"
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
@@ -155,7 +172,7 @@ async def update_ae(
|
||||
entity_type="ae",
|
||||
entity_id=ae_id,
|
||||
action=action,
|
||||
detail=detail or "AE updated",
|
||||
detail=json.dumps({"before": detail_before, "after": detail_after}, ensure_ascii=False, default=str),
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, require_study_member
|
||||
from app.core.deps import get_db_session, require_roles, require_study_member
|
||||
from app.crud import audit as audit_crud
|
||||
from app.schemas.audit import AuditLogRead
|
||||
|
||||
@@ -20,6 +20,7 @@ async def list_audit_logs(
|
||||
entity_type: str | None = None,
|
||||
entity_id: uuid.UUID | None = None,
|
||||
action: str | None = None,
|
||||
operator_id: uuid.UUID | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
@@ -30,7 +31,24 @@ async def list_audit_logs(
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
action=action,
|
||||
operator_id=operator_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
return list(logs)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{log_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(require_roles(["ADMIN"]))],
|
||||
)
|
||||
async def delete_audit_log(
|
||||
study_id: uuid.UUID,
|
||||
log_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
log = await audit_crud.get_log(db, log_id)
|
||||
if not log or log.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Audit log not found")
|
||||
await audit_crud.delete_log(db, log)
|
||||
|
||||
@@ -7,7 +7,7 @@ from app.core.deps import get_current_user, get_db_session, require_study_member
|
||||
from app.crud import comment as comment_crud
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import study as study_crud
|
||||
from app.schemas.comment import CommentCreate, CommentRead
|
||||
from app.schemas.comment import CommentCreate, CommentRead, CommentQuote
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -34,6 +34,18 @@ async def create_comment(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> CommentRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
quote = None
|
||||
if comment_in.quote_comment_id:
|
||||
quote = await comment_crud.get_comment(db, comment_in.quote_comment_id, include_deleted=True)
|
||||
if (
|
||||
not quote
|
||||
or quote.study_id != study_id
|
||||
or quote.entity_type != entity_type
|
||||
or quote.entity_id != entity_id
|
||||
):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="引用评论不存在")
|
||||
if quote.is_deleted:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="引用评论已删除")
|
||||
comment = await comment_crud.create_comment(
|
||||
db,
|
||||
study_id=study_id,
|
||||
@@ -52,7 +64,15 @@ async def create_comment(
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
return comment
|
||||
return CommentRead(
|
||||
id=comment.id,
|
||||
content=comment.content,
|
||||
created_by=comment.created_by,
|
||||
created_at=comment.created_at,
|
||||
quote_comment_id=comment.quote_comment_id,
|
||||
quote=CommentQuote.model_validate(quote) if quote else None,
|
||||
is_deleted=comment.is_deleted,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -68,4 +88,57 @@ async def list_comments(
|
||||
) -> list[CommentRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
comments = await comment_crud.list_comments(db, study_id, entity_type, entity_id)
|
||||
return list(comments)
|
||||
quote_ids = {c.quote_comment_id for c in comments if c.quote_comment_id}
|
||||
quote_map = await comment_crud.get_comments_by_ids(db, quote_ids, include_deleted=True)
|
||||
return [
|
||||
CommentRead(
|
||||
id=c.id,
|
||||
content=c.content,
|
||||
created_by=c.created_by,
|
||||
created_at=c.created_at,
|
||||
quote_comment_id=c.quote_comment_id,
|
||||
quote=CommentQuote.model_validate(quote_map.get(c.quote_comment_id)) if c.quote_comment_id else None,
|
||||
is_deleted=c.is_deleted,
|
||||
)
|
||||
for c in comments
|
||||
]
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{comment_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def delete_comment(
|
||||
study_id: uuid.UUID,
|
||||
entity_type: str,
|
||||
entity_id: uuid.UUID,
|
||||
comment_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
await _ensure_study_exists(db, study_id)
|
||||
comment = await comment_crud.get_comment(db, comment_id, include_deleted=True)
|
||||
if (
|
||||
not comment
|
||||
or comment.study_id != study_id
|
||||
or comment.entity_type != entity_type
|
||||
or comment.entity_id != entity_id
|
||||
):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Comment not found")
|
||||
role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role
|
||||
if role_value != "ADMIN" and comment.created_by != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
if comment.is_deleted:
|
||||
return
|
||||
await comment_crud.soft_delete_comment(db, comment)
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
action="DELETE_COMMENT",
|
||||
detail=f"Comment deleted by {current_user.full_name}",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
|
||||
@@ -15,13 +15,9 @@ from app.utils.pagination import paginate
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _check_permission_for_scope(study_id: uuid.UUID | None, current_user, member_role: str | None):
|
||||
if study_id is None:
|
||||
if current_user.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only admin can manage global FAQ")
|
||||
else:
|
||||
if current_user.role != "ADMIN" and member_role != "PM":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
def _check_permission_for_scope(study_id: uuid.UUID, current_user, member_role: str | None):
|
||||
if current_user.role != "ADMIN" and member_role != "PM":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -36,15 +32,16 @@ async def create_category(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> CategoryRead:
|
||||
if not payload.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="study_id is required")
|
||||
existing = await category_crud.get_category_by_name(db, payload.study_id, payload.name)
|
||||
if existing:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分类名称已存在")
|
||||
member_role = None
|
||||
if payload.study_id:
|
||||
member = await member_crud.get_member(db, payload.study_id, current_user.id)
|
||||
if not member:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
member_role = member.role_in_study
|
||||
member = await member_crud.get_member(db, payload.study_id, current_user.id)
|
||||
if not member and current_user.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
member_role = member.role_in_study if member else None
|
||||
_check_permission_for_scope(payload.study_id, current_user, member_role)
|
||||
category = await category_crud.create_category(db, payload)
|
||||
await audit_crud.log_action(
|
||||
@@ -68,16 +65,17 @@ async def create_category(
|
||||
)
|
||||
async def list_categories(
|
||||
study_id: uuid.UUID | None = None,
|
||||
include_global: bool = True,
|
||||
is_active: bool | None = True,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[CategoryRead]:
|
||||
if not study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="study_id is required")
|
||||
if study_id:
|
||||
member = await member_crud.get_member(db, study_id, current_user.id)
|
||||
if not member and current_user.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
categories = await category_crud.list_categories(db, study_id, include_global=include_global, is_active=is_active)
|
||||
categories = await category_crud.list_categories(db, study_id, include_global=False, is_active=is_active)
|
||||
return paginate([CategoryRead.model_validate(c) for c in categories], total=len(categories))
|
||||
|
||||
|
||||
@@ -105,11 +103,12 @@ async def update_category(
|
||||
existing = await category_crud.get_category_by_name(db, target_study_id, target_name)
|
||||
if existing and existing.id != category.id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分类名称已存在")
|
||||
if target_study_id:
|
||||
member = await member_crud.get_member(db, target_study_id, current_user.id)
|
||||
member_role = member.role_in_study if member else None
|
||||
if not member and current_user.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
if not target_study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="study_id is required")
|
||||
member = await member_crud.get_member(db, target_study_id, current_user.id)
|
||||
member_role = member.role_in_study if member else None
|
||||
if not member and current_user.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
_check_permission_for_scope(target_study_id, current_user, member_role)
|
||||
updated = await category_crud.update_category(db, category, payload)
|
||||
await audit_crud.log_action(
|
||||
|
||||
+37
-44
@@ -25,22 +25,14 @@ from app.utils.pagination import paginate
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _check_write_permission(study_id: uuid.UUID | None, current_user, member_role: str | None):
|
||||
if study_id is None:
|
||||
if current_user.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only admin can manage global FAQ")
|
||||
else:
|
||||
if current_user.role != "ADMIN" and member_role != "PM":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
def _check_write_permission(study_id: uuid.UUID, current_user, member_role: str | None):
|
||||
if current_user.role != "ADMIN" and member_role != "PM":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
|
||||
|
||||
def _check_create_permission(study_id: uuid.UUID | None, current_user, is_member: bool):
|
||||
if study_id is None:
|
||||
if current_user.role != "ADMIN":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only admin can manage global FAQ")
|
||||
else:
|
||||
if current_user.role != "ADMIN" and not is_member:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
def _check_create_permission(current_user, is_member: bool):
|
||||
if current_user.role != "ADMIN" and not is_member:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -55,6 +47,8 @@ async def create_faq(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> FaqRead:
|
||||
if not payload.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="study_id is required")
|
||||
cat = await category_crud.get_category(db, payload.category_id)
|
||||
if not cat:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found")
|
||||
@@ -62,11 +56,10 @@ async def create_faq(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Category scope mismatch")
|
||||
member_role = None
|
||||
is_member = False
|
||||
if payload.study_id:
|
||||
member = await member_crud.get_member(db, payload.study_id, current_user.id)
|
||||
member_role = member.role_in_study if member else None
|
||||
is_member = member is not None
|
||||
_check_create_permission(payload.study_id, current_user, is_member)
|
||||
member = await member_crud.get_member(db, payload.study_id, current_user.id)
|
||||
member_role = member.role_in_study if member else None
|
||||
is_member = member is not None
|
||||
_check_create_permission(current_user, is_member)
|
||||
try:
|
||||
item = await faq_crud.create_item(db, payload, created_by=current_user.id)
|
||||
except ValueError as exc:
|
||||
@@ -104,7 +97,6 @@ async def list_faqs(
|
||||
category_id: uuid.UUID | None = None,
|
||||
keyword: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
study_scope: str | None = "project",
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> list[FaqRead]:
|
||||
@@ -118,16 +110,15 @@ async def list_faqs(
|
||||
membership_cache[sid] = role
|
||||
return role
|
||||
|
||||
if study_id:
|
||||
if current_user.role != "ADMIN":
|
||||
role = await _get_member_role(study_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
if not study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="study_id is required")
|
||||
if current_user.role != "ADMIN":
|
||||
role = await _get_member_role(study_id)
|
||||
if not role:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
|
||||
if is_active is False and current_user.role != "ADMIN":
|
||||
role = None
|
||||
if study_id:
|
||||
role = await _get_member_role(study_id)
|
||||
role = await _get_member_role(study_id)
|
||||
if role != "PM":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions")
|
||||
|
||||
@@ -137,17 +128,17 @@ async def list_faqs(
|
||||
category_id=category_id,
|
||||
keyword=keyword,
|
||||
is_active=is_active,
|
||||
study_scope=study_scope,
|
||||
study_scope="project",
|
||||
)
|
||||
|
||||
visible: list[FaqRead] = []
|
||||
for it in items:
|
||||
if it.study_id and current_user.role != "ADMIN":
|
||||
if current_user.role != "ADMIN":
|
||||
role = await _get_member_role(it.study_id)
|
||||
if not role:
|
||||
continue
|
||||
if not it.is_active and current_user.role != "ADMIN":
|
||||
role = await _get_member_role(it.study_id) if it.study_id else None
|
||||
role = await _get_member_role(it.study_id)
|
||||
if role != "PM":
|
||||
continue
|
||||
visible.append(FaqRead.model_validate(it))
|
||||
@@ -168,15 +159,15 @@ async def get_faq(
|
||||
item = await faq_crud.get_item(db, item_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found")
|
||||
if item.study_id and current_user.role != "ADMIN":
|
||||
if not item.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found")
|
||||
if current_user.role != "ADMIN":
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
if not member:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member")
|
||||
if not item.is_active and current_user.role not in {"ADMIN"}:
|
||||
member_role = None
|
||||
if item.study_id:
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
member_role = member.role_in_study if member else None
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
member_role = member.role_in_study if member else None
|
||||
if member_role != "PM":
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive FAQ")
|
||||
return FaqRead.model_validate(item)
|
||||
@@ -267,11 +258,12 @@ async def set_best_reply(
|
||||
item = await faq_crud.get_item(db, item_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found")
|
||||
if not item.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found")
|
||||
is_member = False
|
||||
if item.study_id:
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
is_member = member is not None
|
||||
_check_create_permission(item.study_id, current_user, is_member)
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
is_member = member is not None
|
||||
_check_create_permission(current_user, is_member)
|
||||
if payload.best_reply_id:
|
||||
reply = await reply_crud.get_reply(db, payload.best_reply_id)
|
||||
if not reply or reply.faq_id != item.id:
|
||||
@@ -348,13 +340,14 @@ async def create_reply(
|
||||
item = await faq_crud.get_item(db, item_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found")
|
||||
if not item.study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found")
|
||||
if not payload.content.strip():
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Reply content is required")
|
||||
is_member = False
|
||||
if item.study_id:
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
is_member = member is not None
|
||||
_check_create_permission(item.study_id, current_user, is_member)
|
||||
member = await member_crud.get_member(db, item.study_id, current_user.id)
|
||||
is_member = member is not None
|
||||
_check_create_permission(current_user, is_member)
|
||||
quote = None
|
||||
if payload.quote_reply_id:
|
||||
quote = await reply_crud.get_reply(db, payload.quote_reply_id)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles
|
||||
from app.crud import audit as audit_crud
|
||||
from app.crud import attachment as attachment_crud
|
||||
from app.crud import milestone as milestone_crud
|
||||
from app.crud import study as study_crud
|
||||
from app.crud import user as user_crud
|
||||
@@ -36,6 +38,8 @@ async def create_milestone(
|
||||
current_user=Depends(get_current_user),
|
||||
) -> MilestoneRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
if milestone_in.planned_date and milestone_in.planned_date < date.today():
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="计划日期不得早于今天")
|
||||
milestone = await milestone_crud.create(db, study_id, milestone_in)
|
||||
owner = None
|
||||
site = None
|
||||
@@ -127,6 +131,15 @@ async def update_milestone(
|
||||
milestone = await milestone_crud.get(db, milestone_id)
|
||||
if not milestone or milestone.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Milestone not found")
|
||||
if milestone_in.planned_date and milestone_in.planned_date < date.today():
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="计划日期不得早于今天")
|
||||
if milestone_in.actual_date and milestone_in.actual_date < date.today():
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="实际日期不得早于今天")
|
||||
if milestone_in.actual_date:
|
||||
attachments = await attachment_crud.list_attachments(db, study_id, "ethics_node", milestone_id)
|
||||
if not attachments:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请先上传伦理批件后再更新实际日期")
|
||||
milestone_in.status = "DONE"
|
||||
old_status = milestone.status
|
||||
updated = await milestone_crud.update(db, milestone, milestone_in)
|
||||
owner = None
|
||||
@@ -164,3 +177,32 @@ async def update_milestone(
|
||||
created_at=updated.created_at,
|
||||
updated_at=updated.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{milestone_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def delete_milestone(
|
||||
study_id: uuid.UUID,
|
||||
milestone_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> None:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
milestone = await milestone_crud.get(db, milestone_id)
|
||||
if not milestone or milestone.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Milestone not found")
|
||||
await milestone_crud.delete_milestone(db, milestone)
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
entity_type="milestone",
|
||||
entity_id=milestone_id,
|
||||
action="DELETE_MILESTONE",
|
||||
detail=f"Milestone {milestone_id} deleted",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -52,6 +52,17 @@ async def update_user(
|
||||
db_user = await user_crud.get_by_id(db, user_id)
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
if db_user.role.value == "ADMIN":
|
||||
requested_role = user_in.role
|
||||
requested_status = user_in.status
|
||||
if user_in.is_active is not None:
|
||||
requested_status = "ACTIVE" if user_in.is_active else "DISABLED"
|
||||
will_leave_admin = requested_role is not None and requested_role != "ADMIN"
|
||||
will_disable = requested_status is not None and requested_status != "ACTIVE"
|
||||
if (will_leave_admin or will_disable) and db_user.status.value == "ACTIVE":
|
||||
active_admins = await user_crud.count_active_admins(db)
|
||||
if active_admins <= 1:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="至少保留一个管理员账号")
|
||||
user = await user_crud.update_user(db, db_user, user_in)
|
||||
return user
|
||||
|
||||
@@ -71,6 +82,10 @@ async def delete_user(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
if db_user.id == current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不允许删除自己")
|
||||
if db_user.role.value == "ADMIN" and db_user.status.value == "ACTIVE":
|
||||
active_admins = await user_crud.count_active_admins(db)
|
||||
if active_admins <= 1:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="至少保留一个管理员账号")
|
||||
has_membership = await member_crud.user_has_memberships(db, db_user.id)
|
||||
if has_membership:
|
||||
raise HTTPException(
|
||||
|
||||
Reference in New Issue
Block a user