253 lines
14 KiB
Python
253 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,
|
|
parameters={
|
|
"featureInstanceId": WorkflowActionParameter(
|
|
name="featureInstanceId", type="str", frontendType=FrontendType.TEXT,
|
|
required=True, description="Redmine feature instance ID",
|
|
),
|
|
"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,
|
|
parameters={
|
|
"featureInstanceId": WorkflowActionParameter(
|
|
name="featureInstanceId", type="str", frontendType=FrontendType.TEXT,
|
|
required=True, description="Redmine feature instance ID",
|
|
),
|
|
"trackerIds": WorkflowActionParameter(
|
|
name="trackerIds", type="list", 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,
|
|
parameters={
|
|
"featureInstanceId": WorkflowActionParameter(
|
|
name="featureInstanceId", type="str", frontendType=FrontendType.TEXT,
|
|
required=True, description="Redmine feature instance ID",
|
|
),
|
|
"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", 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", 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,
|
|
parameters={
|
|
"featureInstanceId": WorkflowActionParameter(
|
|
name="featureInstanceId", type="str", frontendType=FrontendType.TEXT,
|
|
required=True, description="Redmine feature instance ID",
|
|
),
|
|
"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", 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", frontendType=FrontendType.TEXTAREA,
|
|
required=False, description="Journal entry (comment) added to the ticket.",
|
|
),
|
|
"customFields": WorkflowActionParameter(
|
|
name="customFields", type="dict", 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,
|
|
parameters={
|
|
"featureInstanceId": WorkflowActionParameter(
|
|
name="featureInstanceId", type="str", frontendType=FrontendType.TEXT,
|
|
required=True, description="Redmine feature instance ID",
|
|
),
|
|
"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", 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,
|
|
parameters={
|
|
"featureInstanceId": WorkflowActionParameter(
|
|
name="featureInstanceId", type="str", frontendType=FrontendType.TEXT,
|
|
required=True, description="Redmine feature instance ID",
|
|
),
|
|
"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__)
|