gateway/modules/workflows/methods/methodRedmine/actions/listTickets.py
2026-04-21 21:30:11 +02:00

82 lines
2.8 KiB
Python

# Copyright (c) 2026 Patrick Motsch
# All rights reserved.
"""Workflow action: list Redmine tickets from the mirror with filters."""
import logging
from typing import Any, Dict, List, Optional
from modules.datamodels.datamodelChat import ActionResult
from modules.features.redmine.serviceRedmine import listTickets
from ._shared import resolveInstanceContext, ticketToDict
logger = logging.getLogger(__name__)
def _normalizeTrackerIds(value: Any) -> Optional[List[int]]:
if value is None or value == "":
return None
if isinstance(value, int):
return [value]
if isinstance(value, str):
value = [v.strip() for v in value.split(",") if v.strip()]
if isinstance(value, list):
ids: List[int] = []
for v in value:
try:
ids.append(int(v))
except (TypeError, ValueError):
continue
return ids or None
return None
async def listTicketsAction(self, parameters: Dict[str, Any]) -> ActionResult:
"""List Redmine tickets from the local mirror."""
try:
user, mandateId, featureInstanceId = resolveInstanceContext(self.services, parameters)
except ValueError as exc:
return ActionResult.isFailure(error=str(exc))
trackerIds = _normalizeTrackerIds(parameters.get("trackerIds"))
statusFilter = (parameters.get("status") or "*").lower()
if statusFilter not in {"*", "open", "closed"}:
statusFilter = "*"
updatedFrom = parameters.get("dateFrom") or None
updatedTo = parameters.get("dateTo") or None
assignedToId: Optional[int] = None
if parameters.get("assignedToId") not in (None, ""):
try:
assignedToId = int(parameters["assignedToId"])
except (TypeError, ValueError):
return ActionResult.isFailure(error="assignedToId must be an int")
try:
tickets = listTickets(
user, mandateId, featureInstanceId,
trackerIds=trackerIds,
statusFilter=statusFilter,
updatedOnFrom=updatedFrom,
updatedOnTo=updatedTo,
assignedToId=assignedToId,
)
except Exception as exc:
logger.exception("redmine.listTickets failed")
return ActionResult.isFailure(error=f"List tickets failed: {exc}")
# AI-friendly pagination: always capped so we don't accidentally feed a
# 20k-ticket dump into a context window. Callers that need more must
# paginate via filters.
limit = 100
try:
limit = max(1, min(500, int(parameters.get("limit") or 100)))
except (TypeError, ValueError):
limit = 100
truncated = tickets[:limit]
return ActionResult.isSuccess(data={
"count": len(truncated),
"totalMatched": len(tickets),
"truncated": len(tickets) > limit,
"tickets": [ticketToDict(t) for t in truncated],
})