source: multipleworkflowplugin/trunk/multipleworkflow/workflow.py

Last change on this file was 18098, checked in by Cinc-th, 3 years ago

MultipleWorkflowPlugin: Python 3 fix. Added classifiers to setup.py.

File size: 8.9 KB
RevLine 
[13465]1# -*- coding: utf-8 -*-
2#
[17783]3# Copyright (C) 2009-2015 ermal
4# Copyright (C) 2015-2020 Cinc
[13465]5# All rights reserved.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution.
9
[16598]10from trac.core import implements
[13465]11from trac.ticket import model
[16593]12from trac.ticket.default_workflow import ConfigurableTicketWorkflow, \
13                                         parse_workflow_config
[16598]14from trac.ticket.api import ITicketActionController
[17958]15from trac.util import lazy, sub_val
[17782]16from trac.web.api import IRequestFilter
17from trac.web.chrome import add_script
[13465]18
[17958]19
[18098]20try:
21    dict.iteritems
22except AttributeError:
23    # Python 3
24    def iteritems(d):
25        return iter(d.items())
26else:
27    # Python 2
28    def iteritems(d):
29        return d.iteritems()
30
31
[16598]32def get_workflow_config_by_type(config, ticket_type):
[13465]33    """return the [ticket-workflow-type] session"""
[16598]34    if ticket_type == 'default':
35        raw_actions = list(config.options('ticket-workflow'))
36    else:
37        raw_actions = list(config.options('ticket-workflow-%s' % ticket_type))
38    return parse_workflow_config(raw_actions)
[13465]39
40
[16598]41def get_all_status(actions):
[14597]42    """Calculate all states from the given list of actions.
[13465]43
[14597]44    :return a list of states like 'new', 'closed' etc.
45    """
46    all_status = set()
[16598]47    for attributes in actions.values():
48        all_status.update(attributes['oldstates'])
49        all_status.add(attributes['newstate'])
[14597]50    all_status.discard('*')
[15044]51    all_status.discard('')
[16598]52    all_status.discard(None)
[14597]53    return all_status
54
55
[15044]56class MultipleWorkflowPlugin(ConfigurableTicketWorkflow):
[14566]57    """Ticket action controller providing actions according to the ticket type.
58
[17520]59    The [http://trac-hacks.org/wiki/MultipleWorkflowPlugin MultipleWorkflowPlugin]
60    replaces the [TracWorkflow ConfigurableTicketWorkflow] used by Trac to
61    control what actions can be performed on a ticket. The actions are
[16593]62    specified in the {{{[ticket-workflow]}}} section of the TracIni file.
[14566]63
[17520]64    With [http://trac-hacks.org/wiki/MultipleWorkflowPlugin MultipleWorkflowPlugin]
65    Trac can read the workflow based on the type of a ticket. If a section for
[16593]66    that ticket type doesn't exist, then it uses the default workflow.
[14566]67
[15044]68    == Installation
69
70    Enable the plugin by adding the following to your trac.ini file:
71
[16593]72    {{{#!ini
[15044]73    [components]
74    multipleworkflow.* = enabled
75    }}}
76    Add the controller to the workflow controller list:
77
[16593]78    {{{#!ini
[15044]79    [ticket]
80    workflow = MultipleWorkflowPlugin
81    }}}
82
83    == Example
[17520]84    To define a different workflow for a ticket with type {{{Requirement}}}
[16593]85    create a section in ''trac.ini'' called
[14566]86    {{{[ticket-workflow-Requirement]}}} and add your workflow items:
[16593]87    {{{#!ini
[14566]88    [ticket-workflow-Requirement]
89    leave = * -> *
90    leave.default = 1
91    leave.operations = leave_status
92
93    approve = new, reopened -> approved
94    approve.operations = del_resolution
95    approve.permissions = TICKET_MODIFY
96
97    reopen_verified = closed -> reopened
98    reopen_verified.name= Reopen
99    reopen_verified.operations = set_resolution
100    reopen_verified.set_resolution=from verified
101    reopen_verified.permissions = TICKET_MODIFY
102
103    reopen_approved = approved -> reopened
104    reopen_approved.name = Reopen
105    reopen_approved.operations = set_resolution
106    reopen_approved.set_resolution=from approved
107    reopen_approved.permissions = TICKET_CREATE
108
109    remove = new, reopened, approved, closed -> removed
110    remove.name=Remove this Requirement permanently
111    remove.operations = set_resolution
112    remove.set_resolution= removed
113    remove.permissions = TICKET_MODIFY
114
115    verify = approved -> closed
116    verify.name=Verifiy the Requirement and mark
117    verify.operations = set_resolution
118    verify.set_resolution=verified
119    verify.permissions = TICKET_MODIFY
120    }}}
[13465]121    """
[17782]122    implements(ITicketActionController, IRequestFilter)
[13465]123
[17958]124    @lazy
125    def type_actions(self):
126        type_actions = {}
[16598]127        for t in self._ticket_types + ['default']:
128            actions = self.get_all_actions_for_type(t)
[15137]129            if actions:
[17958]130                type_actions[t] = actions
[16598]131        self.log.debug('Workflow actions at initialization: %s\n',
[17958]132                       type_actions)
133        return type_actions
[15137]134
[16598]135    @property
136    def _ticket_types(self):
137        return [enum.name for enum in model.Type.select(self.env)]
138
139    def get_actions_by_type(self, ticket_type):
[17520]140        """Return the ticket actions defined by the workflow for the given
[16593]141        ticket type or {}.
142        """
[15137]143        try:
[16598]144            return self.type_actions[ticket_type]
[15137]145        except KeyError:
[16598]146            return self.type_actions['default']
[15137]147
[16598]148    # IRequestFilter methods
[13465]149
[16598]150    def pre_process_request(self, req, handler):
151        return handler
[17520]152
[16598]153    def post_process_request(self, req, template, data, content_type):
[17782]154        """Implements the special behaviour for requests with 'mw_refresh'
[16598]155        argument should provide the proper list of available actions.
156        """
[17782]157        mine = ('/newticket', '/ticket', '/simpleticket')
[13465]158
[16598]159        match = False
160        for target in mine:
161            if req.path_info.startswith(target):
162                match = True
163                break
[13465]164
[16598]165        if match:
[17782]166            if 'mw_refresh' in req.args:
167                # This is our outosubmit handler for the type field requesting an update for the
168                # ticket actions
[16598]169                template = 'ticket_workflow.html'
[17782]170            else:
171                add_script(req, 'multipleworkflow/js/refresh_actions.js')
[16598]172        return template, data, content_type
[17520]173
[16598]174    # ITicketActionController methods
[13465]175
[16598]176    def get_ticket_actions(self, req, ticket):
177        ticket_type = req.args.get('field_type') or ticket['type']
178        self.actions = self.get_actions_by_type(ticket_type)
179        return super(MultipleWorkflowPlugin, self).\
180               get_ticket_actions(req, ticket)
[14566]181
[16598]182    def get_all_status_for_type(self, ticket_type):
183        actions = self.get_actions_by_type(ticket_type)
184        return get_all_status(actions)
185
[14566]186    def get_all_status(self):
187        """Return a list of all states described by the configuration.
188        """
189        # Default workflow
[16598]190        all_status = self.get_all_status_for_type('default')
[14566]191
[14565]192        # for all ticket types do
[16598]193        for t in self._ticket_types:
[14566]194            all_status.update(self.get_all_status_for_type(t))
[13465]195        return all_status
196
[15044]197    def render_ticket_action_control(self, req, ticket, action):
[16598]198        self.actions = self.get_actions_by_type(ticket['type'])
199        return super(MultipleWorkflowPlugin, self).\
200               render_ticket_action_control(req, ticket, action)
[14566]201
[16598]202    def get_ticket_changes(self, req, ticket, action):
203        self.actions = self.get_actions_by_type(ticket['type'])
204        return super(MultipleWorkflowPlugin, self). \
205               get_ticket_changes(req, ticket, action)
[13465]206
[16598]207    # Public methods (for other ITicketActionControllers that want to use
208    #                 our config file and provide an operation for an action)
[13465]209
[16598]210    def get_all_actions_for_type(self, ticket_type):
211        actions = get_workflow_config_by_type(self.config, ticket_type)
[14566]212        if not actions:
[16598]213            return actions
[13465]214
[16598]215        # Special action that gets enabled if the current status no longer
216        # exists, as no other action can then change its state. (#5307/#11850)
217        reset = {
218            'default': 0,
219            'label': 'reset',
220            'newstate': 'new',
221            'oldstates': [],
222            'operations': ['reset_workflow'],
223            'permissions': ['TICKET_ADMIN']
224        }
225        for key, val in reset.items():
226            actions['_reset'].setdefault(key, val)
[13465]227
[18098]228        for name, info in iteritems(actions):
[16598]229            for val in ('<none>', '< none >'):
230                sub_val(actions[name]['oldstates'], val, None)
231            if not info['newstate']:
232                self.log.warning("Ticket workflow action '%s' doesn't define "
233                                 "any transitions", name)
234        return actions
[13465]235
236    def get_actions_by_operation(self, operation):
237        """Return a list of all actions with a given operation
238        (for use in the controller's get_all_status())
239        """
[14566]240        all_actions = {}
[16598]241        all_actions.update(self.get_actions_by_type('default'))
242        for t in self._ticket_types:
243            all_actions.update(self.get_actions_by_type(t))
244        self.actions = all_actions
[13465]245
[16598]246        return super(MultipleWorkflowPlugin, self).\
247               get_actions_by_operation(operation)
[13465]248
249    def get_actions_by_operation_for_req(self, req, ticket, operation):
250        """Return list of all actions with a given operation that are valid
251        in the given state for the controller's get_ticket_actions().
252
253        If state='*' (the default), all actions with the given operation are
254        returned.
255        """
[16598]256        ticket_type = ticket._old.get('type', ticket['type'])
257        self.actions = self.get_actions_by_type(ticket_type)
[13465]258
[16598]259        return super(MultipleWorkflowPlugin, self).\
260               get_actions_by_operation_for_req(req, ticket, operation)
Note: See TracBrowser for help on using the repository browser.