细节优化——1

This commit is contained in:
Cheng Zhou
2025-12-30 14:07:57 +08:00
parent 71db309e12
commit 0c1fc49f17
40 changed files with 1610 additions and 389 deletions
+21 -4
View File
@@ -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,
)
+20 -2
View File
@@ -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)
+76 -3
View File
@@ -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,
)
+18 -19
View File
@@ -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
View File
@@ -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)
+42
View File
@@ -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
+15
View File
@@ -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(
+4 -2
View File
@@ -52,7 +52,7 @@ async def create_ae(
severity=ae_in.severity,
causality=ae_in.causality,
action_taken=None,
outcome=None,
outcome=ae_in.outcome,
reported_to_sponsor=False,
report_due_date=due_date,
status="NEW",
@@ -96,8 +96,10 @@ async def list_ae(
return result.scalars().all()
async def update_ae(db: AsyncSession, ae: AdverseEvent, ae_in: AEUpdate) -> AdverseEvent:
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 "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:
onset = update_data.get("onset_date", ae.onset_date)
seriousness = update_data.get("seriousness", ae.seriousness)
+13
View File
@@ -40,6 +40,7 @@ async def list_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,
) -> Sequence[AuditLog]:
@@ -50,6 +51,18 @@ async def list_logs(
stmt = stmt.where(AuditLog.entity_id == entity_id)
if action:
stmt = stmt.where(AuditLog.action == action)
if operator_id:
stmt = stmt.where(AuditLog.operator_id == operator_id)
stmt = stmt.order_by(AuditLog.created_at.desc()).offset(skip).limit(limit)
result = await db.execute(stmt)
return result.scalars().all()
async def get_log(db: AsyncSession, log_id: uuid.UUID) -> AuditLog | None:
result = await db.execute(select(AuditLog).where(AuditLog.id == log_id))
return result.scalar_one_or_none()
async def delete_log(db: AsyncSession, log: AuditLog) -> None:
await db.delete(log)
await db.commit()
+33
View File
@@ -21,6 +21,7 @@ async def create_comment(
entity_type=entity_type,
entity_id=entity_id,
content=comment_in.content,
quote_comment_id=comment_in.quote_comment_id,
created_by=created_by,
)
db.add(comment)
@@ -46,3 +47,35 @@ async def list_comments(
.order_by(Comment.created_at.asc())
)
return result.scalars().all()
async def get_comment(db: AsyncSession, comment_id: uuid.UUID, *, include_deleted: bool = False) -> Comment | None:
stmt = select(Comment).where(Comment.id == comment_id)
if not include_deleted:
stmt = stmt.where(Comment.is_deleted.is_(False))
result = await db.execute(stmt)
return result.scalar_one_or_none()
async def get_comments_by_ids(
db: AsyncSession,
ids: set[uuid.UUID],
*,
include_deleted: bool = False,
) -> dict[uuid.UUID, Comment]:
if not ids:
return {}
stmt = select(Comment).where(Comment.id.in_(ids))
if not include_deleted:
stmt = stmt.where(Comment.is_deleted.is_(False))
result = await db.execute(stmt)
comments = result.scalars().all()
return {c.id: c for c in comments}
async def soft_delete_comment(db: AsyncSession, comment: Comment) -> Comment:
comment.is_deleted = True
db.add(comment)
await db.commit()
await db.refresh(comment)
return comment
+6 -1
View File
@@ -1,7 +1,7 @@
import uuid
from typing import Sequence
from sqlalchemy import select, update as sa_update
from sqlalchemy import delete, select, update as sa_update
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.milestone import Milestone
@@ -47,3 +47,8 @@ async def update(db: AsyncSession, milestone: Milestone, milestone_in: Milestone
await db.commit()
await db.refresh(milestone)
return milestone
async def delete_milestone(db: AsyncSession, milestone: Milestone) -> None:
await db.execute(delete(Milestone).where(Milestone.id == milestone.id))
await db.commit()
+9
View File
@@ -71,6 +71,15 @@ async def list_users(db: AsyncSession, skip: int = 0, limit: int = 100) -> Seque
return result.scalars().all()
async def count_active_admins(db: AsyncSession) -> int:
result = await db.execute(
select(func.count())
.select_from(User)
.where(User.role == UserRole.ADMIN, User.status == UserStatus.ACTIVE)
)
return int(result.scalar_one() or 0)
async def list_users_by_status(
db: AsyncSession, status: UserStatus | None = None, skip: int = 0, limit: int = 100
) -> Sequence[User]:
+1
View File
@@ -16,6 +16,7 @@ class Comment(Base):
entity_type: Mapped[str] = mapped_column(String(50), nullable=False)
entity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
quote_comment_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("comments.id"), nullable=True)
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false")
+4
View File
@@ -15,9 +15,13 @@ class AECreate(BaseModel):
severity: str
causality: Optional[str] = None
description: Optional[str] = None
outcome: Optional[str] = None
class AEUpdate(BaseModel):
subject_id: Optional[uuid.UUID] = None
term: Optional[str] = None
onset_date: Optional[date] = None
resolution_date: Optional[date] = None
seriousness: Optional[str] = None
severity: Optional[str] = None
+14
View File
@@ -6,6 +6,17 @@ from pydantic import BaseModel, ConfigDict, Field
class CommentCreate(BaseModel):
content: str = Field(min_length=1)
quote_comment_id: uuid.UUID | None = None
class CommentQuote(BaseModel):
id: uuid.UUID
content: str
created_by: uuid.UUID
created_at: datetime
is_deleted: bool = False
model_config = ConfigDict(from_attributes=True)
class CommentRead(BaseModel):
@@ -13,5 +24,8 @@ class CommentRead(BaseModel):
content: str
created_by: uuid.UUID
created_at: datetime
quote_comment_id: uuid.UUID | None = None
quote: CommentQuote | None = None
is_deleted: bool = False
model_config = ConfigDict(from_attributes=True)