feat: paginated list responses, issue sorting + filtering by assignee/tag
This commit is contained in:
76
app/main.py
76
app/main.py
@@ -110,26 +110,64 @@ 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:
|
||||
query = query.filter(models.Issue.project_id == project_id)
|
||||
if issue_status:
|
||||
query = query.filter(models.Issue.status == issue_status)
|
||||
if issue_type:
|
||||
query = query.filter(models.Issue.issue_type == issue_type)
|
||||
|
||||
issues = query.offset(skip).limit(limit).all()
|
||||
return issues
|
||||
if assignee_id:
|
||||
query = query.filter(models.Issue.assignee_id == assignee_id)
|
||||
if tag:
|
||||
query = query.filter(models.Issue.tags.contains(tag))
|
||||
|
||||
# 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