| 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 | |
|---|
| 16 | from operator import itemgetter |
|---|
| 17 | from trac.core import Interface, Component, implements, ExtensionPoint, \ |
|---|
| 18 | TracError |
|---|
| 19 | from trac.resource import Resource, get_resource_url |
|---|
| 20 | from trac.util import get_reporter_id |
|---|
| 21 | from trac.util.html import html as tag, TracHTMLSanitizer |
|---|
| 22 | from trac.util.text import to_unicode |
|---|
| 23 | from trac.util.translation import _ |
|---|
| 24 | from trac.web.api import IRequestHandler |
|---|
| 25 | from trac.web.chrome import Chrome, ITemplateProvider |
|---|
| 26 | from string import Template |
|---|
| 27 | |
|---|
| 28 | if not hasattr(Chrome, 'jenv'): |
|---|
| 29 | # This is Trac 1.2 |
|---|
| 30 | from genshi import HTML |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | from ..tracgenericclass.util import formatExceptionInfo |
|---|
| 34 | from .model import ResourceWorkflowState |
|---|
| 35 | |
|---|
| 36 | |
|---|
| 37 | class 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 | |
|---|
| 54 | class 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 | |
|---|
| 73 | class 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 | |
|---|
| 119 | class 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'))] |
|---|