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
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2009-2015 ermal
4# Copyright (C) 2015-2020 Cinc
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
10from trac.core import implements
11from trac.ticket import model
12from trac.ticket.default_workflow import ConfigurableTicketWorkflow, \
13                                         parse_workflow_config
14from trac.ticket.api import ITicketActionController
15from trac.util import lazy, sub_val
16from trac.web.api import IRequestFilter
17from trac.web.chrome import add_script
18
19
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
32def get_workflow_config_by_type(config, ticket_type):
33    """return the [ticket-workflow-type] session"""
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)
39
40
41def get_all_status(actions):
42    """Calculate all states from the given list of actions.
43
44    :return a list of states like 'new', 'closed' etc.
45    """
46    all_status = set()
47    for attributes in actions.values():
48        all_status.update(attributes['oldstates'])
49        all_status.add(attributes['newstate'])
50    all_status.discard('*')
51    all_status.discard('')
52    all_status.discard(None)
53    return all_status
54
55
56class MultipleWorkflowPlugin(ConfigurableTicketWorkflow):
57    """Ticket action controller providing actions according to the ticket type.
58
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
62    specified in the {{{[ticket-workflow]}}} section of the TracIni file.
63
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
66    that ticket type doesn't exist, then it uses the default workflow.
67
68    == Installation
69
70    Enable the plugin by adding the following to your trac.ini file:
71
72    {{{#!ini
73    [components]
74    multipleworkflow.* = enabled
75    }}}
76    Add the controller to the workflow controller list:
77
78    {{{#!ini
79    [ticket]
80    workflow = MultipleWorkflowPlugin
81    }}}
82
83    == Example
84    To define a different workflow for a ticket with type {{{Requirement}}}
85    create a section in ''trac.ini'' called
86    {{{[ticket-workflow-Requirement]}}} and add your workflow items:
87    {{{#!ini
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    }}}
121    """
122    implements(ITicketActionController, IRequestFilter)
123
124    @lazy
125    def type_actions(self):
126        type_actions = {}
127        for t in self._ticket_types + ['default']:
128            actions = self.get_all_actions_for_type(t)
129            if actions:
130                type_actions[t] = actions
131        self.log.debug('Workflow actions at initialization: %s\n',
132                       type_actions)
133        return type_actions
134
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):
140        """Return the ticket actions defined by the workflow for the given
141        ticket type or {}.
142        """
143        try:
144            return self.type_actions[ticket_type]
145        except KeyError:
146            return self.type_actions['default']
147
148    # IRequestFilter methods
149
150    def pre_process_request(self, req, handler):
151        return handler
152
153    def post_process_request(self, req, template, data, content_type):
154        """Implements the special behaviour for requests with 'mw_refresh'
155        argument should provide the proper list of available actions.
156        """
157        mine = ('/newticket', '/ticket', '/simpleticket')
158
159        match = False
160        for target in mine:
161            if req.path_info.startswith(target):
162                match = True
163                break
164
165        if match:
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
169                template = 'ticket_workflow.html'
170            else:
171                add_script(req, 'multipleworkflow/js/refresh_actions.js')
172        return template, data, content_type
173
174    # ITicketActionController methods
175
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)
181
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
186    def get_all_status(self):
187        """Return a list of all states described by the configuration.
188        """
189        # Default workflow
190        all_status = self.get_all_status_for_type('default')
191
192        # for all ticket types do
193        for t in self._ticket_types:
194            all_status.update(self.get_all_status_for_type(t))
195        return all_status
196
197    def render_ticket_action_control(self, req, ticket, action):
198        self.actions = self.get_actions_by_type(ticket['type'])
199        return super(MultipleWorkflowPlugin, self).\
200               render_ticket_action_control(req, ticket, action)
201
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)
206
207    # Public methods (for other ITicketActionControllers that want to use
208    #                 our config file and provide an operation for an action)
209
210    def get_all_actions_for_type(self, ticket_type):
211        actions = get_workflow_config_by_type(self.config, ticket_type)
212        if not actions:
213            return actions
214
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)
227
228        for name, info in iteritems(actions):
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
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        """
240        all_actions = {}
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
245
246        return super(MultipleWorkflowPlugin, self).\
247               get_actions_by_operation(operation)
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        """
256        ticket_type = ticket._old.get('type', ticket['type'])
257        self.actions = self.get_actions_by_type(ticket_type)
258
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.