Trees Indices Help
Trac
Package trac :: Package ticket :: Module model

Source Code for Module trac.ticket.model

 1 # -*- coding: utf-8 -*- 
 2 # 
 3 # Copyright (C) 2003-2009 Edgewall Software 
 4 # Copyright (C) 2003-2006 Jonas Borgström <jonas@edgewall.com> 
 5 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de> 
 6 # Copyright (C) 2006 Christian Boos <cboos@edgewall.org> 
 7 # All rights reserved. 
 8 # 
 9 # This software is licensed as described in the file COPYING, which 
 10 # you should have received as part of this distribution. The terms 
 11 # are also available at http://trac.edgewall.org/wiki/TracLicense. 
 12 # 
 13 # This software consists of voluntary contributions made by many 
 14 # individuals. For the exact contribution history, see the revision 
 15 # history and logs, available at http://trac.edgewall.org/log/. 
 16 # 
 17 # Author: Jonas Borgström <jonas@edgewall.com> 
 18 # Christopher Lenz <cmlenz@gmx.de> 
 19 
 20 import re 
 21 from datetime import datetime 
 22 
 23 from trac .attachment  import Attachment  
 24 from trac .core  import TracError  
 25 from trac .resource  import Resource , ResourceNotFound  
 26 from trac .ticket .api  import TicketSystem  
 27 from trac .util  import embedded_numbers , partition  
 28 from trac .util .text  import empty  
 29 from trac .util .datefmt  import from_utimestamp , to_utimestamp , utc , utcmax  
 30 from trac .util .translation  import _  
 31 
 32 __all__ = ['Ticket', 'Type', 'Status', 'Resolution', 'Priority', 'Severity', 
 33  'Component', 'Milestone', 'Version', 'group_milestones'] 
