Ticket #31: trac-0.8.1-ticket-relations.patch
| File trac-0.8.1-ticket-relations.patch, 18.3 KB (added by havarden@…, 7 years ago) |
|---|
-
templates/macros.cs
diff -Naur trac-0.8.1-orig/templates/macros.cs trac-0.8.1/templates/macros.cs
old new 10 10 </select><?cs 11 11 /def?> 12 12 13 <?cs def:hdf_select_with_values(options, name, selected) ?> 14 <select size="1" id="<?cs var:name ?>" name="<?cs var:name ?>"><?cs 15 each:option = options ?><?cs 16 if option.name == $selected ?> 17 <option value="<?cs var:option.value ?>" selected="selected"> 18 <?cs var:option.name ?></option><?cs 19 else ?> 20 <option value="<?cs var:option.value ?>"><?cs var:option.name ?></option><?cs 21 /if ?><?cs 22 /each ?> 23 </select><?cs 24 /def?> 25 13 26 <?cs def:hdf_select_multiple(options, name, size) ?> 14 27 <select size="<?cs var:size ?>" id="<?cs var:name ?>" name="<?cs 15 28 var:name ?>" multiple="multiple"><?cs -
templates/ticket.cs
diff -Naur trac-0.8.1-orig/templates/ticket.cs trac-0.8.1/templates/ticket.cs
old new 29 29 /if ?></td><?cs if numprops % #2 && !$last_prop || fullrow ?> 30 30 </tr><tr><?cs /if ?><?cs set numprops = $numprops + #1 - fullrow ?><?cs 31 31 /def ?> 32 <?cs def:ticketrel(rel) ?> 33 <tr> 34 <form id="removerelation" method="post" action="<?cs var:cgi_location?>/ticket/<?cs 35 var:ticket.id ?>"> 36 <input type="hidden" name="rel_from" value="<?cs var:rel.from_id ?>" /> 37 <input type="hidden" name="rel_type" value="<?cs var:rel.type ?>" /> 38 <input type="hidden" name="rel_to" value="<?cs var:rel.to_id ?>" /> 39 <td colspan="2"> 40 <div title="<?cs var:rel.description ?>"><?cs var:rel.relation ?></div> 41 <td><input type="submit" name="removerel_submit" value="Remove" /></td> 42 </form> 43 </tr><?cs 44 /def ?> 32 45 33 46 <div id="ticket"> 34 47 <div class="date"><?cs var:ticket.opened ?></div> … … 66 79 /each ?> 67 80 </tr></table><?cs /if ?> 68 81 <hr /> 82 <table> 83 <tr><th colspan="3">Ticket relations</th></tr> 84 <?cs each:rel = ticket.relations ?> 85 <?cs call:ticketrel(rel) ?> 86 <?cs /each ?> 87 <?cs if trac.acl.TICKET_MODIFY ?> 88 <form id="newrelation" method="post" action="<?cs var:cgi_location?>/ticket/<?cs 89 var:ticket.id ?>"> 90 <tr> 91 <td align="center"><?cs call:hdf_select_with_values(ticket.all_tickets, "rel_from", "This ticket") ?></td> 92 <td align="center"><?cs call:hdf_select(ticket.valid_relations, "rel_type", 0) ?><br /> 93 <input type="submit" name="newrel_submit" value="Add Relation" /></td> 94 <td align="center"><?cs call:hdf_select_with_values(ticket.all_tickets, "rel_to", "this ticket") ?></td> 95 </tr> 96 </form> 97 <?cs /if ?> 98 </table> 99 100 <hr /> 69 101 <h3>Description<?cs if:ticket.reporter ?> by <?cs 70 102 var:ticket.reporter ?><?cs /if ?>:</h3> 71 103 <div class="description"> … … 131 163 </div><?cs /if ?> 132 164 133 165 <?cs if $trac.acl.TICKET_MODIFY ?> 134 <form action="<?cs var:cgi_location ?>#preview" method="post">135 166 <hr /> 136 167 <h3><a name="edit" onfocus="document.getElementById('comment').focus()">Add/Change #<?cs 137 168 var:ticket.id ?> (<?cs var:ticket.summary ?>)</a></h3> 169 <form action="<?cs var:cgi_location ?>#preview" method="post"> 138 170 <div class="field"> 139 171 <input type="hidden" name="mode" value="ticket" /> 140 172 <input type="hidden" name="id" value="<?cs var:ticket.id ?>" /> -
trac/Ticket.py
diff -Naur trac-0.8.1-orig/trac/Ticket.py trac-0.8.1/trac/Ticket.py
old new 26 26 from UserDict import UserDict 27 27 28 28 import perm 29 import relation_hooks 30 import sqlite 29 31 import util 30 32 from Module import Module 31 33 from WikiFormatter import wiki_to_html … … 42 44 def __init__(self, *args): 43 45 UserDict.__init__(self) 44 46 self._old = {} 47 self.relations = [] 45 48 if len(args) == 2: 46 49 self._fetch_ticket(*args) 47 50 … … 78 81 if rows: 79 82 for r in rows: 80 83 self['custom_' + r[0]] = r[1] 84 cursor.close() 85 86 cursor = db.cursor() 87 cursor.execute("SELECT * FROM xref WHERE source='ticket:%i' OR target='ticket:%i' ORDER BY context, source", id, id) 88 rows = cursor.fetchall() 89 if rows: 90 for r in rows: 91 self.relations.append({'from':r['source'], 'type':r['context'], 92 'to':r['target']}) 93 cursor.close() 94 81 95 self._forget_changes() 82 96 83 97 def populate(self, dict): … … 266 280 hdf.setValue('%s.height' % pfx, f['height']) 267 281 i += 1 268 282 283 def insert_relation_fields(env, db, hdf, relations, id): 284 i = 0 285 for rel in relations: 286 name = 'ticket.relations.%i' % i 287 rel_string = '' 288 if rel['from'] == id: 289 rel_string += 'This ticket ' 290 else: 291 rel_string += '%s ' % rel['from'] 292 rel_string += rel['type'] 293 if rel['to'] == id: 294 rel_string += ' this ticket.' 295 else: 296 rel_string += ' %s.' % rel['to'] 297 hdf.setValue('%s.relation' % name, wiki_to_html(rel_string, hdf, env, db)) 298 hdf.setValue('%s.from_id' % name, str(rel['from'])) 299 hdf.setValue('%s.to_id' % name, str(rel['to'])) 300 hdf.setValue('%s.type' % name, str(rel['type'])) 301 hdf.setValue('%s.description' % name, env.get_config('relation:' + rel['type'], 'description', 'No description available')) 302 i += 1 303 304 def _valid_ticket(ticket, db): 305 cursor = db.cursor() 306 cursor.execute('SELECT COUNT(*) FROM ticket WHERE id=%s', ticket) 307 row = cursor.fetchone() 308 if row: 309 if row['COUNT(*)'] > 0: 310 return True 311 return False 312 313 def _valid_relation(relation, env): 314 relations = _get_valid_relations(env) 315 for i in relations: 316 if relation == i: 317 return True 318 return False 319 320 def _get_valid_relations(env): 321 """Returns a list of all valid relations (fetched from configuration file). 322 To set valid relations, edit the configuration and change this part: 323 324 [ticket] 325 valid_relations = depends on, related to, duplicate of 326 327 and add/remove the relations you want, i.e. 328 329 [ticket] 330 valid_relations = depends on, blocks, duplicate of, is subissue of 331 332 Each relation may also have a set of hooks associated with it. The valid hooks 333 to put functions in are: 334 hooks_ticket_modify 335 hooks_relation_add 336 hooks_relation_remove 337 338 The hooks are defined in the configuration file as follows: 339 [relation:<relation name>] 340 hooks_ticket_modify = <function_name> [ , <function_name> ... ] 341 hooks_relation_add = <function_name> [ , <function_name> ... ] 342 hooks_relation_remove = <function_name> [ , <function_name> ... ] 343 344 An example: 345 [relation:depends on] 346 hooks_relation_add = check_circular 347 """ 348 349 rels = env.get_config('ticket', 'valid_relations') 350 return [i.strip() for i in rels.split(',')] 351 269 352 270 353 class NewticketModule(Module): 271 354 template_name = 'newticket.cs' … … 369 452 370 453 now = int(time.time()) 371 454 455 cursor = self.db.cursor() 456 cursor.execute("SELECT source, target, context FROM xref WHERE source='ticket:%s' OR target='ticket:%s'", id, id) 457 rows = cursor.fetchall() 458 if rows: 459 for r in rows: 460 self.call_relation_hooks('hooks_ticket_modify', action, r[0], r[1], r[2], id) 461 372 462 ticket.save_changes(self.db, 373 463 self.args.get('author', self.req.authname), 374 464 self.args.get('comment'), … … 378 468 tn.notify(ticket, newticket=0, modtime=now) 379 469 self.req.redirect(self.env.href.ticket(id)) 380 470 471 def save_relation(self, id): 472 self.perm.assert_permission(perm.TICKET_MODIFY) 473 ticket = Ticket(self.db, id) 474 newrel_from = self.args.get('rel_from') 475 newrel_type = self.args.get('rel_type') 476 newrel_to = self.args.get('rel_to') 477 478 # Check sanity of arguments 479 if not newrel_from or not newrel_type or not newrel_to: 480 raise util.TracError('Ticket relations must have to, from and type fields.') 481 if not _valid_ticket(newrel_from, self.db) or not _valid_ticket(newrel_to, self.db): 482 raise util.TracError('Ticket relations must use existing tickets.') 483 if not _valid_relation(newrel_type, self.env): 484 raise util.TracError('Ticket relations must be in the set of valid relations: %s' % str(_get_valid_relations(self.env))) 485 if (not newrel_from == str(id) and not newrel_to == str(id)) or (newrel_from == newrel_to): 486 raise util.TracError('Either the from field or the to field, but not both, must be this ticket.') 487 488 # Call hooks 489 self.call_relation_hooks('hooks_relation_add', self.args.get('action', None), newrel_from, newrel_to, newrel_type, id) 490 491 # Insert into database and commit 492 cursor = self.db.cursor() 493 try: 494 cursor.execute("INSERT INTO xref VALUES (%s, %s, %s)", 'ticket:' + str(newrel_from), 'ticket:' + str(newrel_to), newrel_type) 495 except sqlite.IntegrityError: 496 self.db.rollback() 497 raise util.TracError('This relation already exists.') 498 cursor.close() 499 self.db.commit() 500 501 def remove_relation(self, id): 502 self.perm.assert_permission(perm.TICKET_MODIFY) 503 ticket = Ticket(self.db, id) 504 rel_from = self.args.get('rel_from') 505 rel_type = self.args.get('rel_type') 506 rel_to = self.args.get('rel_to') 507 508 # Check sanity. 509 if not rel_from or not rel_type or not rel_to: 510 raise util.TracError('Cannot remove relation: Required information is missing.') 511 512 # Call hooks 513 self.call_relation_hooks('hooks_relation_remove', self.args.get('action', None), rel_from, rel_to, rel_type, id) 514 515 # Commit to database. 516 cursor = self.db.cursor() 517 cursor.execute("DELETE FROM xref WHERE source=%s AND target=%s and context=%s", rel_from, rel_to, rel_type) 518 cursor.close() 519 self.db.commit() 520 521 def call_relation_hooks(self, hook_type, action, rel_from, rel_to, rel_type, id): 522 """Calls all hooks of the given hook_type.""" 523 for hook_name in self.env.get_config('relation:' + rel_type, 524 hook_type).split(','): 525 hook_name = hook_name.strip() 526 if hook_name == '': 527 continue 528 if not hasattr(relation_hooks, hook_name): 529 raise util.TracError('Could not add relation: Hook "%s" not found.' % hook_name) 530 getattr(relation_hooks, hook_name)(self.db, self.env, rel_from, rel_to, 531 rel_type, action, id) 532 381 533 def insert_ticket_data(self, hdf, id, ticket, reporter_id): 382 534 """Insert ticket data into the hdf""" 383 535 evals = util.mydict(zip(ticket.keys(), … … 400 552 util.hdf_add_if_missing(self.req.hdf, 'enums.severity', ticket['severity']) 401 553 util.hdf_add_if_missing(self.req.hdf, 'enums.resolution', 'fixed') 402 554 555 556 403 557 self.req.hdf.setValue('ticket.reporter_id', util.escape(reporter_id)) 404 558 self.req.hdf.setValue('title', '#%d (%s)' % (id, util.escape(ticket['summary']))) 405 559 self.req.hdf.setValue('ticket.description.formatted', … … 427 581 idx = idx + 1 428 582 429 583 insert_custom_fields(self.env, hdf, ticket) 584 # Insert all relations in the database 585 insert_relation_fields(self.env, self.db, hdf, ticket.relations, id) 586 587 # Add list of all available tickets and valid relations 588 cursor = self.db.cursor() 589 cursor.execute('SELECT SUBSTR(id, 0, 1024) FROM ticket ORDER BY id') 590 rows = cursor.fetchall() 591 ctr = 0 592 if rows: 593 for r in rows: 594 if str(r[0]) == str(id): 595 hdf.setValue('ticket.all_tickets.%d.name' % ctr, 'This ticket') 596 else: 597 hdf.setValue('ticket.all_tickets.%d.name' % ctr, r[0]) 598 hdf.setValue('ticket.all_tickets.%d.value' % ctr, r[0]) 599 ctr += 1 600 601 ctr = 0 602 for i in _get_valid_relations(self.env): 603 hdf.setValue('ticket.valid_relations.%d.name' % ctr, i) 604 ctr += 1 605 430 606 # List attached files 431 607 self.env.get_attachments_hdf(self.db, 'ticket', str(id), self.req.hdf, 432 608 'ticket.attachments') … … 440 616 if not self.args.has_key('id'): 441 617 self.req.redirect(self.env.href.wiki()) 442 618 619 443 620 id = int(self.args.get('id')) 444 621 445 622 if not preview \ 446 623 and action in ['leave', 'accept', 'reopen', 'resolve', 'reassign']: 447 624 self.save_changes (id) 448 625 626 if not preview and self.args.has_key('newrel_submit'): 627 self.save_relation(id) 628 if not preview and self.args.has_key('removerel_submit'): 629 self.remove_relation(id) 630 449 631 ticket = Ticket(self.db, id) 450 632 reporter_id = util.get_reporter_id(self.req) 451 633 -
trac/db_default.py
diff -Naur trac-0.8.1-orig/trac/db_default.py trac-0.8.1/trac/db_default.py
old new 111 111 value text, 112 112 UNIQUE(ticket,name) 113 113 ); 114 115 CREATE TABLE xref ( 116 source text, 117 target text, 118 context text 119 ); 120 114 121 CREATE TABLE report ( 115 122 id integer PRIMARY KEY, 116 123 author text, -
trac/relation_hooks.py
diff -Naur trac-0.8.1-orig/trac/relation_hooks.py trac-0.8.1/trac/relation_hooks.py
old new 1 import util 2 3 def dummy(db, env, ticket_from, ticket_to, relation_type, action, target_ticket): 4 """A dummy hook that does nothing.""" 5 return 6 7 def check_circular(db, env, ticket_from, ticket_to, relation_type, action, target_ticket): 8 """Checks for a circular relation between the two tickets.""" 9 _circular(db, ticket_from, ticket_to, relation_type) 10 return 11 12 def _circular(db, target_ticket, origin, relation_type): 13 cursor = db.cursor() 14 cursor.execute('SELECT target FROM xref WHERE source=%s AND context=%s', origin, relation_type) 15 rows = cursor.fetchall() 16 if rows: 17 for r in rows: 18 if r[0] == target_ticket: 19 cursor.close() 20 raise util.TracError('This relation creates a circular relationship between tickets, and cannot be created.') 21 _circular(db, target_ticket, r[0], relation_type) 22 cursor.close() 23 24 25 def check_excludes(db, env, ticket_from, ticket_to, relation_type, action, target_ticket): 26 """Checks if there already is a relation between the two tickets that excludes 27 the relation we are trying to add.""" 28 excludes = env.get_config('relation:' + relation_type, 'excludes_relations') 29 if not excludes: 30 return 31 excludes = excludes.split(',') 32 cursor = db.cursor() 33 sql = 'SELECT COUNT(*) FROM xref WHERE source=%s AND target=%s AND' 34 where = [] 35 args = [ticket_from, ticket_to] 36 for i in excludes: 37 where.append('context=%s') 38 args.append(i) 39 sql += ('(%s)') % " OR ".join(where) 40 cursor.execute(sql, *args) 41 row = cursor.fetchone() 42 if row: 43 if row[0] > 0: 44 raise util.TracError('Cannot add relation; another relation which excludes this one already exists.') 45 46 def check_from_tickets_closed(db, env, ticket_from, ticket_to, relation_type, action, target_ticket): 47 """This hook checks that closed(source) => closed(target) and 48 open(source) => open(target) for all targets which have a relation 49 of type relation_type from source to target.""" 50 if not (action == 'resolve' or action == 'reopen'): 51 return 52 cursor = db.cursor() 53 if action == 'resolve' and ticket_to == target_ticket: 54 cursor.execute("SELECT COUNT(*) FROM xref r, ticket t WHERE r.context=%s AND r.source='ticket:'+t.id AND t.status != 'closed' AND r.target=%s", relation_type, target_ticket) 55 elif action == 'reopen' and ticket_from == target_ticket: 56 cursor.execute("SELECT COUNT(*) FROM xref r, ticket t WHERE r.context=%s AND r.source=%s AND t.status=\'closed\' AND r.target='ticket:'+t.id", relation_type, target_ticket) 57 else: 58 return 59 row = cursor.fetchone() 60 if row and row[0] > 0: 61 if action == 'resolve': 62 raise util.TracError('One or more of this ticket\'s relations require another ticket to be resolved before this one can be resolved.') 63 elif action == 'reopen': 64 raise util.TracError('One or more of this ticket\'s relations require another ticket to be reopened before this one can be reopened.') 65 66 def check_to_tickets_closed(db, env, ticket_from, ticket_to, relation_type, action, target_ticket): 67 """This hook checks that closed(source) => closed(target) and 68 open(source) => open(target) for all targets which have a relation 69 of type relation_type from source to target.""" 70 if not (action == 'resolve' or action == 'reopen'): 71 return 72 cursor = db.cursor() 73 if action == 'resolve' and ticket_from == target_ticket: 74 cursor.execute("SELECT COUNT(*) FROM xref r, ticket t WHERE r.context=%s AND r.target='ticket:'+t.id AND t.status != 'closed' AND r.source=%s", relation_type, target_ticket) 75 elif action == 'reopen' and ticket_to == target_ticket: 76 cursor.execute("SELECT COUNT(*) FROM xref r, ticket t WHERE r.context=%s AND r.target=%s AND t.status='closed' AND r.source='ticket:'+t.id", relation_type, target_ticket) 77 else: 78 return 79 row = cursor.fetchone() 80 if row and row[0] > 0: 81 if action == 'resolve': 82 raise util.TracError('One or more of this ticket\'s relations require another ticket to be resolved before this one can be resolved.') 83 elif action == 'reopen': 84 raise util.TracError('One or more of this ticket\'s relations require another ticket to be reopened before this one can be reopened.') 85 return 86 87 def failing(db, env, ticket_from, ticket_to, relation_type, action, target_ticket): 88 """A hook that always fails, for testing purposes.""" 89 raise util.TracError("The failing hook failed.")
