gateway/modules/workflows/methods/methodRedmine/methodRedmine.py
2026-04-25 01:13:01 +02:00

259 lines
14 KiB
Python

# Copyright (c) 2026 Patrick Motsch
# All rights reserved.
"""Redmine workflow method.
Exposes read/write/stats/sync actions against a configured Redmine
feature instance. All reads go through the local mirror; writes update
Redmine and then the mirror (see ``serviceRedmine``).
This module is auto-discovered by ``methodDiscovery.py`` (any package
under ``modules.workflows.methods.method*`` with a ``MethodBase``
subclass is picked up). No manual registration needed.
"""
import logging
from modules.datamodels.datamodelWorkflowActions import (
WorkflowActionDefinition,
WorkflowActionParameter,
)
from modules.shared.frontendTypes import FrontendType
from modules.workflows.methods.methodBase import MethodBase
from .actions.createTicket import createTicketAction
from .actions.getStats import getStatsAction
from .actions.listTickets import listTicketsAction
from .actions.readTicket import readTicket
from .actions.runSync import runSyncAction
from .actions.updateTicket import updateTicketAction
logger = logging.getLogger(__name__)
class MethodRedmine(MethodBase):
"""Redmine read/write/stats/sync actions for the workflow runtime."""
def __init__(self, services):
super().__init__(services)
self.name = "redmine"
self.description = "Redmine ticketing: read, list, create, update, stats, sync."
self._actions = {
"readTicket": WorkflowActionDefinition(
actionId="redmine.readTicket",
description="Read a single Redmine ticket from the local mirror by ticketId.",
dynamicMode=False,
outputType="RedmineTicket",
parameters={
"featureInstanceId": WorkflowActionParameter(
name="featureInstanceId", type="FeatureInstanceRef", frontendType=FrontendType.TEXT,
required=True, description="Redmine feature instance",
),
"ticketId": WorkflowActionParameter(
name="ticketId", type="int", frontendType=FrontendType.TEXT,
required=True, description="Redmine issue id to read",
),
},
execute=readTicket.__get__(self, self.__class__),
),
"listTickets": WorkflowActionDefinition(
actionId="redmine.listTickets",
description="List tickets from the mirror with optional filters (tracker, status, period, assignee).",
dynamicMode=False,
outputType="RedmineTicketList",
parameters={
"featureInstanceId": WorkflowActionParameter(
name="featureInstanceId", type="FeatureInstanceRef", frontendType=FrontendType.TEXT,
required=True, description="Redmine feature instance",
),
"trackerIds": WorkflowActionParameter(
name="trackerIds", type="List[int]", frontendType=FrontendType.JSON,
required=False, description="Restrict to these tracker ids (list of int or comma-separated string).",
),
"status": WorkflowActionParameter(
name="status", type="str", frontendType=FrontendType.TEXT,
required=False, description="'open' | 'closed' | '*' (default '*').",
),
"dateFrom": WorkflowActionParameter(
name="dateFrom", type="str", frontendType=FrontendType.TEXT,
required=False, description="ISO date -- filter by 'updated_on >= dateFrom'.",
),
"dateTo": WorkflowActionParameter(
name="dateTo", type="str", frontendType=FrontendType.TEXT,
required=False, description="ISO date -- filter by 'updated_on <= dateTo'.",
),
"assignedToId": WorkflowActionParameter(
name="assignedToId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Only tickets assigned to this Redmine user id.",
),
"limit": WorkflowActionParameter(
name="limit", type="int", frontendType=FrontendType.TEXT,
required=False, description="Max tickets in the result (1-500, default 100).",
),
},
execute=listTicketsAction.__get__(self, self.__class__),
),
"createTicket": WorkflowActionDefinition(
actionId="redmine.createTicket",
description="Create a new Redmine ticket. Requires subject and trackerId.",
dynamicMode=False,
outputType="RedmineTicket",
parameters={
"featureInstanceId": WorkflowActionParameter(
name="featureInstanceId", type="FeatureInstanceRef", frontendType=FrontendType.TEXT,
required=True, description="Redmine feature instance",
),
"subject": WorkflowActionParameter(
name="subject", type="str", frontendType=FrontendType.TEXT,
required=True, description="Ticket title.",
),
"trackerId": WorkflowActionParameter(
name="trackerId", type="int", frontendType=FrontendType.TEXT,
required=True, description="Tracker id (Userstory, Feature, Task ...).",
),
"description": WorkflowActionParameter(
name="description", type="str", uiHint="textarea", frontendType=FrontendType.TEXTAREA,
required=False, description="Markdown/Textile description body.",
),
"statusId": WorkflowActionParameter(
name="statusId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Status id (optional, Redmine default otherwise).",
),
"priorityId": WorkflowActionParameter(
name="priorityId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Priority id.",
),
"assignedToId": WorkflowActionParameter(
name="assignedToId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Assignee user id.",
),
"parentIssueId": WorkflowActionParameter(
name="parentIssueId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Parent issue id (tree parent, not relation).",
),
"fixedVersionId": WorkflowActionParameter(
name="fixedVersionId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Target/fixed version id.",
),
"customFields": WorkflowActionParameter(
name="customFields", type="Dict[str,Any]", frontendType=FrontendType.JSON,
required=False, description="Custom fields as {customFieldId: value}.",
),
},
execute=createTicketAction.__get__(self, self.__class__),
),
"updateTicket": WorkflowActionDefinition(
actionId="redmine.updateTicket",
description="Update a Redmine ticket. Only provided fields are sent.",
dynamicMode=False,
outputType="RedmineTicket",
parameters={
"featureInstanceId": WorkflowActionParameter(
name="featureInstanceId", type="FeatureInstanceRef", frontendType=FrontendType.TEXT,
required=True, description="Redmine feature instance",
),
"ticketId": WorkflowActionParameter(
name="ticketId", type="int", frontendType=FrontendType.TEXT,
required=True, description="Redmine issue id to update",
),
"subject": WorkflowActionParameter(
name="subject", type="str", frontendType=FrontendType.TEXT,
required=False, description="New title.",
),
"description": WorkflowActionParameter(
name="description", type="str", uiHint="textarea", frontendType=FrontendType.TEXTAREA,
required=False, description="New description.",
),
"trackerId": WorkflowActionParameter(
name="trackerId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Change tracker.",
),
"statusId": WorkflowActionParameter(
name="statusId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Change status.",
),
"priorityId": WorkflowActionParameter(
name="priorityId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Change priority.",
),
"assignedToId": WorkflowActionParameter(
name="assignedToId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Change assignee.",
),
"parentIssueId": WorkflowActionParameter(
name="parentIssueId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Change parent issue.",
),
"fixedVersionId": WorkflowActionParameter(
name="fixedVersionId", type="int", frontendType=FrontendType.TEXT,
required=False, description="Change fixed version.",
),
"notes": WorkflowActionParameter(
name="notes", type="str", uiHint="textarea", frontendType=FrontendType.TEXTAREA,
required=False, description="Journal entry (comment) added to the ticket.",
),
"customFields": WorkflowActionParameter(
name="customFields", type="Dict[str,Any]", frontendType=FrontendType.JSON,
required=False, description="Custom fields as {customFieldId: value}.",
),
},
execute=updateTicketAction.__get__(self, self.__class__),
),
"getStats": WorkflowActionDefinition(
actionId="redmine.getStats",
description="Aggregated stats (KPIs, throughput, status distribution, backlog) from the mirror.",
dynamicMode=False,
outputType="RedmineStats",
parameters={
"featureInstanceId": WorkflowActionParameter(
name="featureInstanceId", type="FeatureInstanceRef", frontendType=FrontendType.TEXT,
required=True, description="Redmine feature instance",
),
"dateFrom": WorkflowActionParameter(
name="dateFrom", type="str", frontendType=FrontendType.TEXT,
required=False, description="ISO date -- lower bound for 'created_in_period' / 'closed_in_period'.",
),
"dateTo": WorkflowActionParameter(
name="dateTo", type="str", frontendType=FrontendType.TEXT,
required=False, description="ISO date -- upper bound.",
),
"bucket": WorkflowActionParameter(
name="bucket", type="str", frontendType=FrontendType.TEXT,
required=False, description="'day' | 'week' | 'month' (default 'week').",
),
"trackerIds": WorkflowActionParameter(
name="trackerIds", type="List[int]", frontendType=FrontendType.JSON,
required=False, description="Restrict to these tracker ids.",
),
},
execute=getStatsAction.__get__(self, self.__class__),
),
"runSync": WorkflowActionDefinition(
actionId="redmine.runSync",
description="Sync Redmine tickets and relations into the local mirror (incremental by default).",
dynamicMode=False,
outputType="ActionResult",
parameters={
"featureInstanceId": WorkflowActionParameter(
name="featureInstanceId", type="FeatureInstanceRef", frontendType=FrontendType.TEXT,
required=True, description="Redmine feature instance",
),
"force": WorkflowActionParameter(
name="force", type="bool", frontendType=FrontendType.CHECKBOX,
required=False, description="True -> ignore lastSyncAt and pull every issue.",
),
},
execute=runSyncAction.__get__(self, self.__class__),
),
}
self._validateActions()
# Expose the callables directly on the instance too so workflow
# engines that resolve by attribute (``method.actionName(...)``)
# rather than through the action dict also work.
self.readTicket = readTicket.__get__(self, self.__class__)
self.listTickets = listTicketsAction.__get__(self, self.__class__)
self.createTicket = createTicketAction.__get__(self, self.__class__)
self.updateTicket = updateTicketAction.__get__(self, self.__class__)
self.getStats = getStatsAction.__get__(self, self.__class__)
self.runSync = runSyncAction.__get__(self, self.__class__)