source: peerreviewplugin/tags/0.12/3.1/codereview/tracgenericworkflow/api.py

Last change on this file was 16616, checked in by Ryan J Ollos, 6 years ago

TracCodeReview 3.1: Fix incorrect imports

Refs #13193.

File size: 18.3 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2010-2015 Roberto Longobardi
4#
5# This file is part of the Test Manager plugin for Trac.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution. The terms
9# are also available at:
10#   https://trac-hacks.org/wiki/TestManagerForTracPluginLicense
11#
12# Author: Roberto Longobardi <otrebor.dev@gmail.com>
13#
14
15from operator import itemgetter
16from trac.core import Interface, Component, implements, ExtensionPoint, \
17    TracError
18from trac.resource import Resource, get_resource_url
19from trac.util import get_reporter_id
20from trac.util.html import html as tag
21from trac.util.translation import _
22from trac.web.api import IRequestHandler
23from trac.web.chrome import ITemplateProvider
24from string import Template
25from genshi import HTML
26
27from ..tracgenericclass.util import formatExceptionInfo
28from .model import ResourceWorkflowState
29
30
31class IWorkflowTransitionListener(Interface):
32    """
33    Extension point interface for components that require notification
34    when objects transition between states.
35    """
36
37    def object_transition(self, res_wf_state, resource, action, old_state, new_state):
38        """
39        Called when an object has transitioned to a new state.
40
41        :param res_wf_state: the ResourceWorkflowState
42                             transitioned from old_state to new_state
43        :param resource: the Resource object transitioned.
44        :param action: the action been performed.
45        """
46
47
48class IWorkflowTransitionAuthorization(Interface):
49    """
50    Extension point interface for components that wish to augment the
51    state machine at runtime, by allowing or denying each transition
52    based on the object and the current and new states.
53    """
54
55    def is_authorized(self, res_wf_state, resource, action, old_state, new_state):
56        """
57        Called before allowing the transition.
58        Return True to allow for the transition, False to deny it.
59
60        :param res_wf_state: the ResourceWorkflowState being
61                             transitioned from old_state to new_state
62        :param resource: the Resource object being transitioned.
63        :param action: the action being performed.
64        """
65
66
67class IWorkflowOperationProvider(Interface):
68    """
69    Extension point interface for components willing to implement
70    custom workflow operations.
71    """
72
73    def get_implemented_operations(self):
74        """
75        Return custom actions provided by the component.
76
77        :rtype: `basestring` generator
78        """
79
80    def get_operation_control(self, req, action, operation, res_wf_state, resource):
81        """
82        Asks the provider to provide UI control to let the User
83        perform the specified operation on the given resource.
84        This control(s) will be rendered inside a form and the values
85        will be eventually available to this provvider in the
86        perform_operation method, to actually perform the operation.
87
88        :param req: the http request.
89        :param action: the action being performed by the User.
90        :param operation: the name of the operation to be rendered.
91        :param res_wf_state: the ResourceWorkflowState
92                             transitioned from old_state to new_state
93        :param resource: the Resource object transitioned.
94        :return: a Genshi tag with the required control(s) and a string
95                 with the operation hint.
96        """
97
98    def perform_operation(self, req, action, operation, old_state, new_state, res_wf_state, resource):
99        """
100        Perform the specified operation on the given resource, which
101        has transitioned from the given old to the given new state.
102
103        :param req: the http request, which parameters contain the
104                    input fields (provided by means of
105                    'get_operation_control') that the User has now
106                    valorized.
107        :param res_wf_state: the ResourceWorkflowState
108                             transitioned from old_state to new_state
109        :param resource: the Resource object transitioned.
110        """
111
112
113class ResourceWorkflowSystem(Component):
114    """
115    Generic Resource workflow system for Trac.
116    """
117
118    implements(IRequestHandler, ITemplateProvider)
119
120    transition_listeners = ExtensionPoint(IWorkflowTransitionListener)
121    transition_authorizations = ExtensionPoint(IWorkflowTransitionAuthorization)
122    operation_providers = ExtensionPoint(IWorkflowOperationProvider)
123
124    actions = {}
125    _operation_providers_map = None
126
127    def __init__(self, *args, **kwargs):
128        """
129        Parses the configuration file to find all the workflows defined.
130
131        To define a workflow state machine for a particular resource
132        realm, add a "<realm>-resource_workflow" section in trac.ini
133        and describe the state machine with the same syntax as the
134        ConfigurableTicketWorkflow component.
135        """
136
137        Component.__init__(self, *args, **kwargs)
138
139        from trac.ticket.default_workflow import parse_workflow_config
140
141        for section in self.config.sections():
142            if section.find('-resource_workflow') > 0:
143                self.log.debug("ResourceWorkflowSystem - parsing config section %s" % section)
144                realm = section.partition('-')[0]
145                raw_actions = list(self.config.options(section))
146
147                self.actions[realm] = parse_workflow_config(raw_actions)
148
149        self._operation_providers_map = None
150
151
152    # Workflow state machine management
153
154    def get_available_actions(self, req, realm, resource=None):
155        """
156        Returns a list of (weight, action) tuples, for the specified
157        realm,  that are valid for this request and the current state.
158        """
159        self.log.debug(">>> ResourceWorkflowSystem - get_available_actions")
160
161        # Get the list of actions that can be performed
162
163        user_perms = None
164        curr_state = 'new'
165        if resource is not None:
166            user_perms = req.perm(resource)
167            rws = ResourceWorkflowState(self.env, resource.id, realm)
168            if rws.exists:
169                curr_state = rws['state']
170
171        allowed_actions = []
172
173        if realm in self.actions:
174            for action_name, action_info in self.actions[realm].items():
175                oldstates = action_info['oldstates']
176                if oldstates == ['*'] or curr_state in oldstates:
177                    # This action is valid in this state.
178                    # Check permissions if possible.
179                    if user_perms:
180                        required_perms = action_info['permissions']
181                        if not self._is_action_allowed(user_perms, required_perms):
182                            continue
183
184                    allowed_actions.append((action_info['default'],
185                                            action_name))
186
187        self.log.debug("<<< ResourceWorkflowSystem - get_available_actions")
188        return sorted(allowed_actions, key=itemgetter(0), reverse=True)
189
190    def _is_action_allowed(self, user_perms, required_perms):
191        if not required_perms:
192            return True
193        for permission in required_perms:
194            if permission in user_perms:
195                return True
196        return False
197
198    def get_all_states(self, realm):
199        """
200        Return a set with all the states described by the configuration
201        for the specified realm.
202        Returns an empty set if none.
203        """
204        all_states = set()
205
206        if realm in self.actions:
207            for action_name, action_info in self.actions[realm].items():
208                all_states.update(action_info['oldstates'])
209                all_states.add(action_info['newstate'])
210            all_states.discard('*')
211
212        return all_states
213
214    def get_action_markup(self, req, realm, action, resource=None):
215        self.log.debug('get_action_markup: action "%s"' % action)
216
217        id = None
218        if resource is not None:
219            id = resource.id
220
221        rws = ResourceWorkflowState(self.env, id, realm)
222
223        this_action = self.actions[realm][action]
224        status = this_action['newstate']
225        operations = this_action['operations']
226
227        controls = [] # default to nothing
228        hints = []
229
230        for operation in operations:
231            self.env.log.debug(">>>>>>>>>>>>>>> "+operation)
232            provider = self.get_operation_provider(operation)
233            self.env.log.debug (provider)
234
235            if provider is not None:
236                control, hint = provider.get_operation_control(req, action, operation, rws, resource)
237
238                controls.append(control)
239                hints.append(hint)
240
241        if 'leave_status' not in operations:
242            if status != '*':
243                hints.append(_(" Next status will be '%(name)s'", name=status))
244
245        return (this_action['name'], tag(*controls), '. '.join(hints))
246
247    def get_workflow_markup(self, req, base_href, realm, resource, data=None):
248        form_tmpl = u"""
249                <form class="workflow-actions" method="post" action="$action" name="resource_workflow_form">
250                    <fieldset>
251                        <input name="id" type="hidden" value="$resource_id" />
252                        <input name="res_realm" type="hidden" value="$realm" />
253                        <input name="redirect" value="$redirect" type="hidden" />
254                        <dl>
255                            <dt>Current state:</dt>
256                            <dd>$cur_state</dd>
257                        </dl>
258                        $ctrls
259                        <div class="buttons" $display>
260                            <input type="submit" id="resource_workflow_form_submit_button" value="Perform Action" />
261                        </div>
262                    </fieldset>
263                </form> """
264
265        rws = ResourceWorkflowState(self.env, resource.id, realm)
266
267        # action_controls is an ordered list of "renders" tuples, where
268        # renders is a list of (action_key, label, widgets, hints) representing
269        # the user interface for each action
270        action_controls = []
271        sorted_actions = self.get_available_actions(
272            req, realm, resource=resource)
273
274        tdata = {'action': base_href+'/workflowtransition',
275                 'resource_id': resource.id,
276                 'cur_state': rws['state'],
277                 'ctrls': "",
278                 'realm': realm,
279                 'redirect': '',
280                 'display': ''}
281
282        tmpl = Template(form_tmpl)
283        if len(sorted_actions) > 0:
284            for action in sorted_actions:
285                first_label = None
286                hints = []
287                widgets = []
288
289                label, widget, hint = self.get_action_markup(req, realm, action[1], resource)
290
291                if not first_label:
292                    first_label = label
293
294                widgets.append(widget)
295                hints.append(hint)
296
297                action_controls.append((action[1], first_label, tag(widgets), hints))
298
299            ctrls = ""
300            for i, ac in enumerate(action_controls):
301                # The default action is the first in the action_controls list.
302                cdata = {'is_checked': 'checked="1"' if i == 0 else '',
303                         'val': ac[0],
304                         'label': ac[1],
305                         'ctrl': ac[2].generate().render(encoding=None),
306                         'hint': ac[3][0]}
307                if len(ac[2].children):
308                    # This is some custom workflow creating it's own markup
309                    ctrl_tmpl2 = u"""
310                    <div>
311                        <input id="wf-$val" name="selected_action" type="radio" value="$val" $is_checked />
312                        $ctrl
313                        <span class="hint">$hint</span>
314                    </div>"""
315                    ctrls += Template(ctrl_tmpl2).safe_substitute(cdata)
316                else:
317                    ctrl_tmpl = u"""
318                    <div>
319                        <input id="wf-$val" name="selected_action" type="radio" value="$val" $is_checked />
320                        <label for="wf-$val">$label</label>
321                        $ctrl
322                        <span class="hint">$hint</span>
323                    </div>"""
324                    ctrls += Template(ctrl_tmpl).safe_substitute(cdata)
325
326            tdata['ctrls'] = ctrls
327            if data and data.get('redirect'):
328                tdata['redirect'] = data.get('redirect')
329
330            return HTML(tmpl.safe_substitute(tdata), encoding="utf-8")
331        else:
332            tdata['display'] = 'style="display: none;"'
333            return HTML(tmpl.safe_substitute(tdata), encoding="utf-8")
334
335    # Workflow operations management
336
337    def get_operation_provider(self, operation_name):
338        """Return the component responsible for providing the specified
339        custom workflow operation
340
341        :param operation_name: the operation name
342        :return: a `Component` implementing `IWorkflowOperationProvider`
343                 or `None`
344        """
345        # build a dict of operation keys to IWorkflowOperationProvider
346        # implementations
347        if not self._operation_providers_map:
348            map = {}
349            for provider in self.operation_providers:
350                for operation_name in provider.get_implemented_operations() or []:
351                    map[operation_name] = provider
352            self._operation_providers_map = map
353
354        if operation_name in self._operation_providers_map:
355            return self._operation_providers_map.get(operation_name)
356        else:
357            return None
358
359    def get_known_operations(self):
360        """
361        Return a list of all the operation names of
362        operation providers.
363        """
364        operation_names = []
365        for provider in self.operation_providers:
366            for operation_name in provider.get_implemented_operations() or []:
367                operation_names.append(operation_name)
368
369        return operation_names
370
371
372    # IRequestHandler methods
373    # Workflow transition implementation
374
375    def match_request(self, req):
376        return req.path_info.startswith('/workflowtransition')
377
378    def process_request(self, req):
379        """Handles requests to perform a state transition."""
380
381        author = get_reporter_id(req, 'author')
382
383        if req.path_info.startswith('/workflowtransition'):
384            # Check permission
385            #req.perm.require('TEST_EXECUTE')
386
387            selected_action = req.args.get('selected_action')
388
389            id = req.args.get('id')
390            res_realm = req.args.get('res_realm')
391
392            res = Resource(res_realm, id)
393            rws = ResourceWorkflowState(self.env, id, res_realm)
394            rws.authname = req.authname
395
396            # from codereview.tracgenericclass.model import GenericClassModelProvider
397            # gclass_modelprovider = GenericClassModelProvider(self.env)
398            # obj = gclass_modelprovider.get_object(res_realm, str(9))
399            # print "   ", res_realm, " object: ",  obj
400            # if obj.exists:
401            #     print obj['review_id']
402            #     print obj.resource.realm
403            #     print "Resource id", obj.resource.id
404
405            if rws.exists:
406                curr_state = rws['state']
407            else:
408                curr_state = 'new'
409
410            this_action = self.actions[res_realm][selected_action]
411            new_state = this_action['newstate']
412
413            if new_state == '*':
414                new_state = curr_state
415            self.env.log.debug("Performing action %s. Transitioning the resource %s in realm %s from the state %s to the state %s" % (selected_action, id, res_realm, curr_state, new_state))
416
417            try:
418                # Check external authorizations
419                for external_auth in self.transition_authorizations:
420                    if not external_auth.is_authorized(rws, res, selected_action, curr_state, new_state):
421                        TracError("External authorization to the workflow transition denied.")
422
423                # Perform operations
424                operations = this_action['operations']
425                for operation in operations:
426                    provider = self.get_operation_provider(operation)
427
428                    if provider is not None:
429                        provider.perform_operation(req, selected_action, operation, curr_state, new_state, rws, res)
430                    else:
431                        self.env.log.debug("Unable to find operation provider for operation %s" % operation)
432
433                # Transition the resource to the new state
434                if rws.exists:
435                    if not new_state == curr_state:
436                        # Check that the resource is still in the state it
437                        # was when the User browsed it
438                        if rws['state'] == curr_state:
439                            rws['state'] = new_state
440                            try:
441                                rws.save_changes(author, "State changed")
442                            except:
443                                self.log.debug("Error saving the resource %s with id %s" % (res_realm, id))
444                        else:
445                            TracError("Resource with id %s has already changed state in the meanwhile. Current state is %s." % (id, rws['state']))
446                else:
447                    rws['state'] = new_state
448                    rws.insert()
449
450                # Call listeners
451                for listener in self.transition_listeners:
452                    listener.object_transition(rws, res, selected_action, curr_state, new_state)
453            except:
454                self.env.log.debug(formatExceptionInfo())
455                raise
456
457            href = req.args.get('redirect')
458            if not href:
459                # Redirect to the resource URL.
460                href = get_resource_url(self.env, res, req.href)
461            req.redirect(href)
462
463        return 'empty.html', {}, None
464
465
466    # ITemplateProvider methods
467    def get_templates_dirs(self):
468        """
469        Return the absolute path of the directory containing the provided
470        Genshi templates.
471        """
472        from pkg_resources import resource_filename
473        return [resource_filename(__name__, 'templates')]
474
475    def get_htdocs_dirs(self):
476        """Return the absolute path of a directory containing additional
477        static resources (such as images, style sheets, etc).
478        """
479        from pkg_resources import resource_filename
480        return [('tracgenericworkflow', resource_filename(__name__, 'htdocs'))]
Note: See TracBrowser for help on using the repository browser.