113 lines
4.1 KiB
Python
113 lines
4.1 KiB
Python
|
|
"""API endpoints for Forgejo issue operations."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import TYPE_CHECKING
|
||
|
|
from uuid import UUID
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||
|
|
from sqlmodel import select, func
|
||
|
|
|
||
|
|
from app.api.deps import require_org_admin
|
||
|
|
from app.core.auth import get_auth_context
|
||
|
|
from app.db import crud
|
||
|
|
from app.db.session import get_session
|
||
|
|
from app.models.forgejo_issues import ForgejoIssue
|
||
|
|
from app.schemas.forgejo_issues import ForgejoIssueListResponse, ForgejoIssueRead
|
||
|
|
from app.services.organizations import OrganizationContext
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||
|
|
|
||
|
|
|
||
|
|
router = APIRouter(prefix="/forgejo/issues", tags=["forgejo-issues"])
|
||
|
|
SESSION_DEP = Depends(get_session)
|
||
|
|
AUTH_DEP = Depends(get_auth_context)
|
||
|
|
ORG_ADMIN_DEP = Depends(require_org_admin)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("", response_model=ForgejoIssueListResponse)
|
||
|
|
async def list_issues(
|
||
|
|
session: AsyncSession = SESSION_DEP,
|
||
|
|
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
||
|
|
repository_id: str | None = Query(None, description="Filter by repository ID"),
|
||
|
|
state: str | None = Query(None, description="Filter by state (open, closed)"),
|
||
|
|
label: str | None = Query(None, description="Filter by label name"),
|
||
|
|
assignee: str | None = Query(None, description="Filter by assignee login"),
|
||
|
|
search: str | None = Query(None, description="Search in title and body"),
|
||
|
|
page: int = Query(1, ge=1, description="Page number"),
|
||
|
|
limit: int = Query(30, ge=1, le=100, description="Items per page"),
|
||
|
|
) -> ForgejoIssueListResponse:
|
||
|
|
"""List cached issues with optional filters."""
|
||
|
|
# Build query with filters
|
||
|
|
statement = select(ForgejoIssue).where(ForgejoIssue.organization_id == ctx.organization.id)
|
||
|
|
|
||
|
|
if repository_id:
|
||
|
|
try:
|
||
|
|
repo_uuid = UUID(repository_id)
|
||
|
|
except ValueError:
|
||
|
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Invalid repository_id format")
|
||
|
|
statement = statement.where(ForgejoIssue.repository_id == repo_uuid)
|
||
|
|
|
||
|
|
if state:
|
||
|
|
statement = statement.where(ForgejoIssue.state == state)
|
||
|
|
|
||
|
|
if search:
|
||
|
|
statement = statement.where(
|
||
|
|
(ForgejoIssue.title.ilike(f"%{search}%")) |
|
||
|
|
(ForgejoIssue.body_preview.ilike(f"%{search}%"))
|
||
|
|
)
|
||
|
|
|
||
|
|
# Count total
|
||
|
|
total_statement = select(func.count()).select_from(ForgejoIssue).where(ForgejoIssue.organization_id == ctx.organization.id)
|
||
|
|
if repository_id:
|
||
|
|
try:
|
||
|
|
repo_uuid = UUID(repository_id)
|
||
|
|
total_statement = total_statement.where(ForgejoIssue.repository_id == repo_uuid)
|
||
|
|
except ValueError:
|
||
|
|
pass
|
||
|
|
if state:
|
||
|
|
total_statement = total_statement.where(ForgejoIssue.state == state)
|
||
|
|
if search:
|
||
|
|
total_statement = total_statement.where(
|
||
|
|
(ForgejoIssue.title.ilike(f"%{search}%")) |
|
||
|
|
(ForgejoIssue.body_preview.ilike(f"%{search}%"))
|
||
|
|
)
|
||
|
|
total_result = await session.exec(total_statement)
|
||
|
|
total = total_result.one()
|
||
|
|
|
||
|
|
# Pagination
|
||
|
|
offset = (page - 1) * limit
|
||
|
|
statement = statement.offset(offset).limit(limit).order_by(ForgejoIssue.forgejo_issue_number.desc())
|
||
|
|
|
||
|
|
issues = (await session.exec(statement)).all()
|
||
|
|
items = [ForgejoIssueRead.model_validate(issue) for issue in issues]
|
||
|
|
|
||
|
|
return ForgejoIssueListResponse(
|
||
|
|
items=items,
|
||
|
|
total=total,
|
||
|
|
page=page,
|
||
|
|
limit=limit,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{issue_id}", response_model=ForgejoIssueRead)
|
||
|
|
async def get_issue(
|
||
|
|
issue_id: str,
|
||
|
|
session: AsyncSession = SESSION_DEP,
|
||
|
|
ctx: OrganizationContext = ORG_ADMIN_DEP,
|
||
|
|
) -> ForgejoIssueRead:
|
||
|
|
"""Get one cached issue by ID."""
|
||
|
|
try:
|
||
|
|
uuid = UUID(issue_id)
|
||
|
|
except ValueError:
|
||
|
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Invalid issue_id format")
|
||
|
|
|
||
|
|
issue = await crud.get_by_id(session, ForgejoIssue, uuid)
|
||
|
|
if issue is None:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||
|
|
if issue.organization_id != ctx.organization.id:
|
||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||
|
|
|
||
|
|
return ForgejoIssueRead.model_validate(issue)
|