34 35 36 - def _fixup_cc_list (cc_value):
37 """Fix up cc list separators and remove duplicates.""" 38 cclist = [] 39 for cc in re.split(r'[;,\s]+', cc_value): 40 if cc and cc not in cclist: 41 cclist.append(cc) 42 return ', '.join (cclist)
43
44 45 - class Ticket (object):
46 47 # Fields that must not be modified directly by the user 48 protected_fields = ('resolution', 'status', 'time', 'changetime') 49 50 @staticmethod
51 - def id_is_valid (num):
52 return 0 < int(num) <= 1L << 31
53 54 # 0.11 compatibility 55 time_created = property(lambda self: self.values.get ('time')) 56 time_changed = property(lambda self: self.values.get ('changetime')) 57
58 - def __init__ (self, env, tkt_id=None, db=None, version=None):
59 self.env = env 60 if tkt_id is not None: 61 tkt_id = int(tkt_id) 62 self.resource = Resource ('ticket', tkt_id, version ) 63 self.fields = TicketSystem (self.env ).get_ticket_fields () 64 self.time_fields = [f['name'] for f in self.fields 65 if f['type'] == 'time'] 66 self.values = {} 67 if tkt_id is not None: 68 self._fetch_ticket(tkt_id, db ) 69 else: 70 self._init_defaults(db ) 71 self.id = None 72 self._old = {}
73
74 - def _get_db (self, db):
75 return db or self.env .get_read_db ()
76 77 exists = property(lambda self: self.id is not None) 78
79 - def _init_defaults (self, db=None):
80 for field in self.fields : 81 default = None 82 if field['name'] in self.protected_fields : 83 # Ignore for new - only change through workflow 84 pass 85 elif not field.get ('custom'): 86 default = self.env .config .get ('ticket', 87 'default_' + field['name']) 88 else: 89 default = field.get ('value') 90 options = field.get ('options') 91 if default and options and default not in options : 92 try: 93 default = options [int(default )] 94 except (ValueError, IndexError): 95 self.env .log .warning('Invalid default value "%s" ' 96 'for custom field "%s"' 97 % (default , field['name'])) 98 if default : 99 self.values.setdefault(field['name'], default )
100
101 - def _fetch_ticket (self, tkt_id, db=None):
102 row = None 103 if self.id_is_valid (tkt_id): 104 db = self._get_db(db ) 105 106 # Fetch the standard ticket fields 107 std_fields = [f['name'] for f in self.fields 108 if not f.get ('custom')] 109 cursor = db .cursor () 110 cursor .execute ("SELECT %s FROM ticket WHERE id=%%s" 111 % ','.join (std_fields), (tkt_id,)) 112 row = cursor .fetchone () 113 if not row: 114 raise ResourceNotFound (_ ('Ticket %(id)s does not exist.', 115 id =tkt_id), _ ('Invalid ticket number')) 116 117 self.id = tkt_id 118 for i , field in enumerate(std_fields): 119 value = row[i ] 120 if field in self.time_fields: 121 self.values[field] = from_utimestamp (value) 122 elif value is None: 123 self.values[field] = empty 124 else: 125 self.values[field] = value 126 127 # Fetch custom fields if available 128 custom_fields = [f['name'] for f in self.fields if f.get ('custom')] 129 cursor .execute ("SELECT name,value FROM ticket_custom WHERE ticket=%s", 130 (tkt_id,)) 131 for name , value in cursor : 132 if name in custom_fields : 133 if value is None: 134 self.values[name ] = empty 135 else: 136 self.values[name ] = value
137
138 - def __getitem__ (self, name):
139 return self.values.get (name )
140
141 - def __setitem__ (self, name, value):
142 """Log ticket modifications so the table ticket_change can be updated 143 """ 144 if name in self.values and self.values[name ] == value: 145 return 146 if name not in self._old: # Changed field 147 self._old[name ] = self.values.get (name ) 148 elif self._old[name ] == value: # Change of field reverted 149 del self._old[name ] 150 if value: 151 if isinstance(value, list): 152 raise TracError (_ ("Multi-values fields not supported yet")) 153 field = [field for field in self.fields if field['name'] == name ] 154 if field and field[0].get ('type') != 'textarea': 155 value = value.strip() 156 self.values[name ] = value
157
158 - def get_value_or_default (self, name):
159 """Return the value of a field or the default value if it is undefined 160 """ 161 try: 162 value = self.values[name ] 163 if value is not empty : 164 return value 165 field = [field for field in self.fields if field['name'] == name ] 166 if field: 167 return field[0].get ('value', '') 168 except KeyError: 169 pass
170
171 - def populate (self, values):
172 """Populate the ticket with 'suitable' values from a dictionary""" 173 field_names = [f['name'] for f in self.fields ] 174 for name in [name for name in values.keys () if name in field_names]: 175 self[name ] = values.get (name , '') 176 177 # We have to do an extra trick to catch unchecked checkboxes 178 for name in [name for name in values.keys () if name [9:] in field_names 179 and name .startswith('checkbox_')]: 180 if name [9:] not in values: 181 self[name [9:]] = '0'
182
183 - def insert (self, when=None, db=None):
184 """Add ticket to database. 185 186 The `db` argument is deprecated in favor of `with_transaction()`. 187 """ 188 assert not self.exists , 'Cannot insert an existing ticket' 189 190 if 'cc' in self.values: 191 self['cc'] = _fixup_cc_list(self.values['cc']) 192 193 # Add a timestamp 194 if when is None: 195 when = datetime.now(utc ) 196 self.values['time'] = self.values['changetime'] = when 197 198 # The owner field defaults to the component owner 199 if self.values.get ('component') and not self.values.get ('owner'): 200 try: 201 component = Component (self.env , self['component'], db =db ) 202 if component.owner: 203 self['owner'] = component.owner 204 except ResourceNotFound : 205 # No such component exists 206 pass 207 208 # Perform type conversions 209 values = dict(self.values) 210 for field in self.time_fields: 211 if field in values: 212 values[field] = to_utimestamp (values[field]) 213 214 # Insert ticket record 215 std_fields = [] 216 custom_fields = [] 217 for f in self.fields : 218 fname = f['name'] 219 if fname in self.values: 220 if f.get ('custom'): 221 custom_fields .append(fname) 222 else: 223 std_fields.append(fname) 224 225 tkt_id = [None] 226 @self.env .with_transaction (db ) 227 def do_insert(db): 228 cursor = db .cursor () 229 cursor .execute ("INSERT INTO ticket (%s) VALUES (%s)" 230 % (','.join (std_fields), 231 ','.join (['%s'] * len(std_fields))), 232 [values[name ] for name in std_fields]) 233 tkt_id[0] = db .get_last_id (cursor , 'ticket') 234 235 # Insert custom fields 236 if custom_fields : 237 cursor .executemany (""" 238 INSERT INTO ticket_custom (ticket,name,value) VALUES (%s,%s,%s) 239 """, [(tkt_id[0], name , self[name ]) for name in custom_fields ])
240 241 self.id = tkt_id[0] 242 self.resource = self.resource (id =tkt_id[0]) 243 self._old = {} 244 245 for listener in TicketSystem (self.env ).change_listeners : 246 listener.ticket_created (self) 247 248 return self.id
249
250 - def save_changes (self, author=None, comment=None, when=None, db=None, cnum=''):
251 """ 252 Store ticket changes in the database. The ticket must already exist in 253 the database. Returns False if there were no changes to save, True 254 otherwise. 255 256 The `db` argument is deprecated in favor of `with_transaction()`. 257 """ 258 assert self.exists , 'Cannot update a new ticket' 259 260 if 'cc' in self.values: 261 self['cc'] = _fixup_cc_list(self.values['cc']) 262 263 if not self._old and not comment: 264 return False # Not modified 265 266 if when is None: 267 when = datetime.now(utc ) 268 when_ts = to_utimestamp (when) 269 270 if 'component' in self.values: 271 # If the component is changed on a 'new' ticket 272 # then owner field is updated accordingly. (#623). 273 if self.values.get ('status') == 'new' \ 274 and 'component' in self._old \ 275 and 'owner' not in self._old: 276 try: 277 old_comp = Component (self.env , self._old['component']) 278 old_owner = old_comp.owner or '' 279 current_owner = self.values.get ('owner') or '' 280 if old_owner == current_owner: 281 new_comp = Component (self.env , self['component']) 282 if new_comp.owner: 283 self['owner'] = new_comp.owner 284 except TracError : 285 # If the old component has been removed from the database 286 # we just leave the owner as is. 287 pass 288 289 @self.env .with_transaction (db ) 290 def do_save(db): 291 cursor = db .cursor () 292 293 # find cnum if it isn't provided 294 comment_num = cnum 295 if not comment_num: 296 num = 0 297 cursor .execute (""" 298 SELECT DISTINCT tc1.time,COALESCE(tc2.oldvalue,'') 299 FROM ticket_change AS tc1 300 LEFT OUTER JOIN ticket_change AS tc2 301 ON tc2.ticket=%s AND tc2.time=tc1.time 302 AND tc2.field='comment' 303 WHERE tc1.ticket=%s ORDER BY tc1.time DESC 304 """, (self.id , self.id )) 305 for ts, old in cursor : 306 # Use oldvalue if available, else count edits 307 try: 308 num += int(old.rsplit('.', 1)[-1]) 309 break 310 except ValueError: 311 num += 1 312 comment_num = str(num + 1) 313 314 # store fields 315 custom_fields = [f['name'] for f in self.fields if f.get ('custom')] 316 317 for name in self._old.keys (): 318 if name in custom_fields : 319 cursor .execute (""" 320 SELECT * FROM ticket_custom 321 WHERE ticket=%s and name=%s 322 """, (self.id , name )) 323 if cursor .fetchone (): 324 cursor .execute (""" 325 UPDATE ticket_custom SET value=%s 326 WHERE ticket=%s AND name=%s 327 """, (self[name ], self.id , name )) 328 else: 329 cursor .execute (""" 330 INSERT INTO ticket_custom (ticket,name,value) 331 VALUES(%s,%s,%s) 332 """, (self.id , name , self[name ])) 333 else: 334 cursor .execute ("UPDATE ticket SET %s=%%s WHERE id=%%s" 335 % name , (self[name ], self.id )) 336 cursor .execute (""" 337 INSERT INTO ticket_change 338 (ticket,time,author,field,oldvalue,newvalue) 339 VALUES (%s, %s, %s, %s, %s, %s) 340 """, (self.id , when_ts, author, name , self._old[name ], 341 self[name ])) 342 343 # always save comment, even if empty 344 # (numbering support for timeline) 345 cursor .execute (""" 346 INSERT INTO ticket_change 347 (ticket,time,author,field,oldvalue,newvalue) 348 VALUES (%s,%s,%s,'comment',%s,%s) 349 """, (self.id , when_ts, author, comment_num, comment)) 350 351 cursor .execute ("UPDATE ticket SET changetime=%s WHERE id=%s", 352 (when_ts, self.id ))
353 354 old_values = self._old 355 self._old = {} 356 self.values['changetime'] = when 357 358 for listener in TicketSystem (self.env ).change_listeners : 359 listener.ticket_changed (self, comment, author, old_values) 360 return True
361
362 - def get_changelog (self, when=None, db=None):
363 """Return the changelog as a list of tuples of the form 364 (time, author, field, oldvalue, newvalue, permanent). 365 366 While the other tuple elements are quite self-explanatory, 367 the `permanent` flag is used to distinguish collateral changes 368 that are not yet immutable (like attachments, currently). 369 """ 370 db = self._get_db(db ) 371 cursor = db .cursor () 372 sid = str(self.id ) 373 when_ts = to_utimestamp (when) 374 if when_ts: 375 cursor .execute (""" 376 SELECT time,author,field,oldvalue,newvalue, 1 AS permanent 377 FROM ticket_change WHERE ticket=%s AND time=%s 378 UNION 379 SELECT time,author,'attachment',null,filename, 0 AS permanent 380 FROM attachment WHERE type='ticket' AND id=%s AND time=%s 381 UNION 382 SELECT time,author,'comment',null,description, 0 AS permanent 383 FROM attachment WHERE type='ticket' AND id=%s AND time=%s 384 ORDER BY time,permanent,author 385 """, (self.id , when_ts, sid , when_ts, sid , when_ts)) 386 else: 387 cursor .execute (""" 388 SELECT time,author,field,oldvalue,newvalue, 1 AS permanent 389 FROM ticket_change WHERE ticket=%s 390 UNION 391 SELECT time,author,'attachment',null,filename, 0 AS permanent 392 FROM attachment WHERE type='ticket' AND id=%s 393 UNION 394 SELECT time,author,'comment',null,description, 0 AS permanent 395 FROM attachment WHERE type='ticket' AND id=%s 396 ORDER BY time,permanent,author 397 """, (self.id , sid , sid )) 398 log = [] 399 for t , author, field, oldvalue, newvalue, permanent in cursor : 400 log .append((from_utimestamp (t ), author, field, 401 oldvalue or '', newvalue or '', permanent)) 402 return log
403
404 - def delete (self, db=None):
405 """Delete the ticket. 406 407 The `db` argument is deprecated in favor of `with_transaction()`. 408 """ 409 @self.env .with_transaction (db ) 410 def do_delete(db): 411 Attachment .delete_all (self.env , 'ticket', self.id , db ) 412 cursor = db .cursor () 413 cursor .execute ("DELETE FROM ticket WHERE id=%s", (self.id ,)) 414 cursor .execute ("DELETE FROM ticket_change WHERE ticket=%s", 415 (self.id ,)) 416 cursor .execute ("DELETE FROM ticket_custom WHERE ticket=%s", 417 (self.id ,))
418 419 for listener in TicketSystem (self.env ).change_listeners : 420 listener.ticket_deleted (self) 421
422 - def get_change (self, cnum, db=None):
423 """Return a ticket change by its number.""" 424 db = self._get_db(db ) 425 cursor = db .cursor () 426 row = self._find_change(cnum, db ) 427 if row: 428 ts, author, comment = row 429 cursor .execute (""" 430 SELECT field,author,oldvalue,newvalue 431 FROM ticket_change WHERE ticket=%s AND time=%s 432 """, (self.id , ts)) 433 fields = {} 434 change = {'date': from_utimestamp (ts), 435 'author': author, 'fields': fields } 436 for field, author, old, new in cursor : 437 fields [field] = {'author': author, 'old': old, 'new': new} 438 return change
439
440 - def delete_change (self, cnum):
441 """Delete a ticket change.""" 442 @self.env .with_transaction () 443 def do_delete(db): 444 cursor = db .cursor () 445 row = self._find_change(cnum, db ) 446 if not row: 447 return 448 ts = row[0] 449 450 std_fields = set (f['name'] for f in self.fields 451 if not f.get ('custom')) 452 453 # Find modified fields and their previous value 454 cursor .execute (""" 455 SELECT field, oldvalue, newvalue FROM ticket_change 456 WHERE ticket=%s AND time=%s 457 """, (self.id , ts)) 458 fields = [(field, old, new) for field, old, new in cursor 459 if field != 'comment' and not field.startswith('_')] 460 for field, oldvalue, newvalue in fields : 461 # Find the next change 462 cursor .execute (""" 463 SELECT time FROM ticket_change 464 WHERE ticket=%s AND time>%s AND field=%s 465 LIMIT 1 466 """, (self.id , ts, field)) 467 for next_ts, in cursor : 468 # Modify the old value of the next change if it is equal 469 # to the new value of the deleted change 470 cursor .execute (""" 471 UPDATE ticket_change SET oldvalue=%s 472 WHERE ticket=%s AND time=%s AND field=%s 473 AND oldvalue=%s 474 """, (oldvalue, self.id , next_ts, field, newvalue)) 475 break 476 else: 477 # No next change, edit ticket field 478 if field in std_fields: 479 cursor .execute (""" 480 UPDATE ticket SET %s=%%s WHERE id=%%s 481 """ % field, (oldvalue, self.id )) 482 else: 483 cursor .execute (""" 484 UPDATE ticket_custom SET value=%s 485 WHERE ticket=%s AND name=%s 486 """, (oldvalue, self.id , field)) 487 488 # Delete the change 489 cursor .execute (""" 490 DELETE FROM ticket_change WHERE ticket=%s AND time=%s 491 """, (self.id , ts)) 492 493 # Fix the last modification time 494 # Work around MySQL ERROR 1093 with the same table for the update 495 # target and the subquery FROM clause 496 cursor .execute (""" 497 UPDATE ticket SET changetime=( 498 SELECT time FROM ticket_change WHERE ticket=%s 499 UNION 500 SELECT time FROM ( 501 SELECT time FROM ticket WHERE id=%s LIMIT 1) AS t 502 ORDER BY time DESC LIMIT 1) 503 WHERE id=%s 504 """, (self.id , self.id , self.id ))
505 506 self._fetch_ticket(self.id ) 507
508 - def modify_comment (self, cdate, author, comment, when=None):
509 """Modify a ticket comment specified by its date, while keeping a 510 history of edits. 511 """ 512 ts = to_utimestamp (cdate) 513 if when is None: 514 when = datetime.now(utc ) 515 when_ts = to_utimestamp (when) 516 517 @self.env .with_transaction () 518 def do_modify(db): 519 cursor = db .cursor () 520 # Find the current value of the comment 521 cursor .execute (""" 522 SELECT newvalue FROM ticket_change 523 WHERE ticket=%s AND time=%s AND field='comment' 524 """, (self.id , ts)) 525 old_comment = False 526 for old_comment, in cursor : 527 break 528 if comment == (old_comment or ''): 529 return 530 531 # Comment history is stored in fields named "_comment%d" 532 # Find the next edit number 533 cursor .execute (""" 534 SELECT field FROM ticket_change 535 WHERE ticket=%%s AND time=%%s AND field %s 536 """ % db .prefix_match (), 537 (self.id , ts, db .prefix_match_value ('_comment'))) 538 fields = list(cursor ) 539 rev = fields and max(int(field[8:]) for field, in fields ) + 1 or 0 540 cursor .execute (""" 541 INSERT INTO ticket_change 542 (ticket,time,author,field,oldvalue,newvalue) 543 VALUES (%s,%s,%s,%s,%s,%s) 544 """, (self.id , ts, author, '_comment%d' % rev, 545 old_comment or '', str(when_ts))) 546 if old_comment is False: 547 # There was no comment field, add one, find the original author 548 # in one of the other changed fields 549 cursor .execute (""" 550 SELECT author FROM ticket_change 551 WHERE ticket=%%s AND time=%%s AND NOT field %s 552 LIMIT 1 553 """ % db .prefix_match (), 554 (self.id , ts, db .prefix_match_value ('_'))) 555 old_author = None 556 for old_author, in cursor : 557 break 558 cursor .execute (""" 559 INSERT INTO ticket_change 560 (ticket,time,author,field,oldvalue,newvalue) 561 VALUES (%s,%s,%s,'comment','',%s) 562 """, (self.id , ts, old_author, comment)) 563 else: 564 cursor .execute (""" 565 UPDATE ticket_change SET newvalue=%s 566 WHERE ticket=%s AND time=%s AND 567 field='comment' 568 """, (comment, self.id , ts)) 569 570 # Update last changed time 571 cursor .execute ("UPDATE ticket SET changetime=%s WHERE id=%s", 572 (when_ts, self.id ))
573 574 self.values['changetime'] = when 575
576 - def get_comment_history (self, cnum, db=None):
577 db = self._get_db(db ) 578 history = [] 579 cursor = db .cursor () 580 row = self._find_change(cnum, db ) 581 if row: 582 ts0, author0, last_comment = row 583 # Get all fields of the form "_comment%d" 584 cursor .execute (""" 585 SELECT field,author,oldvalue,newvalue 586 FROM ticket_change 587 WHERE ticket=%%s AND time=%%s AND field %s 588 """ % db .prefix_match (), 589 (self.id , ts0, db .prefix_match_value ('_comment'))) 590 rows = sorted((int(field[8:]), author, old, new) 591 for field, author, old, new in cursor ) 592 for rev, author, comment, ts in rows: 593 history.append((rev, from_utimestamp (long(ts0)), author0, 594 comment)) 595 ts0, author0 = ts, author 596 history.sort() 597 rev = history and (history[-1][0] + 1) or 0 598 history.append((rev, from_utimestamp (long(ts0)), author0, 599 last_comment)) 600 return history
601
602 - def _find_change (self, cnum, db):
603 """Find a comment by its number.""" 604 scnum = str(cnum) 605 cursor = db .cursor () 606 cursor .execute (""" 607 SELECT time,author,newvalue FROM ticket_change 608 WHERE ticket=%%s AND field='comment' 609 AND (oldvalue=%%s OR oldvalue %s) 610 """ % db .like (), (self.id , scnum, 611 '%' + db .like_escape ('.' + scnum))) 612 for row in cursor : 613 return row 614 615 # Fallback when comment number is not available in oldvalue 616 num = 0 617 cursor .execute (""" 618 SELECT DISTINCT tc1.time,COALESCE(tc2.oldvalue,''), 619 tc2.author,COALESCE(tc2.newvalue,'') 620 FROM ticket_change AS tc1 621 LEFT OUTER JOIN ticket_change AS tc2 622 ON tc2.ticket=%s AND tc2.time=tc1.time AND tc2.field='comment' 623 WHERE tc1.ticket=%s ORDER BY tc1.time 624 """, (self.id , self.id )) 625 for ts, old, author, comment in cursor : 626 # Use oldvalue if available, else count edits 627 try: 628 num = int(old.rsplit('.', 1)[-1]) 629 except ValueError: 630 num += 1 631 if num == cnum: 632 break 633 else: 634 return 635 636 # Find author if NULL 637 if author is None: 638 cursor .execute (""" 639 SELECT author FROM ticket_change 640 WHERE ticket=%%s AND time=%%s AND NOT field %s 641 LIMIT 1 642 """ % db .prefix_match (), 643 (self.id , ts, db .prefix_match_value ('_'))) 644 for author, in cursor : 645 break 646 return (ts, author, comment)
647
648 - def simplify_whitespace (name):
649 """Strip spaces and remove duplicate spaces within names""" 650 if name : 651 return ' '.join (name .split()) 652 return name
653
654 655 - class AbstractEnum (object):
656 type = None 657 ticket_col = None 658
659 - def __init__ (self, env, name=None, db=None):
660 if not self.ticket_col : 661 self.ticket_col = self.type 662 self.env = env 663 if name : 664 if not db : 665 db = self.env .get_read_db () 666 cursor = db .cursor () 667 cursor .execute ("SELECT value FROM enum WHERE type=%s AND name=%s", 668 (self.type , name )) 669 row = cursor .fetchone () 670 if not row: 671 raise ResourceNotFound (_ ('%(type)s %(name)s does not exist.', 672 type =self.type , name =name )) 673 self.value = self._old_value = row[0] 674 self.name = self._old_name = name 675 else: 676 self.value = self._old_value = None 677 self.name = self._old_name = None
678 679 exists = property(lambda self: self._old_value is not None) 680
681 - def delete (self, db=None):
682 """Delete the enum value. 683 684 The `db` argument is deprecated in favor of `with_transaction()`. 685 """ 686 assert self.exists , 'Cannot delete non-existent %s' % self.type 687 688 @self.env .with_transaction (db ) 689 def do_delete(db): 690 cursor = db .cursor () 691 self.env .log .info('Deleting %s %s' % (self.type , self.name )) 692 cursor .execute ("DELETE FROM enum WHERE type=%s AND value=%s", 693 (self.type , self._old_value)) 694 # Re-order any enums that have higher value than deleted 695 # (close gap) 696 for enum in list(self.select (self.env , db )): 697 try: 698 if int(enum.value) > int(self._old_value): 699 enum.value = unicode(int(enum.value) - 1) 700 enum.update () 701 except ValueError: 702 pass # Ignore cast error for this non-essential operation 703 TicketSystem (self.env ).reset_ticket_fields ()
704 self.value = self._old_value = None 705 self.name = self._old_name = None
706
707 - def insert (self, db=None):
708 """Add a new enum value. 709 710 The `db` argument is deprecated in favor of `with_transaction()`. 711 """ 712 assert not self.exists , 'Cannot insert existing %s' % self.type 713 self.name = simplify_whitespace(self.name ) 714 if not self.name : 715 raise TracError (_ ('Invalid %(type)s name.', type =self.type )) 716 717 @self.env .with_transaction (db ) 718 def do_insert(db): 719 cursor = db .cursor () 720 self.env .log .debug("Creating new %s '%s'" % (self.type , self.name )) 721 if not self.value: 722 cursor .execute (""" 723 SELECT COALESCE(MAX(%s),0) FROM enum WHERE type=%%s 724 """ % db .cast ('value', 'int'), (self.type ,)) 725 self.value = int(float(cursor .fetchone ()[0])) + 1 726 cursor .execute ("INSERT INTO enum (type,name,value) " 727 "VALUES (%s,%s,%s)", 728 (self.type , self.name , self.value)) 729 TicketSystem (self.env ).reset_ticket_fields ()
730 731 self._old_name = self.name 732 self._old_value = self.value 733
734 - def update (self, db=None):
735 """Update the enum value. 736 737 The `db` argument is deprecated in favor of `with_transaction()`. 738 """ 739 assert self.exists , 'Cannot update non-existent %s' % self.type 740 self.name = simplify_whitespace(self.name ) 741 if not self.name : 742 raise TracError (_ ('Invalid %(type)s name.', type =self.type )) 743 744 @self.env .with_transaction (db ) 745 def do_update(db): 746 cursor = db .cursor () 747 self.env .log .info('Updating %s "%s"' % (self.type , self.name )) 748 cursor .execute (""" 749 UPDATE enum SET name=%s,value=%s 750 WHERE type=%s AND name=%s 751 """, (self.name , self.value, self.type , self._old_name)) 752 if self.name != self._old_name: 753 # Update tickets 754 cursor .execute ("UPDATE ticket SET %s=%%s WHERE %s=%%s" % 755 (self.ticket_col , self.ticket_col ), 756 (self.name , self._old_name)) 757 TicketSystem (self.env ).reset_ticket_fields ()
758 759 self._old_name = self.name 760 self._old_value = self.value 761 762 @classmethod
763 - def select (cls, env, db=None):
764 if not db : 765 db = env .get_read_db () 766 cursor = db .cursor () 767 cursor .execute (""" 768 SELECT name,value FROM enum WHERE type=%s 769 ORDER BY 770 """ + db .cast ('value', 'int'), (cls.type ,)) 771 for name , value in cursor : 772 obj = cls(env ) 773 obj.name = obj._old_name = name 774 obj.value = obj._old_value = value 775 yield obj
776
777 778 - class Type (AbstractEnum):
779 type = 'ticket_type' 780 ticket_col = 'type'
781
782 783 - class Status (object):
784 - def __init__ (self, env):
785 self.env = env
786 787 @classmethod
788 - def select (cls, env, db=None):
789 for state in TicketSystem (env ).get_all_status (): 790 status = cls(env ) 791 status.name = state 792 yield status
793
794 795 - class Resolution (AbstractEnum):
796 type = 'resolution'
797
798 799 - class Priority (AbstractEnum):
800 type = 'priority'
801
802 803 - class Severity (AbstractEnum):
804 type = 'severity'
805
806 807 - class Component (object):
808 - def __init__ (self, env, name=None, db=None):
809 self.env = env 810 if name : 811 if not db : 812 db = self.env .get_read_db () 813 cursor = db .cursor () 814 cursor .execute (""" 815 SELECT owner,description FROM component WHERE name=%s 816 """, (name ,)) 817 row = cursor .fetchone () 818 if not row: 819 raise ResourceNotFound (_ ('Component %(name)s does not exist.', 820 name =name )) 821 self.name = self._old_name = name 822 self.owner = row[0] or None 823 self.description = row[1] or '' 824 else: 825 self.name = self._old_name = None 826 self.owner = None 827 self.description = None
828 829 exists = property(lambda self: self._old_name is not None) 830
831 - def delete (self, db=None):
832 """Delete the component. 833 834 The `db` argument is deprecated in favor of `with_transaction()`. 835 """ 836 assert self.exists , 'Cannot delete non-existent component' 837 838 @self.env .with_transaction (db ) 839 def do_delete(db): 840 cursor = db .cursor () 841 self.env .log .info('Deleting component %s' % self.name ) 842 cursor .execute ("DELETE FROM component WHERE name=%s", (self.name ,)) 843 self.name = self._old_name = None 844 TicketSystem (self.env ).reset_ticket_fields ()
845
846 - def insert (self, db=None):
847 """Insert a new component. 848 849 The `db` argument is deprecated in favor of `with_transaction()`. 850 """ 851 assert not self.exists , 'Cannot insert existing component' 852 self.name = simplify_whitespace(self.name ) 853 if not self.name : 854 raise TracError (_ ('Invalid component name.')) 855 856 @self.env .with_transaction (db ) 857 def do_insert(db): 858 cursor = db .cursor () 859 self.env .log .debug("Creating new component '%s'" % self.name ) 860 cursor .execute (""" 861 INSERT INTO component (name,owner,description) 862 VALUES (%s,%s,%s) 863 """, (self.name , self.owner, self.description )) 864 self._old_name = self.name 865 TicketSystem (self.env ).reset_ticket_fields ()
866
867 - def update (self, db=None):
868 """Update the component. 869 870 The `db` argument is deprecated in favor of `with_transaction()`. 871 """ 872 assert self.exists , 'Cannot update non-existent component' 873 self.name = simplify_whitespace(self.name ) 874 if not self.name : 875 raise TracError (_ ('Invalid component name.')) 876 877 @self.env .with_transaction (db ) 878 def do_update(db): 879 cursor = db .cursor () 880 self.env .log .info('Updating component "%s"' % self.name ) 881 cursor .execute (""" 882 UPDATE component SET name=%s,owner=%s, description=%s 883 WHERE name=%s 884 """, (self.name , self.owner, self.description , self._old_name)) 885 if self.name != self._old_name: 886 # Update tickets 887 cursor .execute (""" 888 UPDATE ticket SET component=%s WHERE component=%s 889 """, (self.name , self._old_name)) 890 self._old_name = self.name 891 TicketSystem (self.env ).reset_ticket_fields ()
892 893 @classmethod
894 - def select (cls, env, db=None):
895 if not db : 896 db = env .get_read_db () 897 cursor = db .cursor () 898 cursor .execute (""" 899 SELECT name,owner,description FROM component ORDER BY name 900 """) 901 for name , owner, description in cursor : 902 component = cls(env ) 903 component.name = component._old_name = name 904 component.owner = owner or None 905 component.description = description or '' 906 yield component
907
908 909 - class Milestone (object):
910 - def __init__ (self, env, name=None, db=None):
911 self.env = env 912 if name : 913 self._fetch(name , db ) 914 else: 915 self.name = None 916 self.due = self.completed = None 917 self.description = '' 918 self._to_old()
919 920 @property
921 - def resource (self):
922 return Resource ('milestone', self.name ) ### .version !!!
923
924 - def _fetch (self, name, db=None):
925 if not db : 926 db = self.env .get_read_db () 927 cursor = db .cursor () 928 cursor .execute (""" 929 SELECT name,due,completed,description 930 FROM milestone WHERE name=%s 931 """, (name ,)) 932 row = cursor .fetchone () 933 if not row: 934 raise ResourceNotFound (_ ('Milestone %(name)s does not exist.', 935 name =name ), _ ('Invalid milestone name')) 936 self._from_database(row)
937 938 exists = property(lambda self: self._old['name'] is not None) 939 is_completed = property(lambda self: self.completed is not None) 940 is_late = property(lambda self: self.due and 941 self.due < datetime.now(utc )) 942
943 - def _from_database (self, row):
944 name , due, completed, description = row 945 self.name = name 946 self.due = due and from_utimestamp (due) or None 947 self.completed = completed and from_utimestamp (completed) or None 948 self.description = description or '' 949 self._to_old()
950
951 - def _to_old (self):
952 self._old = {'name': self.name , 'due': self.due, 953 'completed': self.completed, 954 'description': self.description }
955
956 - def delete (self, retarget_to=None, author=None, db=None):
957 """Delete the milestone. 958 959 The `db` argument is deprecated in favor of `with_transaction()`. 960 """ 961 @self.env .with_transaction (db ) 962 def do_delete(db): 963 cursor = db .cursor () 964 self.env .log .info('Deleting milestone %s' % self.name ) 965 cursor .execute ("DELETE FROM milestone WHERE name=%s", (self.name ,)) 966 Attachment .delete_all (self.env , 'milestone', self.name , db ) 967 968 # Retarget/reset tickets associated with this milestone 969 now = datetime.now(utc ) 970 cursor .execute ("SELECT id FROM ticket WHERE milestone=%s", 971 (self.name ,)) 972 tkt_ids = [int(row[0]) for row in cursor ] 973 for tkt_id in tkt_ids: 974 ticket = Ticket (self.env , tkt_id, db ) 975 ticket ['milestone'] = retarget_to 976 ticket .save_changes (author, 'Milestone %s deleted' % self.name , 977 now) 978 self._old['name'] = None 979 TicketSystem (self.env ).reset_ticket_fields ()
980 981 for listener in TicketSystem (self.env ).milestone_change_listeners : 982 listener.milestone_deleted (self)
983
984 - def insert (self, db=None):
985 """Insert a new milestone. 986 987 The `db` argument is deprecated in favor of `with_transaction()`. 988 """ 989 self.name = simplify_whitespace(self.name ) 990 if not self.name : 991 raise TracError (_ ('Invalid milestone name.')) 992 993 @self.env .with_transaction (db ) 994 def do_insert(db): 995 cursor = db .cursor () 996 self.env .log .debug("Creating new milestone '%s'" % self.name ) 997 cursor .execute (""" 998 INSERT INTO milestone (name,due,completed,description) 999 VALUES (%s,%s,%s,%s) 1000 """, (self.name , to_utimestamp (self.due), 1001 to_utimestamp (self.completed), self.description )) 1002 self._to_old() 1003 TicketSystem (self.env ).reset_ticket_fields ()
1004 1005 for listener in TicketSystem (self.env ).milestone_change_listeners : 1006 listener.milestone_created (self) 1007
1008 - def update (self, db=None):
1009 """Update the milestone. 1010 1011 The `db` argument is deprecated in favor of `with_transaction()`. 1012 """ 1013 self.name = simplify_whitespace(self.name ) 1014 if not self.name : 1015 raise TracError (_ ('Invalid milestone name.')) 1016 1017 @self.env .with_transaction (db ) 1018 def do_update(db): 1019 cursor = db .cursor () 1020 old_name = self._old['name'] 1021 self.env .log .info('Updating milestone "%s"' % self.name ) 1022 cursor .execute (""" 1023 UPDATE milestone 1024 SET name=%s,due=%s,completed=%s,description=%s WHERE name=%s 1025 """, (self.name , to_utimestamp (self.due), 1026 to_utimestamp (self.completed), 1027 self.description , old_name)) 1028 1029 if self.name != old_name: 1030 # Update milestone field in tickets 1031 self.env .log .info('Updating milestone field of all tickets ' 1032 'associated with milestone "%s"' % self.name ) 1033 cursor .execute (""" 1034 UPDATE ticket SET milestone=%s WHERE milestone=%s 1035 """, (self.name , old_name)) 1036 TicketSystem (self.env ).reset_ticket_fields () 1037 1038 # Reparent attachments 1039 Attachment .reparent_all (self.env , 'milestone', old_name, 1040 'milestone', self.name )
1041 1042 old_values = dict((k, v) for k, v in self._old.iteritems() 1043 if getattr(self, k) != v) 1044 self._to_old() 1045 for listener in TicketSystem (self.env ).milestone_change_listeners : 1046 listener.milestone_changed (self, old_values) 1047 1048 @classmethod
1049 - def select (cls, env, include_completed=True, db=None):
1050 if not db : 1051 db = env .get_read_db () 1052 sql = "SELECT name,due,completed,description FROM milestone " 1053 if not include_completed: 1054 sql += "WHERE COALESCE(completed,0)=0 " 1055 cursor = db .cursor () 1056 cursor .execute (sql ) 1057 milestones = [] 1058 for row in cursor : 1059 milestone = Milestone (env ) 1060 milestone._from_database(row) 1061 milestones.append(milestone) 1062 def milestone_order(m): 1063 return (m.completed or utcmax , 1064 m.due or utcmax , 1065 embedded_numbers (m.name ))
1066 return sorted(milestones, key=milestone_order) 1067
1068 1069 - def group_milestones (milestones, include_completed):
1070 """Group milestones into "open with due date", "open with no due date", 1071 and possibly "completed". Return a list of (label, milestones) tuples.""" 1072 def category(m): 1073 return m.is_completed and 1 or m.due and 2 or 3
1074 open_due_milestones, open_not_due_milestones, \ 1075 closed_milestones = partition ([(m, category(m)) 1076 for m in milestones], (2, 3, 1)) 1077 groups = [ 1078 (_ ('Open (by due date)'), open_due_milestones), 1079 (_ ('Open (no due date)'), open_not_due_milestones), 1080 ] 1081 if include_completed: 1082 groups.append((_ ('Closed'), closed_milestones)) 1083 return groups 1084
1085 1086 - class Version (object):
1087 - def __init__ (self, env, name=None, db=None):
1088 self.env = env 1089 if name : 1090 if not db : 1091 db = self.env .get_read_db () 1092 cursor = db .cursor () 1093 cursor .execute (""" 1094 SELECT time,description FROM version WHERE name=%s 1095 """, (name ,)) 1096 row = cursor .fetchone () 1097 if not row: 1098 raise ResourceNotFound (_ ('Version %(name)s does not exist.', 1099 name =name )) 1100 self.name = self._old_name = name 1101 self.time = row[0] and from_utimestamp (row[0]) or None 1102 self.description = row[1] or '' 1103 else: 1104 self.name = self._old_name = None 1105 self.time = None 1106 self.description = None
1107 1108 exists = property(lambda self: self._old_name is not None) 1109
1110 - def delete (self, db=None):
1111 """Delete the version. 1112 1113 The `db` argument is deprecated in favor of `with_transaction()`. 1114 """ 1115 assert self.exists , 'Cannot delete non-existent version' 1116 1117 @self.env .with_transaction (db ) 1118 def do_delete(db): 1119 cursor = db .cursor () 1120 self.env .log .info('Deleting version %s' % self.name ) 1121 cursor .execute ("DELETE FROM version WHERE name=%s", (self.name ,)) 1122 self.name = self._old_name = None 1123 TicketSystem (self.env ).reset_ticket_fields ()
1124
1125 - def insert (self, db=None):
1126 """Insert a new version. 1127 1128 The `db` argument is deprecated in favor of `with_transaction()`. 1129 """ 1130 assert not self.exists , 'Cannot insert existing version' 1131 self.name = simplify_whitespace(self.name ) 1132 if not self.name : 1133 raise TracError (_ ('Invalid version name.')) 1134 1135 @self.env .with_transaction (db ) 1136 def do_insert(db): 1137 cursor = db .cursor () 1138 self.env .log .debug("Creating new version '%s'" % self.name ) 1139 cursor .execute (""" 1140 INSERT INTO version (name,time,description) VALUES (%s,%s,%s) 1141 """, (self.name , to_utimestamp (self.time), self.description )) 1142 self._old_name = self.name 1143 TicketSystem (self.env ).reset_ticket_fields ()
1144
1145 - def update (self, db=None):
1146 """Update the version. 1147 1148 The `db` argument is deprecated in favor of `with_transaction()`. 1149 """ 1150 assert self.exists , 'Cannot update non-existent version' 1151 self.name = simplify_whitespace(self.name ) 1152 if not self.name : 1153 raise TracError (_ ('Invalid version name.')) 1154 1155 @self.env .with_transaction (db ) 1156 def do_update(db): 1157 cursor = db .cursor () 1158 self.env .log .info('Updating version "%s"' % self.name ) 1159 cursor .execute (""" 1160 UPDATE version SET name=%s,time=%s,description=%s WHERE name=%s 1161 """, (self.name , to_utimestamp (self.time), 1162 self.description , self._old_name)) 1163 if self.name != self._old_name: 1164 # Update tickets 1165 cursor .execute ("UPDATE ticket SET version=%s WHERE version=%s", 1166 (self.name , self._old_name)) 1167 self._old_name = self.name 1168 TicketSystem (self.env ).reset_ticket_fields ()
1169 1170 @classmethod
1171 - def select (cls, env, db=None):
1172 if not db : 1173 db = env .get_read_db () 1174 cursor = db .cursor () 1175 cursor .execute ("SELECT name,time,description FROM version") 1176 versions = [] 1177 for name , time, description in cursor : 1178 version = cls(env ) 1179 version .name = version ._old_name = name 1180 version .time = time and from_utimestamp (time) or None 1181 version .description = description or '' 1182 versions.append(version ) 1183 def version_order(v): 1184 return (v.time or utcmax , embedded_numbers (v.name ))
1185 return sorted(versions, key=version_order, reverse=True) 1186
Trees Indices Help
Trac
Generated by Epydoc 3.0.1 on Mon Feb 13 23:37:33 2023 http://epydoc.sourceforge.net

AltStyle によって変換されたページ (->オリジナル) /