feat: enrich member/comment/propose APIs with usernames
- ProjectMemberResponse now includes username and full_name - Comment list endpoint returns author_username - ProposeResponse now includes created_by_username - All serializers resolve User objects to surface human-readable names - Supports frontend code-first migration (TODO §3.1/3.2)
This commit is contained in:
@@ -50,9 +50,22 @@ def create_comment(comment: schemas.CommentCreate, db: Session = Depends(get_db)
|
|||||||
return db_comment
|
return db_comment
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tasks/{task_id}/comments", response_model=List[schemas.CommentResponse])
|
@router.get("/tasks/{task_id}/comments")
|
||||||
def list_comments(task_id: int, db: Session = Depends(get_db)):
|
def list_comments(task_id: int, db: Session = Depends(get_db)):
|
||||||
return db.query(models.Comment).filter(models.Comment.task_id == task_id).all()
|
comments = db.query(models.Comment).filter(models.Comment.task_id == task_id).all()
|
||||||
|
result = []
|
||||||
|
for c in comments:
|
||||||
|
author = db.query(models.User).filter(models.User.id == c.author_id).first()
|
||||||
|
result.append({
|
||||||
|
"id": c.id,
|
||||||
|
"content": c.content,
|
||||||
|
"task_id": c.task_id,
|
||||||
|
"author_id": c.author_id,
|
||||||
|
"author_username": author.username if author else None,
|
||||||
|
"created_at": c.created_at,
|
||||||
|
"updated_at": c.updated_at,
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/comments/{comment_id}", response_model=schemas.CommentResponse)
|
@router.patch("/comments/{comment_id}", response_model=schemas.CommentResponse)
|
||||||
|
|||||||
@@ -332,9 +332,12 @@ def list_project_members(project_id: str, db: Session = Depends(get_db)):
|
|||||||
role = db.query(Role).filter(Role.id == m.role_id).first()
|
role = db.query(Role).filter(Role.id == m.role_id).first()
|
||||||
if role:
|
if role:
|
||||||
role_name = role.name
|
role_name = role.name
|
||||||
|
user = db.query(models.User).filter(models.User.id == m.user_id).first()
|
||||||
result.append({
|
result.append({
|
||||||
"id": m.id,
|
"id": m.id,
|
||||||
"user_id": m.user_id,
|
"user_id": m.user_id,
|
||||||
|
"username": user.username if user else None,
|
||||||
|
"full_name": user.full_name if user else None,
|
||||||
"project_id": m.project_id,
|
"project_id": m.project_id,
|
||||||
"role": role_name
|
"role": role_name
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,24 @@ from app.services.activity import log_activity
|
|||||||
router = APIRouter(prefix="/projects/{project_id}/proposes", tags=["Proposes"])
|
router = APIRouter(prefix="/projects/{project_id}/proposes", tags=["Proposes"])
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_propose(db: Session, propose: Propose) -> dict:
|
||||||
|
"""Serialize propose with created_by_username."""
|
||||||
|
creator = db.query(models.User).filter(models.User.id == propose.created_by_id).first() if propose.created_by_id else None
|
||||||
|
return {
|
||||||
|
"id": propose.id,
|
||||||
|
"title": propose.title,
|
||||||
|
"description": propose.description,
|
||||||
|
"propose_code": propose.propose_code,
|
||||||
|
"status": propose.status.value if hasattr(propose.status, "value") else propose.status,
|
||||||
|
"project_id": propose.project_id,
|
||||||
|
"created_by_id": propose.created_by_id,
|
||||||
|
"created_by_username": creator.username if creator else None,
|
||||||
|
"feat_task_id": propose.feat_task_id,
|
||||||
|
"created_at": propose.created_at,
|
||||||
|
"updated_at": propose.updated_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _find_project(db, identifier):
|
def _find_project(db, identifier):
|
||||||
"""Look up project by numeric id or project_code."""
|
"""Look up project by numeric id or project_code."""
|
||||||
try:
|
try:
|
||||||
@@ -92,7 +110,7 @@ def list_proposes(
|
|||||||
.order_by(Propose.id.desc())
|
.order_by(Propose.id.desc())
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
return proposes
|
return [_serialize_propose(db, p) for p in proposes]
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=schemas.ProposeResponse, status_code=status.HTTP_201_CREATED)
|
@router.post("", response_model=schemas.ProposeResponse, status_code=status.HTTP_201_CREATED)
|
||||||
@@ -123,7 +141,7 @@ def create_propose(
|
|||||||
|
|
||||||
log_activity(db, "create", "propose", propose.id, user_id=current_user.id, details={"title": propose.title})
|
log_activity(db, "create", "propose", propose.id, user_id=current_user.id, details={"title": propose.title})
|
||||||
|
|
||||||
return propose
|
return _serialize_propose(db, propose)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{propose_id}", response_model=schemas.ProposeResponse)
|
@router.get("/{propose_id}", response_model=schemas.ProposeResponse)
|
||||||
@@ -140,7 +158,7 @@ def get_propose(
|
|||||||
propose = _find_propose(db, propose_id, project.id)
|
propose = _find_propose(db, propose_id, project.id)
|
||||||
if not propose:
|
if not propose:
|
||||||
raise HTTPException(status_code=404, detail="Propose not found")
|
raise HTTPException(status_code=404, detail="Propose not found")
|
||||||
return propose
|
return _serialize_propose(db, propose)
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{propose_id}", response_model=schemas.ProposeResponse)
|
@router.patch("/{propose_id}", response_model=schemas.ProposeResponse)
|
||||||
@@ -177,7 +195,7 @@ def update_propose(
|
|||||||
|
|
||||||
log_activity(db, "update", "propose", propose.id, user_id=current_user.id, details=data)
|
log_activity(db, "update", "propose", propose.id, user_id=current_user.id, details=data)
|
||||||
|
|
||||||
return propose
|
return _serialize_propose(db, propose)
|
||||||
|
|
||||||
|
|
||||||
# ---- Actions ----
|
# ---- Actions ----
|
||||||
@@ -256,7 +274,7 @@ def accept_propose(
|
|||||||
"task_code": task_code,
|
"task_code": task_code,
|
||||||
})
|
})
|
||||||
|
|
||||||
return propose
|
return _serialize_propose(db, propose)
|
||||||
|
|
||||||
|
|
||||||
class RejectRequest(schemas.BaseModel):
|
class RejectRequest(schemas.BaseModel):
|
||||||
@@ -293,7 +311,7 @@ def reject_propose(
|
|||||||
"reason": body.reason if body else None,
|
"reason": body.reason if body else None,
|
||||||
})
|
})
|
||||||
|
|
||||||
return propose
|
return _serialize_propose(db, propose)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{propose_id}/reopen", response_model=schemas.ProposeResponse)
|
@router.post("/{propose_id}/reopen", response_model=schemas.ProposeResponse)
|
||||||
@@ -323,4 +341,4 @@ def reopen_propose(
|
|||||||
|
|
||||||
log_activity(db, "reopen", "propose", propose.id, user_id=current_user.id)
|
log_activity(db, "reopen", "propose", propose.id, user_id=current_user.id)
|
||||||
|
|
||||||
return propose
|
return _serialize_propose(db, propose)
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ class ProjectMemberCreate(ProjectMemberBase):
|
|||||||
class ProjectMemberResponse(BaseModel):
|
class ProjectMemberResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
user_id: int
|
user_id: int
|
||||||
|
username: Optional[str] = None
|
||||||
|
full_name: Optional[str] = None
|
||||||
project_id: int
|
project_id: int
|
||||||
role: str = "dev"
|
role: str = "dev"
|
||||||
|
|
||||||
@@ -290,6 +292,7 @@ class ProposeResponse(ProposeBase):
|
|||||||
status: ProposeStatusEnum
|
status: ProposeStatusEnum
|
||||||
project_id: int
|
project_id: int
|
||||||
created_by_id: Optional[int] = None
|
created_by_id: Optional[int] = None
|
||||||
|
created_by_username: Optional[str] = None
|
||||||
feat_task_id: Optional[str] = None
|
feat_task_id: Optional[str] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
|
|||||||
Reference in New Issue
Block a user