Step 5:里程碑与任务(伦理/启动会进度)
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.deps import get_db_session, require_study_member
|
||||
from app.models.milestone import Milestone
|
||||
from app.models.task import Task
|
||||
from app.schemas.progress import StudyProgressRead
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/progress", response_model=StudyProgressRead, dependencies=[Depends(require_study_member())])
|
||||
async def get_progress(
|
||||
study_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> StudyProgressRead:
|
||||
# Milestones
|
||||
milestone_total_stmt = select(func.count()).select_from(Milestone).where(Milestone.study_id == study_id)
|
||||
milestone_done_stmt = select(func.count()).select_from(Milestone).where(
|
||||
Milestone.study_id == study_id, Milestone.status == "DONE"
|
||||
)
|
||||
task_total_stmt = select(func.count()).select_from(Task).where(Task.study_id == study_id)
|
||||
task_done_stmt = select(func.count()).select_from(Task).where(Task.study_id == study_id, Task.status == "DONE")
|
||||
task_overdue_stmt = select(func.count()).select_from(Task).where(
|
||||
Task.study_id == study_id,
|
||||
Task.due_date < date.today(),
|
||||
Task.status != "DONE",
|
||||
)
|
||||
|
||||
milestone_total = (await db.execute(milestone_total_stmt)).scalar_one()
|
||||
milestone_done = (await db.execute(milestone_done_stmt)).scalar_one()
|
||||
task_total = (await db.execute(task_total_stmt)).scalar_one()
|
||||
task_done = (await db.execute(task_done_stmt)).scalar_one()
|
||||
task_overdue = (await db.execute(task_overdue_stmt)).scalar_one()
|
||||
|
||||
total_items = milestone_total + task_total
|
||||
done_items = milestone_done + task_done
|
||||
completion_rate = float(done_items / total_items) if total_items else 0.0
|
||||
|
||||
return StudyProgressRead(
|
||||
milestones_total=milestone_total,
|
||||
milestones_done=milestone_done,
|
||||
tasks_total=task_total,
|
||||
tasks_done=task_done,
|
||||
tasks_overdue=task_overdue,
|
||||
completion_rate=completion_rate,
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
import uuid
|
||||
|
||||
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 milestone as milestone_crud
|
||||
from app.crud import study as study_crud
|
||||
from app.schemas.milestone import MilestoneCreate, MilestoneRead, MilestoneUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||
study = await study_crud.get(db, study_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found")
|
||||
return study
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=MilestoneRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
)
|
||||
async def create_milestone(
|
||||
study_id: uuid.UUID,
|
||||
milestone_in: MilestoneCreate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> MilestoneRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
milestone = await milestone_crud.create(db, study_id, milestone_in)
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
entity_type="milestone",
|
||||
entity_id=milestone.id,
|
||||
action="CREATE_MILESTONE",
|
||||
detail=f"Milestone {milestone.id} created",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
return milestone
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[MilestoneRead],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def list_milestones(
|
||||
study_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> list[MilestoneRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
milestones = await milestone_crud.list_milestones(db, study_id)
|
||||
return list(milestones)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{milestone_id}",
|
||||
response_model=MilestoneRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
)
|
||||
async def update_milestone(
|
||||
study_id: uuid.UUID,
|
||||
milestone_id: uuid.UUID,
|
||||
milestone_in: MilestoneUpdate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> MilestoneRead:
|
||||
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")
|
||||
old_status = milestone.status
|
||||
updated = await milestone_crud.update(db, milestone, milestone_in)
|
||||
detail = None
|
||||
if milestone_in.status and milestone_in.status != old_status:
|
||||
detail = f"milestone {milestone_id} status {old_status} -> {milestone_in.status}"
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
entity_type="milestone",
|
||||
entity_id=milestone_id,
|
||||
action="UPDATE_MILESTONE",
|
||||
detail=detail or "Milestone updated",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
return updated
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs
|
||||
from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
@@ -11,3 +11,6 @@ api_router.include_router(members.router, prefix="/studies/{study_id}/members",
|
||||
api_router.include_router(comments.router, prefix="/studies/{study_id}/{entity_type}/{entity_id}/comments", tags=["comments"])
|
||||
api_router.include_router(attachments.router, prefix="/studies/{study_id}/{entity_type}/{entity_id}/attachments", tags=["attachments"])
|
||||
api_router.include_router(audit_logs.router, prefix="/studies/{study_id}/audit-logs", tags=["audit-logs"])
|
||||
api_router.include_router(milestones.router, prefix="/studies/{study_id}/milestones", tags=["milestones"])
|
||||
api_router.include_router(tasks.router, prefix="/studies/{study_id}/tasks", tags=["tasks"])
|
||||
api_router.include_router(dashboard.router, prefix="/studies/{study_id}/dashboard", tags=["dashboard"])
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import uuid
|
||||
|
||||
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 milestone as milestone_crud
|
||||
from app.crud import study as study_crud
|
||||
from app.crud import task as task_crud
|
||||
from app.schemas.task import TaskCreate, TaskRead, TaskUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
ALLOW_ASSIGNEE_UPDATE_STATUS = False
|
||||
|
||||
|
||||
async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID):
|
||||
study = await study_crud.get(db, study_id)
|
||||
if not study:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found")
|
||||
return study
|
||||
|
||||
|
||||
async def _validate_milestone(db: AsyncSession, study_id: uuid.UUID, milestone_id: uuid.UUID | None):
|
||||
if milestone_id is None:
|
||||
return
|
||||
milestone = await milestone_crud.get(db, milestone_id)
|
||||
if not milestone:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Milestone not found")
|
||||
if milestone.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Milestone does not belong to study")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=TaskRead,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
)
|
||||
async def create_task(
|
||||
study_id: uuid.UUID,
|
||||
task_in: TaskCreate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> TaskRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
await _validate_milestone(db, study_id, task_in.milestone_id)
|
||||
task = await task_crud.create(db, study_id, task_in, created_by=current_user.id)
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
entity_type="task",
|
||||
entity_id=task.id,
|
||||
action="CREATE_TASK",
|
||||
detail=f"Task {task.id} created",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
return task
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=list[TaskRead],
|
||||
dependencies=[Depends(require_study_member())],
|
||||
)
|
||||
async def list_tasks(
|
||||
study_id: uuid.UUID,
|
||||
milestone_id: uuid.UUID | None = None,
|
||||
assignee_id: uuid.UUID | None = None,
|
||||
status: str | None = None,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> list[TaskRead]:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
tasks = await task_crud.list_tasks(db, study_id, milestone_id=milestone_id, assignee_id=assignee_id, status=status)
|
||||
return list(tasks)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{task_id}",
|
||||
response_model=TaskRead,
|
||||
dependencies=[Depends(require_study_roles(["PM"]))],
|
||||
)
|
||||
async def update_task(
|
||||
study_id: uuid.UUID,
|
||||
task_id: uuid.UUID,
|
||||
task_in: TaskUpdate,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(get_current_user),
|
||||
) -> TaskRead:
|
||||
await _ensure_study_exists(db, study_id)
|
||||
task = await task_crud.get(db, task_id)
|
||||
if not task or task.study_id != study_id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
|
||||
if task_in.milestone_id:
|
||||
await _validate_milestone(db, study_id, task_in.milestone_id)
|
||||
|
||||
old_status = task.status
|
||||
updated = await task_crud.update(db, task, task_in)
|
||||
detail = None
|
||||
if task_in.status and task_in.status != old_status:
|
||||
detail = f"task {task_id} status {old_status} -> {task_in.status}"
|
||||
await audit_crud.log_action(
|
||||
db,
|
||||
study_id=study_id,
|
||||
entity_type="task",
|
||||
entity_id=task_id,
|
||||
action="UPDATE_TASK" if not detail else "TASK_STATUS_CHANGE",
|
||||
detail=detail or "Task updated",
|
||||
operator_id=current_user.id,
|
||||
operator_role=current_user.role,
|
||||
)
|
||||
return updated
|
||||
@@ -0,0 +1,48 @@
|
||||
import uuid
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, update as sa_update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.milestone import Milestone
|
||||
from app.schemas.milestone import MilestoneCreate, MilestoneUpdate
|
||||
|
||||
|
||||
async def create(db: AsyncSession, study_id: uuid.UUID, milestone_in: MilestoneCreate) -> Milestone:
|
||||
milestone = Milestone(
|
||||
study_id=study_id,
|
||||
type=milestone_in.type,
|
||||
name=milestone_in.name or milestone_in.type,
|
||||
planned_date=milestone_in.planned_date,
|
||||
actual_date=None,
|
||||
status=milestone_in.status or "NOT_STARTED",
|
||||
owner_id=milestone_in.owner_id,
|
||||
notes=milestone_in.notes,
|
||||
)
|
||||
db.add(milestone)
|
||||
await db.commit()
|
||||
await db.refresh(milestone)
|
||||
return milestone
|
||||
|
||||
|
||||
async def get(db: AsyncSession, milestone_id: uuid.UUID) -> Milestone | None:
|
||||
result = await db.execute(select(Milestone).where(Milestone.id == milestone_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_milestones(db: AsyncSession, study_id: uuid.UUID) -> Sequence[Milestone]:
|
||||
result = await db.execute(select(Milestone).where(Milestone.study_id == study_id).order_by(Milestone.planned_date))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def update(db: AsyncSession, milestone: Milestone, milestone_in: MilestoneUpdate) -> Milestone:
|
||||
update_data = milestone_in.model_dump(exclude_unset=True)
|
||||
if update_data:
|
||||
await db.execute(
|
||||
sa_update(Milestone)
|
||||
.where(Milestone.id == milestone.id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(milestone)
|
||||
return milestone
|
||||
@@ -0,0 +1,77 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, update as sa_update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.task import Task
|
||||
from app.schemas.task import TaskCreate, TaskUpdate
|
||||
|
||||
|
||||
async def create(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
task_in: TaskCreate,
|
||||
*,
|
||||
created_by: uuid.UUID,
|
||||
) -> Task:
|
||||
task = Task(
|
||||
study_id=study_id,
|
||||
milestone_id=task_in.milestone_id,
|
||||
title=task_in.title,
|
||||
description=task_in.description,
|
||||
assignee_id=task_in.assignee_id,
|
||||
priority=task_in.priority,
|
||||
due_date=task_in.due_date,
|
||||
status="TODO",
|
||||
created_by=created_by,
|
||||
)
|
||||
db.add(task)
|
||||
await db.commit()
|
||||
await db.refresh(task)
|
||||
return task
|
||||
|
||||
|
||||
async def get(db: AsyncSession, task_id: uuid.UUID) -> Task | None:
|
||||
result = await db.execute(select(Task).where(Task.id == task_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_tasks(
|
||||
db: AsyncSession,
|
||||
study_id: uuid.UUID,
|
||||
milestone_id: uuid.UUID | None = None,
|
||||
assignee_id: uuid.UUID | None = None,
|
||||
status: str | None = None,
|
||||
) -> Sequence[Task]:
|
||||
stmt = select(Task).where(Task.study_id == study_id)
|
||||
if milestone_id:
|
||||
stmt = stmt.where(Task.milestone_id == milestone_id)
|
||||
if assignee_id:
|
||||
stmt = stmt.where(Task.assignee_id == assignee_id)
|
||||
if status:
|
||||
stmt = stmt.where(Task.status == status)
|
||||
stmt = stmt.order_by(Task.due_date)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def update(db: AsyncSession, task: Task, task_in: TaskUpdate) -> Task:
|
||||
update_data = task_in.model_dump(exclude_unset=True)
|
||||
if "status" in update_data:
|
||||
status_change = update_data["status"]
|
||||
if status_change == "DONE":
|
||||
update_data["completed_at"] = datetime.now(timezone.utc)
|
||||
else:
|
||||
update_data["completed_at"] = None
|
||||
|
||||
if update_data:
|
||||
await db.execute(
|
||||
sa_update(Task)
|
||||
.where(Task.id == task.id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.commit()
|
||||
await db.refresh(task)
|
||||
return task
|
||||
@@ -8,3 +8,5 @@ from app.models.study_member import StudyMember # noqa: F401
|
||||
from app.models.comment import Comment # noqa: F401
|
||||
from app.models.attachment import Attachment # noqa: F401
|
||||
from app.models.audit_log import AuditLog # noqa: F401
|
||||
from app.models.milestone import Milestone # noqa: F401
|
||||
from app.models.task import Task # noqa: F401
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import uuid
|
||||
from datetime import datetime, date
|
||||
|
||||
from sqlalchemy import Date, DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class Milestone(Base):
|
||||
__tablename__ = "milestones"
|
||||
|
||||
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"), index=True, nullable=False)
|
||||
type: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
planned_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
actual_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="NOT_STARTED")
|
||||
owner_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=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()
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
import uuid
|
||||
from datetime import datetime, date
|
||||
|
||||
from sqlalchemy import Date, DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class Task(Base):
|
||||
__tablename__ = "tasks"
|
||||
|
||||
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"), index=True, nullable=False)
|
||||
milestone_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("milestones.id"), nullable=True)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
assignee_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
|
||||
priority: Mapped[str] = mapped_column(String(20), nullable=False, default="MEDIUM")
|
||||
due_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="TODO")
|
||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), 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())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
import uuid
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class MilestoneCreate(BaseModel):
|
||||
type: str
|
||||
name: Optional[str] = None
|
||||
planned_date: Optional[date] = None
|
||||
owner_id: Optional[uuid.UUID] = None
|
||||
notes: Optional[str] = None
|
||||
status: str = Field(default="NOT_STARTED")
|
||||
|
||||
|
||||
class MilestoneUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
planned_date: Optional[date] = None
|
||||
actual_date: Optional[date] = None
|
||||
status: Optional[str] = None
|
||||
owner_id: Optional[uuid.UUID] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class MilestoneRead(BaseModel):
|
||||
id: uuid.UUID
|
||||
study_id: uuid.UUID
|
||||
type: str
|
||||
name: str
|
||||
planned_date: Optional[date]
|
||||
actual_date: Optional[date]
|
||||
status: str
|
||||
owner_id: Optional[uuid.UUID]
|
||||
notes: Optional[str]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class StudyProgressRead(BaseModel):
|
||||
milestones_total: int
|
||||
milestones_done: int
|
||||
tasks_total: int
|
||||
tasks_done: int
|
||||
tasks_overdue: int
|
||||
completion_rate: float
|
||||
@@ -0,0 +1,42 @@
|
||||
import uuid
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class TaskCreate(BaseModel):
|
||||
milestone_id: Optional[uuid.UUID] = None
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
assignee_id: Optional[uuid.UUID] = None
|
||||
priority: str = "MEDIUM"
|
||||
due_date: Optional[date] = None
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
milestone_id: Optional[uuid.UUID] = None
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
assignee_id: Optional[uuid.UUID] = None
|
||||
priority: Optional[str] = None
|
||||
due_date: Optional[date] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class TaskRead(BaseModel):
|
||||
id: uuid.UUID
|
||||
study_id: uuid.UUID
|
||||
milestone_id: Optional[uuid.UUID]
|
||||
title: str
|
||||
description: Optional[str]
|
||||
assignee_id: Optional[uuid.UUID]
|
||||
priority: str
|
||||
due_date: Optional[date]
|
||||
status: str
|
||||
completed_at: Optional[datetime]
|
||||
created_by: uuid.UUID
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user