Ticket #869: patch-customworkflow-r1064.diff
| File patch-customworkflow-r1064.diff, 24.3 KB (added by pkou <pkou at ua.fm>, 7 years ago) |
|---|
-
wiki-default/TracIni
22 22 See also: TracLogging 23 23 24 24 == [ticket] == 25 || workflow || Ticket workflow class. If not specified, it is ''trac.workflows.SimpleWorkflow'' || 25 26 || default_version || Default version for newly created tickets || 26 27 || default_severity || Default severity for newly created tickets || 27 28 || default_priority || Default priority for newly created tickets || -
setup.py
198 198 author_email="info@edgewall.com", 199 199 license=LICENSE, 200 200 url=URL, 201 packages=['trac', 'trac.upgrades', 'trac.wikimacros', 'trac.mimeviewers'], 201 packages=['trac', 'trac.upgrades', 'trac.wikimacros', 'trac.mimeviewers', 202 'trac.workflows'], 202 203 data_files=[(_p('share/trac/templates'), glob('templates/*')), 203 204 (_p('share/trac/htdocs'), glob(_p('htdocs/*.*')) + [_p('htdocs/README')]), 204 205 (_p('share/trac/htdocs/css'), glob(_p('htdocs/css/*'))), -
trac/workflows/Base.py
1 # -*- coding: iso8859-1 -*- 2 # 3 # Copyright (C) 2003, 2004 Edgewall Software 4 # Copyright (C) 2003, 2004 Jonas Borgström <jonas@edgewall.com> 5 # 6 # Trac is free software; you can redistribute it and/or 7 # modify it under the terms of the GNU General Public License as 8 # published by the Free Software Foundation; either version 2 of the 9 # License, or (at your option) any later version. 10 # 11 # Trac is distributed in the hope that it will be useful, 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 # General Public License for more details. 15 # 16 # You should have received a copy of the GNU General Public License 17 # along with this program; if not, write to the Free Software 18 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 19 # 20 # Author: Pavel Kourochka <pkou@ua.fm> 21 # 22 # Abstract workflow definition 23 24 class WorkflowBase: 25 """ 26 Generic workflow class for Trac. 27 """ 28 29 def __init__(self, env, db, user): 30 """ 31 Constructor for workflow class. 32 """ 33 self.env = env 34 self.db = db 35 self.user = user 36 37 def get_actions(self, ticket): 38 """ 39 For existing tickets only. 40 Return the list of available actions for specified ticket. 41 """ 42 raise Exception, "WorkflowBase::get_actions not implemented" 43 44 def do_action(self, ticket, action, args): 45 """ 46 For new and existing tickets. 47 Perform action on a ticket. For new tickets, action name is 'create'. 48 """ 49 raise Exception, "WorkflowBase::do_action not implemented" 50 51 def get_actions_template(self, ticket): 52 """ 53 For new and existing tickets. 54 Return the name of ClearSilver template file for the workflow. 55 Return None if no additional template is required. 56 """ 57 return None 58 59 def init_template(self, ticket, hdf): 60 """ 61 For new and existing tickets. 62 Initialize ClearSilver variables for actions template. 63 Called if get_actions_template() returns file name only. 64 """ 65 pass 66 67 def validate(self, ticket): 68 """ 69 For new and existing tickets. 70 Validate ticket. 71 Return list of Wiki strings that describe errors in the ticket. 72 """ 73 return [] 74 75 def on_insert(self, ticket): 76 """ 77 For new tickets only. 78 Update ticket fields just before inserting the ticket into database. 79 """ 80 pass 81 82 def on_update(self, ticket): 83 """ 84 For existing tickets only. 85 Update ticket fields just before saving the ticket into database. 86 """ 87 pass -
trac/workflows/__init__.py
1 __all__ = ['Base', 'SimpleWorkflow'] -
trac/workflows/SimpleWorkflow.py
1 # -*- coding: iso8859-1 -*- 2 # 3 # Copyright (C) 2003, 2004 Edgewall Software 4 # Copyright (C) 2003, 2004 Jonas Borgström <jonas@edgewall.com> 5 # 6 # Trac is free software; you can redistribute it and/or 7 # modify it under the terms of the GNU General Public License as 8 # published by the Free Software Foundation; either version 2 of the 9 # License, or (at your option) any later version. 10 # 11 # Trac is distributed in the hope that it will be useful, 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 # General Public License for more details. 15 # 16 # You should have received a copy of the GNU General Public License 17 # along with this program; if not, write to the Free Software 18 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 19 # 20 # Author: Pavel Kourochka <pkou@ua.fm> 21 # 22 # Simple workflow definition (as in Trac 0.8) 23 24 from trac.workflows.Base import WorkflowBase 25 26 class SimpleWorkflow(WorkflowBase): 27 28 def get_actions(self, ticket): 29 actions = { 30 'new': ['leave', 'resolve', 'reassign', 'accept'], 31 'assigned': ['leave', 'resolve', 'reassign' ], 32 'reopened': ['leave', 'resolve', 'reassign' ], 33 'closed': ['leave', 'reopen'] 34 } 35 return actions.get(ticket['status'], ['leave']) 36 37 def do_action(self, ticket, action, args): 38 if action == 'accept': 39 ticket['status'] = 'assigned' 40 ticket['owner'] = self.user 41 elif action == 'resolve': 42 ticket['status'] = 'closed' 43 ticket['resolution'] = args.get('resolve_resolution') 44 elif action == 'reassign': 45 ticket['owner'] = args.get('reassign_owner') 46 ticket['status'] = 'new' 47 elif action == 'reopen': 48 ticket['status'] = 'reopened' 49 ticket['resolution'] = '' 50 51 def get_actions_template(self, ticket): 52 if ticket.has_key('id'): 53 return 'ticket_workflow_simple.cs' 54 else: 55 return None 56 57 def init_template(self, ticket, hdf): 58 WorkflowBase.init_template(self, ticket, hdf) 59 if ticket.has_key('id'): 60 for a in self.get_actions(ticket): 61 hdf.setValue('ticket.workflow.action.' + a, '1') 62 63 def validate(self, ticket): 64 err = WorkflowBase.validate(self, ticket) 65 if not ticket.get('summary'): 66 err.append("The ticket must contain '''Summary''' field.") 67 return err 68 69 def on_insert(self, ticket): 70 WorkflowBase.on_insert(self, ticket) 71 72 # The owner field defaults to the component owner 73 cursor = self.db.cursor() 74 if ticket.get('owner', '') == '': 75 cursor.execute('SELECT owner FROM component ' 76 'WHERE name=%s', ticket.get('component', '')) 77 ticket['owner'] = cursor.fetchone()[0] or '' 78 79 def on_update(self, ticket): 80 WorkflowBase.on_update(self, ticket) 81 if not ticket._old: return # Not modified 82 83 # If the component is changed on a 'new' ticket then owner field 84 # is updated accordingly. (#623). 85 cursor = self.db.cursor() 86 if ticket['status'] == 'new' and ticket._old.has_key('component') and \ 87 not ticket._old.has_key('owner'): 88 cursor.execute('SELECT owner FROM component ' 89 'WHERE name=%s', ticket._old['component']) 90 old_owner = cursor.fetchone()[0] 91 if ticket['owner'] == old_owner: 92 cursor.execute('SELECT owner FROM component ' 93 'WHERE name=%s', ticket['component']) 94 ticket['owner'] = cursor.fetchone()[0] or '' -
trac/Ticket.py
130 130 131 131 if not self._old and not comment: return # Not modified 132 132 133 # If the component is changed on a 'new' ticket then owner field134 # is updated accordingly. (#623).135 if self['status'] == 'new' and self._old.has_key('component') and \136 not self._old.has_key('owner'):137 cursor.execute('SELECT owner FROM component '138 'WHERE name=%s', self._old['component'])139 old_owner = cursor.fetchone()[0]140 if self['owner'] == old_owner:141 cursor.execute('SELECT owner FROM component '142 'WHERE name=%s', self['component'])143 self['owner'] = cursor.fetchone()[0]144 145 146 133 for name in self._old.keys(): 147 134 if name[:7] == 'custom_': 148 135 fname = name[7:] … … 264 251 i += 1 265 252 266 253 254 def get_workflow(env, db, user): 255 # from trac.workflows.Simple import SimpleWorkflow 256 # return SimpleWorkflow(env, db, user) 257 modulename = env.get_config('ticket', 'workflow', \ 258 'trac.workflows.SimpleWorkflow') 259 i = modulename.rfind('.') 260 if i == -1: 261 classname = modulename 262 else: 263 classname = modulename[i+1:] 264 265 module = __import__(modulename, globals(), locals(), [classname]) 266 constructor = getattr(module, classname) 267 workflow = constructor(env, db, user) 268 269 from workflows.Base import WorkflowBase 270 if not isinstance(workflow, WorkflowBase): 271 raise EnvironmentError, "Workflow class %s from %s must be " \ 272 "descendant of class WorkflowBase from " \ 273 "trac.workflows.base" \ 274 % (classname, modulename) 275 276 return workflow 277 278 267 279 class NewticketModule(Module): 268 280 template_name = 'newticket.cs' 269 281 270 def create_ticket(self): 271 if not self.args.get('summary'): 272 raise util.TracError('Tickets must contain Summary.') 273 274 ticket = Ticket() 275 ticket.populate(self.args) 282 def create_ticket(self, ticket, workflow): 276 283 ticket.setdefault('reporter',self.req.authname) 277 284 278 # The owner field defaults to the component owner 279 cursor = self.db.cursor() 280 if ticket.get('component') and ticket.get('owner', '') == '': 281 cursor.execute('SELECT owner FROM component ' 282 'WHERE name=%s', ticket['component']) 283 owner = cursor.fetchone()[0] 284 ticket['owner'] = owner 285 285 workflow.on_insert(ticket) 286 286 tktid = ticket.insert(self.db) 287 287 288 288 # Notify … … 294 294 def render (self): 295 295 self.perm.assert_permission(perm.TICKET_CREATE) 296 296 297 if self.args.has_key('create'): 298 self.create_ticket() 297 ticket = Ticket() 299 298 300 ticket = Ticket() 299 preview = self.args.has_key('preview') 300 do_create = self.args.has_key('create') 301 301 ticket.populate(self.args) 302 303 workflow = get_workflow(self.env, self.db, self.req.authname) 304 305 # Validate the ticket 306 err = [] 307 if preview or do_create: 308 err.extend(workflow.validate(ticket)) 309 if len(err) != 0: preview = 1 310 311 # Create the ticket if not in preview mode 312 if not preview and do_create: 313 workflow.do_action(ticket, 'create', self.args) 314 self.create_ticket(ticket, workflow) 315 302 316 ticket.setdefault('component', 303 317 self.env.get_config('ticket', 'default_component')) 304 318 ticket.setdefault('milestone', … … 320 334 evals = util.mydict(zip(ticket.keys(), 321 335 map(lambda x: util.escape(x), ticket.values()))) 322 336 util.add_to_hdf(evals, self.req.hdf, 'newticket') 337 if len(err) != 0: 338 self.req.hdf.setValue('newticket.workflow.error', 339 wiki_to_html(' * ' + '\n * '.join(err), 340 self.req.hdf, self.env, self.db)) 341 tpl = workflow.get_actions_template(ticket) 342 if tpl: 343 self.req.hdf.setValue('newticket.workflow.template', tpl) 344 workflow.init_template(ticket, self.req.hdf) 323 345 324 346 util.sql_to_hdf(self.db, 'SELECT name FROM component ORDER BY name', 325 347 self.req.hdf, 'newticket.components') … … 334 356 class TicketModule (Module): 335 357 template_name = 'ticket.cs' 336 358 337 def save_changes (self, id):359 def save_changes (self, ticket, workflow): 338 360 self.perm.assert_permission (perm.TICKET_MODIFY) 339 ticket = Ticket(self.db, id)340 361 341 if not self.args.get('summary'):342 raise util.TracError('Tickets must contain Summary.')343 344 362 if self.args.has_key('description'): 345 363 self.perm.assert_permission (perm.TICKET_ADMIN) 346 364 347 365 if self.args.has_key('reporter'): 348 366 self.perm.assert_permission (perm.TICKET_ADMIN) 349 367 350 # TODO: this should not be hard-coded like this351 action = self.args.get('action', None)352 if action == 'accept':353 ticket['status'] = 'assigned'354 ticket['owner'] = self.req.authname355 if action == 'resolve':356 ticket['status'] = 'closed'357 ticket['resolution'] = self.args.get('resolve_resolution')358 elif action == 'reassign':359 ticket['owner'] = self.args.get('reassign_owner')360 ticket['status'] = 'new'361 elif action == 'reopen':362 ticket['status'] = 'reopened'363 ticket['resolution'] = ''364 365 ticket.populate(self.args)366 367 368 now = int(time.time()) 368 369 370 workflow.on_update(ticket) 369 371 ticket.save_changes(self.db, 370 372 self.args.get('author', self.req.authname), 371 373 self.args.get('comment'), … … 373 375 374 376 tn = TicketNotifyEmail(self.env) 375 377 tn.notify(ticket, newticket=0, modtime=now) 376 self.req.redirect(self.env.href.ticket( id))378 self.req.redirect(self.env.href.ticket(ticket['id'])) 377 379 378 380 def insert_ticket_data(self, hdf, id, ticket, reporter_id): 379 381 """Insert ticket data into the hdf""" … … 431 433 def render (self): 432 434 self.perm.assert_permission (perm.TICKET_VIEW) 433 435 434 action = self.args.get('action', 'view')435 preview = self.args.has_key('preview')436 437 436 if not self.args.has_key('id'): 438 437 self.req.redirect(self.env.href.wiki()) 439 438 440 439 id = int(self.args.get('id')) 440 ticket = Ticket(self.db, id) 441 441 442 if not preview \ 443 and action in ['leave', 'accept', 'reopen', 'resolve', 'reassign']: 444 self.save_changes (id) 442 action = self.args.get('action', None) 443 preview = self.args.has_key('preview') 444 if action or preview: 445 ticket.populate(self.args) 445 446 446 ticket = Ticket(self.db, id) 447 workflow = get_workflow(self.env, self.db, self.req.authname) 448 449 # Validate ticket 450 err = [] 451 if action or preview: 452 actions = workflow.get_actions(ticket) 453 if action not in actions: 454 err.append("Invalid action '''%s''' is performed on the ticket. " \ 455 "Allowed actions are <''%s''>." % \ 456 (action, ', '.join(actions))) 457 err.extend(workflow.validate(ticket)) 458 if len(err) != 0: preview = 1 459 460 # Save changes if not in preview mode 461 if not preview and action: 462 workflow.do_action(ticket, action, self.args) 463 self.save_changes(ticket, workflow) 464 447 465 reporter_id = util.get_reporter_id(self.req) 448 466 449 467 if preview: 450 # Use user supplied values 451 for field in Ticket.std_fields: 452 if self.args.has_key(field) and field != 'reporter': 453 ticket[field] = self.args.get(field) 454 self.req.hdf.setValue('ticket.action', action) 468 if action: self.req.hdf.setValue('ticket.action', action) 455 469 reporter_id = self.args.get('author') 456 470 comment = self.args.get('comment') 457 471 if comment: … … 462 476 self.req.hdf, self.env, self.db)) 463 477 464 478 self.insert_ticket_data(self.req.hdf, id, ticket, reporter_id) 479 if len(err) != 0: 480 self.req.hdf.setValue('ticket.workflow.error', 481 wiki_to_html(' * ' + '\n * '.join(err), 482 self.req.hdf, self.env, self.db)) 483 tpl = workflow.get_actions_template(ticket) 484 if tpl: 485 self.req.hdf.setValue('ticket.workflow.template', tpl) 486 workflow.init_template(ticket, self.req.hdf) 465 487 466 488 cursor = self.db.cursor() 467 489 cursor.execute("SELECT max(id) FROM ticket") -
templates/ticket.cs
208 208 </div><?cs /if ?> 209 209 </fieldset> 210 210 211 <fieldset id="action"> 212 <legend>Action</legend><?cs 213 if:!ticket.action ?><?cs set:ticket.action = 'leave' ?><?cs 214 /if ?><?cs 215 def:action_radio(id) ?> 216 <input type="radio" id="<?cs var:id ?>" name="action" value="<?cs 217 var:id ?>"<?cs if:$ticket.action == $id ?> checked="checked"<?cs 218 /if ?> /><?cs 219 /def ?> 220 <?cs call:action_radio('leave') ?> 221 <label for="leave">leave as <?cs var:ticket.status ?></label><br /><?cs 222 if $ticket.status == "new" ?> 223 <?cs call:action_radio('accept') ?> 224 <label for="accept">accept ticket</label><br /><?cs 225 /if ?><?cs 226 if $ticket.status == "closed" ?> 227 <?cs call:action_radio('reopen') ?> 228 <label for="reopen">reopen ticket</label><br /><?cs 229 /if ?><?cs 230 if $ticket.status == "new" || $ticket.status == "assigned" || $ticket.status == "reopened" ?> 231 <?cs call:action_radio('resolve') ?> 232 <label for="resolve">resolve</label> 233 <label for="resolve_resolution">as:</label> 234 <?cs call:hdf_select(enums.resolution, "resolve_resolution", args.resolve_resolution) ?><br /> 235 <?cs call:action_radio('reassign') ?> 236 <label for="reassign">reassign</label> 237 <label for="reassign_owner">to:</label> 238 <input type="text" id="reassign_owner" name="reassign_owner" size="40" value="<?cs 239 if:args.reassign_to ?><?cs var:args.reassign_to ?><?cs 240 else ?><?cs var:trac.authname ?><?cs /if ?>" /><?cs 241 /if ?><?cs 242 if $ticket.status == "new" || $ticket.status == "assigned" || $ticket.status == "reopened" ?> 243 <script type="text/javascript"> 244 var resolve = document.getElementById("resolve"); 245 var reassign = document.getElementById("reassign"); 246 var updateActionFields = function() { 247 enableControl('resolve_resolution', resolve.checked); 248 enableControl('reassign_owner', reassign.checked); 249 }; 250 addEvent(window, 'load', updateActionFields); 251 addEvent(document.getElementById("leave"), 'click', updateActionFields);<?cs 252 if $ticket.status == "new" ?> 253 addEvent(document.getElementById("accept"), 'click', updateActionFields);<?cs 254 /if ?> 255 addEvent(resolve, 'click', updateActionFields); 256 addEvent(reassign, 'click', updateActionFields); 257 </script><?cs 258 /if ?> 259 </fieldset> 211 <?cs if ticket.workflow.template ?> 212 <fieldset id="action"> 213 <legend>Action</legend> 214 <?cs include ticket.workflow.template ?> 215 </fieldset> 216 <?cs /if ?> 260 217 218 <?cs if ticket.workflow.error ?> 219 <div class="system-message"> 220 <h2>Ticket Error</h2> 221 <p class="message"><?cs var ticket.workflow.error ?></p> 222 <strong>The ticket will not be saved.</strong> 223 </div> 224 <?cs /if ?> 225 261 226 <div class="buttons"> 262 227 <input type="reset" value="Reset" /> 263 228 <input type="submit" name="preview" value="Preview" /> -
templates/ticket_workflow_simple.cs
1 <?cs 2 if !ticket.action ?><?cs 3 set:ticket.action = 'leave' ?><?cs 4 /if ?><?cs 5 def action_radio(id) ?> 6 <input type="radio" id="<?cs var id ?>" name="action" value="<?cs var id ?>" 7 <?cs if $ticket.action == $id ?> checked="checked"<?cs /if ?> /><?cs 8 /def ?> 9 10 <?cs 11 if ticket.workflow.action.leave ?><?cs 12 call:action_radio('leave') ?> 13 <label for="leave">leave as <?cs var:ticket.status ?></label><br /><?cs 14 /if ?><?cs 15 if ticket.workflow.action.accept ?><?cs 16 call action_radio('accept') ?> 17 <label for="accept">accept ticket</label><br /><?cs 18 /if ?><?cs 19 if ticket.workflow.action.resolve ?><?cs 20 call:action_radio('resolve') ?> 21 <label for="resolve">resolve</label> 22 <label for="resolve_resolution">as:</label><?cs 23 call:hdf_select(enums.resolution, "resolve_resolution", 24 args.resolve_resolution) ?><br /><?cs 25 /if ?><?cs 26 if ticket.workflow.action.reopen ?><?cs 27 call:action_radio('reopen') ?> 28 <label for="reopen">reopen ticket</label><br /><?cs 29 /if ?><?cs 30 if ticket.workflow.action.reassign ?><?cs 31 call:action_radio('reassign') ?> 32 <label for="reassign">reassign</label> 33 <label for="reassign_owner">to:</label> 34 <input type="text" id="reassign_owner" name="reassign_owner" size="40" 35 value=<?cs if args.reassign_to ?>"<?cs var:args.reassign_to ?>" 36 <?cs else ?>"<?cs var:trac.authname ?>" 37 <?cs /if ?> /><?cs 38 /if ?> 39 40 <?cs 41 if ticket.workflow.action.resolve || ticket.workflow.action.reassign ?> 42 <script type="text/javascript"><?cs 43 if ticket.workflow.action.resolve ?> 44 var resolve = document.getElementById("resolve");<?cs 45 /if ?><?cs 46 if ticket.workflow.action.reassign ?> 47 var reassign = document.getElementById("reassign");<?cs 48 /if ?> 49 var updateActionFields = function() {<?cs 50 if ticket.workflow.action.resolve ?> 51 enableControl('resolve_resolution', resolve.checked);<?cs 52 /if ?><?cs 53 if ticket.workflow.action.reassign ?> 54 enableControl('reassign_owner', reassign.checked);<?cs 55 /if ?> 56 }; 57 addEvent(window, 'load', updateActionFields);<?cs 58 if ticket.workflow.action.leave ?> 59 addEvent(document.getElementById("leave"), 'click', updateActionFields);<?cs 60 /if ?><?cs 61 if ticket.workflow.action.accept ?> 62 addEvent(document.getElementById("accept"), 'click', updateActionFields);<?cs 63 /if ?><?cs 64 if ticket.workflow.action.resolve ?> 65 addEvent(resolve, 'click', updateActionFields);<?cs 66 /if ?><?cs 67 if ticket.workflow.action.reopen ?> 68 addEvent(document.getElementById("reopen"), 'click', updateActionFields);<?cs 69 /if ?><?cs 70 if ticket.workflow.action.reassign ?> 71 addEvent(reassign, 'click', updateActionFields);<?cs 72 /if ?> 73 </script> 74 <?cs /if ?> -
templates/newticket.cs
69 69 </div><?cs /if ?> 70 70 </fieldset> 71 71 72 <?cs if newticket.workflow.template ?> 73 <fieldset id="action"> 74 <legend>Action</legend> 75 <?cs include newticket.workflow.template ?> 76 </fieldset> 77 <?cs /if ?> 78 79 <?cs if newticket.workflow.error ?> 80 <div class="system-message"> 81 <h2>Ticket Error</h2> 82 <p class="message"><?cs var newticket.workflow.error ?></p> 83 <strong>The ticket will not be created.</strong> 84 </div> 85 <?cs /if ?> 86 72 87 <div class="buttons"> 73 <input type="submit" value="Preview" /> 88 <input type="submit" name="preview" value="Preview" /> 74 89 <input type="submit" name="create" value="Submit ticket" /> 75 90 </div> 76 91 </form>
