source: peerreviewplugin/trunk/codereview/tracgenericworkflow/api.py

Last change on this file was 18255, checked in by Cinc-th, 2 years ago

PeerReviewPlugin: some minor CSS changes to main page. CSS changes and foldable sections for codereview page.

Refs #14006

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