feat: paginated list responses, issue sorting + filtering by assignee/tag
This commit is contained in:
11
README.md
11
README.md
@@ -9,8 +9,17 @@ Agent/人类协同任务管理平台 - FastAPI 后端
|
||||
- `GET /auth/me` - 获取当前用户信息
|
||||
|
||||
### Issues
|
||||
|
||||
> Issues 和 Search 列表接口返回分页格式:`{items, total, page, page_size, total_pages}`
|
||||
> Issues 支持排序参数:`sort_by` (created_at/priority/title/due_date/status), `sort_order` (asc/desc)
|
||||
> Issues 支持额外过滤:`assignee_id`, `tag`
|
||||
|
||||
|
||||
> Issues 和 Search 列表接口返回分页格式:
|
||||
> Issues 支持排序参数: (created_at/priority/title/due_date/status), (asc/desc)
|
||||
> Issues 支持额外过滤:,
|
||||
- `POST /issues` - 创建 issue(支持 resolution 决议案类型)
|
||||
- `GET /issues` - 列表(支持按 project/status/type 过滤)
|
||||
- `GET /issues` - 列表(分页、排序、按 assignee/tag 过滤)(支持按 project/status/type 过滤)
|
||||
- `GET /issues/{id}` - 详情
|
||||
- `PATCH /issues/{id}` - 更新
|
||||
- `DELETE /issues/{id}` - 删除
|
||||
|
||||
72
app/main.py
72
app/main.py
@@ -110,15 +110,22 @@ def create_issue(issue: schemas.IssueCreate, bg: BackgroundTasks, db: Session =
|
||||
return db_issue
|
||||
|
||||
|
||||
@app.get("/issues", response_model=List[schemas.IssueResponse])
|
||||
import math
|
||||
|
||||
@app.get("/issues")
|
||||
def list_issues(
|
||||
project_id: int = None,
|
||||
issue_status: str = None,
|
||||
issue_type: str = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
assignee_id: int = None,
|
||||
tag: str = None,
|
||||
sort_by: str = "created_at",
|
||||
sort_order: str = "desc",
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""List issues with filtering, sorting, and pagination metadata."""
|
||||
query = db.query(models.Issue)
|
||||
|
||||
if project_id:
|
||||
@@ -127,9 +134,40 @@ def list_issues(
|
||||
query = query.filter(models.Issue.status == issue_status)
|
||||
if issue_type:
|
||||
query = query.filter(models.Issue.issue_type == issue_type)
|
||||
if assignee_id:
|
||||
query = query.filter(models.Issue.assignee_id == assignee_id)
|
||||
if tag:
|
||||
query = query.filter(models.Issue.tags.contains(tag))
|
||||
|
||||
issues = query.offset(skip).limit(limit).all()
|
||||
return issues
|
||||
# Sorting
|
||||
sort_fields = {
|
||||
"created_at": models.Issue.created_at,
|
||||
"updated_at": models.Issue.updated_at,
|
||||
"priority": models.Issue.priority,
|
||||
"title": models.Issue.title,
|
||||
"due_date": models.Issue.due_date,
|
||||
"status": models.Issue.status,
|
||||
}
|
||||
sort_col = sort_fields.get(sort_by, models.Issue.created_at)
|
||||
if sort_order == "asc":
|
||||
query = query.order_by(sort_col.asc())
|
||||
else:
|
||||
query = query.order_by(sort_col.desc())
|
||||
|
||||
total = query.count()
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 200)
|
||||
total_pages = math.ceil(total / page_size) if total else 1
|
||||
skip = (page - 1) * page_size
|
||||
items = query.offset(skip).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"items": [schemas.IssueResponse.model_validate(i) for i in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": total_pages,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/issues/overdue", response_model=List[schemas.IssueResponse])
|
||||
@@ -462,21 +500,35 @@ def transition_issue(issue_id: int, new_status: str, bg: BackgroundTasks, db: Se
|
||||
|
||||
# ============ Search API ============
|
||||
|
||||
@app.get("/search/issues", response_model=List[schemas.IssueResponse])
|
||||
@app.get("/search/issues")
|
||||
def search_issues(
|
||||
q: str,
|
||||
project_id: int = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Search issues by title or description keyword."""
|
||||
"""Search issues by title or description keyword with pagination."""
|
||||
query = db.query(models.Issue).filter(
|
||||
(models.Issue.title.contains(q)) | (models.Issue.description.contains(q))
|
||||
)
|
||||
if project_id:
|
||||
query = query.filter(models.Issue.project_id == project_id)
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
total = query.count()
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 200)
|
||||
total_pages = math.ceil(total / page_size) if total else 1
|
||||
skip = (page - 1) * page_size
|
||||
items = query.offset(skip).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"items": [schemas.IssueResponse.model_validate(i) for i in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": total_pages,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -198,3 +198,15 @@ class MilestoneResponse(MilestoneBase):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Paginated response
|
||||
from typing import Generic, TypeVar
|
||||
T = TypeVar("T")
|
||||
|
||||
class PaginatedResponse(BaseModel, Generic[T]):
|
||||
items: List[T]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
|
||||
Reference in New Issue
Block a user