完善后台管理与测试支撑
1、为后台用户列表增加关键词和状态筛选,后端同步支持过滤与总数统计。 2、创建用户时要求填写初始密码,并补充前端校验和自动填充隔离。 3、优化项目管理列表角色展示、项目详情抽屉脏数据保护和审计详情抽屉关闭体验。 4、清理旧费用附件关联、统一测试模型注册,并更新迁移端点和注册筛选测试。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, is_system_admin, require_roles
|
||||
@@ -8,7 +8,7 @@ from app.schemas.common import PaginatedResponse
|
||||
from app.crud import user as user_crud
|
||||
from app.crud import member as member_crud
|
||||
from app.utils.pagination import paginate
|
||||
from app.schemas.user import UserCreate, UserRead, UserUpdate
|
||||
from app.schemas.user import UserCreate, UserRead, UserStatus, UserUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -17,12 +17,14 @@ router = APIRouter()
|
||||
async def list_users(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
keyword: str | None = Query(default=None),
|
||||
user_status: UserStatus | None = Query(default=None, alias="status"),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> PaginatedResponse[UserRead]:
|
||||
users = await user_crud.list_users(db, skip=skip, limit=limit)
|
||||
total_users = await user_crud.list_users(db, skip=0, limit=10_000_000)
|
||||
return paginate(list(users), total=len(total_users))
|
||||
users = await user_crud.list_users(db, skip=skip, limit=limit, keyword=keyword, status=user_status)
|
||||
total_users = await user_crud.count_users(db, keyword=keyword, status=user_status)
|
||||
return paginate(list(users), total=total_users)
|
||||
|
||||
|
||||
@router.post("/", response_model=UserRead, status_code=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -16,7 +16,6 @@ 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.kickoff_meeting import KickoffMeeting
|
||||
from app.models.precaution import Precaution
|
||||
@@ -33,7 +32,6 @@ 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"
|
||||
|
||||
|
||||
@@ -239,24 +237,7 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None:
|
||||
)
|
||||
)
|
||||
|
||||
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 distribution_ids:
|
||||
@@ -311,7 +292,6 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None:
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import delete, func, select, update
|
||||
from sqlalchemy import delete, func, or_, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import (
|
||||
@@ -79,11 +79,45 @@ async def update_user(db: AsyncSession, user: User, user_in: UserUpdate) -> User
|
||||
return user
|
||||
|
||||
|
||||
async def list_users(db: AsyncSession, skip: int = 0, limit: int = 100) -> Sequence[User]:
|
||||
result = await db.execute(select(User).offset(skip).limit(limit))
|
||||
def _apply_user_filters(query, *, keyword: str | None = None, status: UserStatus | None = None):
|
||||
if keyword:
|
||||
pattern = f"%{keyword.strip()}%"
|
||||
query = query.where(
|
||||
or_(
|
||||
User.email.ilike(pattern),
|
||||
User.full_name.ilike(pattern),
|
||||
User.clinical_department.ilike(pattern),
|
||||
)
|
||||
)
|
||||
if status is not None:
|
||||
query = query.where(User.status == status)
|
||||
return query
|
||||
|
||||
|
||||
async def list_users(
|
||||
db: AsyncSession,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
*,
|
||||
keyword: str | None = None,
|
||||
status: UserStatus | None = None,
|
||||
) -> Sequence[User]:
|
||||
query = _apply_user_filters(select(User), keyword=keyword, status=status)
|
||||
result = await db.execute(query.order_by(User.created_at.desc()).offset(skip).limit(limit))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def count_users(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
keyword: str | None = None,
|
||||
status: UserStatus | None = None,
|
||||
) -> int:
|
||||
query = _apply_user_filters(select(func.count()).select_from(User), keyword=keyword, status=status)
|
||||
result = await db.execute(query)
|
||||
return int(result.scalar_one() or 0)
|
||||
|
||||
|
||||
async def list_active_member_candidates_for_study(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
@@ -141,7 +142,7 @@ class StudySetupConfigVersionRead(BaseModel):
|
||||
parent_version_id: uuid.UUID | None = None
|
||||
merged_from_version_id: uuid.UUID | None = None
|
||||
config: StudySetupConfigData
|
||||
published_project_snapshot: "ProjectPublishSnapshot | None" = None
|
||||
published_project_snapshot: Optional[ProjectPublishSnapshot] = None
|
||||
is_current_published: bool = False
|
||||
is_active_draft_base: bool = False
|
||||
published_by: uuid.UUID | None = None
|
||||
|
||||
Reference in New Issue
Block a user