39
|
1 # -*- coding: utf-8 -*-
|
|
2 #
|
|
3 # Copyright (C) 2003-2006 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@neuf.fr>
|
|
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.com/license.html.
|
|
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://projects.edgewall.com/trac/.
|
|
16 #
|
|
17 # Author: Jonas Borgström <jonas@edgewall.com>
|
|
18 # Christopher Lenz <cmlenz@gmx.de>
|
|
19
|
|
20 import time
|
|
21 import sys
|
|
22 import re
|
|
23
|
|
24 from trac.core import TracError
|
|
25 from trac.ticket import TicketSystem
|
|
26 from trac.util import sorted, embedded_numbers
|
|
27
|
|
28 __all__ = ['Ticket', 'Type', 'Status', 'Resolution', 'Priority', 'Severity',
|
|
29 'Component', 'Milestone', 'Version']
|
|
30
|
|
31
|
|
32 class Ticket(object):
|
|
33
|
|
34 def __init__(self, env, tkt_id=None, db=None):
|
|
35 self.env = env
|
|
36 self.fields = TicketSystem(self.env).get_ticket_fields()
|
|
37 self.values = {}
|
|
38 if tkt_id:
|
|
39 self._fetch_ticket(tkt_id, db)
|
|
40 else:
|
|
41 self._init_defaults(db)
|
|
42 self.id = self.time_created = self.time_changed = None
|
|
43 self._old = {}
|
|
44
|
|
45 def _get_db(self, db):
|
|
46 return db or self.env.get_db_cnx()
|
|
47
|
|
48 def _get_db_for_write(self, db):
|
|
49 if db:
|
|
50 return (db, False)
|
|
51 else:
|
|
52 return (self.env.get_db_cnx(), True)
|
|
53
|
|
54 exists = property(fget=lambda self: self.id is not None)
|
|
55
|
|
56 def _init_defaults(self, db=None):
|
|
57 for field in self.fields:
|
|
58 default = None
|
|
59 if not field.get('custom'):
|
|
60 default = self.env.config.get('ticket',
|
|
61 'default_' + field['name'])
|
|
62 else:
|
|
63 default = field.get('value')
|
|
64 options = field.get('options')
|
|
65 if default and options and default not in options:
|
|
66 try:
|
|
67 default_idx = int(default)
|
|
68 if default_idx > len(options):
|
|
69 raise ValueError
|
|
70 default = options[default_idx]
|
|
71 except ValueError:
|
|
72 self.env.log.warning('Invalid default value for '
|
|
73 'custom field "%s"'
|
|
74 % field['name'])
|
|
75 if default:
|
|
76 self.values.setdefault(field['name'], default)
|
|
77
|
|
78 def _fetch_ticket(self, tkt_id, db=None):
|
|
79 db = self._get_db(db)
|
|
80
|
|
81 # Fetch the standard ticket fields
|
|
82 std_fields = [f['name'] for f in self.fields if not f.get('custom')]
|
|
83 cursor = db.cursor()
|
|
84 cursor.execute("SELECT %s,time,changetime FROM ticket WHERE id=%%s"
|
|
85 % ','.join(std_fields), (tkt_id,))
|
|
86 row = cursor.fetchone()
|
|
87 if not row:
|
|
88 raise TracError('Ticket %d does not exist.' % tkt_id,
|
|
89 'Invalid Ticket Number')
|
|
90
|
|
91 self.id = tkt_id
|
|
92 for i in range(len(std_fields)):
|
|
93 self.values[std_fields[i]] = row[i] or ''
|
|
94 self.time_created = row[len(std_fields)]
|
|
95 self.time_changed = row[len(std_fields) + 1]
|
|
96
|
|
97 # Fetch custom fields if available
|
|
98 custom_fields = [f['name'] for f in self.fields if f.get('custom')]
|
|
99 cursor.execute("SELECT name,value FROM ticket_custom WHERE ticket=%s",
|
|
100 (tkt_id,))
|
|
101 for name, value in cursor:
|
|
102 if name in custom_fields:
|
|
103 self.values[name] = value
|
|
104
|
|
105 def __getitem__(self, name):
|
|
106 return self.values[name]
|
|
107
|
|
108 def __setitem__(self, name, value):
|
|
109 """Log ticket modifications so the table ticket_change can be updated"""
|
|
110 if self.values.has_key(name) and self.values[name] == value:
|
|
111 return
|
|
112 if not self._old.has_key(name): # Changed field
|
|
113 self._old[name] = self.values.get(name)
|
|
114 elif self._old[name] == value: # Change of field reverted
|
|
115 del self._old[name]
|
|
116 if value:
|
|
117 field = [field for field in self.fields if field['name'] == name]
|
|
118 if field and field[0].get('type') != 'textarea':
|
|
119 value = value.strip()
|
|
120 self.values[name] = value
|
|
121
|
|
122 def populate(self, values):
|
|
123 """Populate the ticket with 'suitable' values from a dictionary"""
|
|
124 field_names = [f['name'] for f in self.fields]
|
|
125 for name in [name for name in values.keys() if name in field_names]:
|
|
126 self[name] = values.get(name, '')
|
|
127
|
|
128 # We have to do an extra trick to catch unchecked checkboxes
|
|
129 for name in [name for name in values.keys() if name[9:] in field_names
|
|
130 and name.startswith('checkbox_')]:
|
|
131 if not values.has_key(name[9:]):
|
|
132 self[name[9:]] = '0'
|
|
133
|
|
134 def insert(self, when=0, db=None):
|
|
135 """Add ticket to database"""
|
|
136 assert not self.exists, 'Cannot insert an existing ticket'
|
|
137 db, handle_ta = self._get_db_for_write(db)
|
|
138
|
|
139 # Add a timestamp
|
|
140 if not when:
|
|
141 when = int(time.time())
|
|
142 self.time_created = self.time_changed = when
|
|
143
|
|
144 cursor = db.cursor()
|
|
145
|
|
146 # The owner field defaults to the component owner
|
|
147 if self.values.get('component') and not self.values.get('owner'):
|
|
148 try:
|
|
149 component = Component(self.env, self['component'], db=db)
|
|
150 if component.owner:
|
|
151 self['owner'] = component.owner
|
|
152 except TracError, e:
|
|
153 # Assume that no such component exists
|
|
154 pass
|
|
155
|
|
156 # Insert ticket record
|
|
157 std_fields = [f['name'] for f in self.fields if not f.get('custom')
|
|
158 and self.values.has_key(f['name'])]
|
|
159 cursor.execute("INSERT INTO ticket (%s,time,changetime) VALUES (%s)"
|
|
160 % (','.join(std_fields),
|
|
161 ','.join(['%s'] * (len(std_fields) + 2))),
|
|
162 [self[name] for name in std_fields] +
|
|
163 [self.time_created, self.time_changed])
|
|
164 tkt_id = db.get_last_id(cursor, 'ticket')
|
|
165
|
|
166 # Insert custom fields
|
|
167 custom_fields = [f['name'] for f in self.fields if f.get('custom')
|
|
168 and self.values.has_key(f['name'])]
|
|
169 if custom_fields:
|
|
170 cursor.executemany("INSERT INTO ticket_custom (ticket,name,value) "
|
|
171 "VALUES (%s,%s,%s)", [(tkt_id, name, self[name])
|
|
172 for name in custom_fields])
|
|
173
|
|
174 if handle_ta:
|
|
175 db.commit()
|
|
176
|
|
177 self.id = tkt_id
|
|
178 self._old = {}
|
|
179
|
|
180 for listener in TicketSystem(self.env).change_listeners:
|
|
181 listener.ticket_created(self)
|
|
182
|
|
183 return self.id
|
|
184
|
|
185 def save_changes(self, author, comment, when=0, db=None, cnum=''):
|
|
186 """
|
|
187 Store ticket changes in the database. The ticket must already exist in
|
|
188 the database.
|
|
189 """
|
|
190 assert self.exists, 'Cannot update a new ticket'
|
|
191
|
|
192 if not self._old and not comment:
|
|
193 return # Not modified
|
|
194
|
|
195 db, handle_ta = self._get_db_for_write(db)
|
|
196 cursor = db.cursor()
|
|
197 when = int(when or time.time())
|
|
198
|
|
199 if self.values.has_key('component'):
|
|
200 # If the component is changed on a 'new' ticket then owner field
|
|
201 # is updated accordingly. (#623).
|
|
202 if self.values.get('status') == 'new' \
|
|
203 and self._old.has_key('component') \
|
|
204 and not self._old.has_key('owner'):
|
|
205 try:
|
|
206 old_comp = Component(self.env, self._old['component'], db)
|
|
207 old_owner = old_comp.owner or ''
|
|
208 current_owner = self.values.get('owner') or ''
|
|
209 if old_owner == current_owner:
|
|
210 new_comp = Component(self.env, self['component'], db)
|
|
211 self['owner'] = new_comp.owner
|
|
212 except TracError, e:
|
|
213 # If the old component has been removed from the database we
|
|
214 # just leave the owner as is.
|
|
215 pass
|
|
216
|
|
217 # Fix up cc list separators and remove duplicates
|
|
218 if self.values.has_key('cc'):
|
|
219 cclist = []
|
|
220 for cc in re.split(r'[;,\s]+', self.values['cc']):
|
|
221 if cc not in cclist:
|
|
222 cclist.append(cc)
|
|
223 self.values['cc'] = ', '.join(cclist)
|
|
224
|
|
225 custom_fields = [f['name'] for f in self.fields if f.get('custom')]
|
|
226 for name in self._old.keys():
|
|
227 if name in custom_fields:
|
|
228 cursor.execute("SELECT * FROM ticket_custom "
|
|
229 "WHERE ticket=%s and name=%s", (self.id, name))
|
|
230 if cursor.fetchone():
|
|
231 cursor.execute("UPDATE ticket_custom SET value=%s "
|
|
232 "WHERE ticket=%s AND name=%s",
|
|
233 (self[name], self.id, name))
|
|
234 else:
|
|
235 cursor.execute("INSERT INTO ticket_custom (ticket,name,"
|
|
236 "value) VALUES(%s,%s,%s)",
|
|
237 (self.id, name, self[name]))
|
|
238 else:
|
|
239 cursor.execute("UPDATE ticket SET %s=%%s WHERE id=%%s" % name,
|
|
240 (self[name], self.id))
|
|
241 cursor.execute("INSERT INTO ticket_change "
|
|
242 "(ticket,time,author,field,oldvalue,newvalue) "
|
|
243 "VALUES (%s, %s, %s, %s, %s, %s)",
|
|
244 (self.id, when, author, name, self._old[name],
|
|
245 self[name]))
|
|
246 # always save comment, even if empty (numbering support for timeline)
|
|
247 cursor.execute("INSERT INTO ticket_change "
|
|
248 "(ticket,time,author,field,oldvalue,newvalue) "
|
|
249 "VALUES (%s,%s,%s,'comment',%s,%s)",
|
|
250 (self.id, when, author, cnum, comment))
|
|
251
|
|
252 cursor.execute("UPDATE ticket SET changetime=%s WHERE id=%s",
|
|
253 (when, self.id))
|
|
254
|
|
255 if handle_ta:
|
|
256 db.commit()
|
|
257 self._old = {}
|
|
258 self.time_changed = when
|
|
259
|
|
260 for listener in TicketSystem(self.env).change_listeners:
|
|
261 listener.ticket_changed(self, comment, self._old)
|
|
262
|
|
263 def get_changelog(self, when=0, db=None):
|
|
264 """Return the changelog as a list of tuples of the form
|
|
265 (time, author, field, oldvalue, newvalue, permanent).
|
|
266
|
|
267 While the other tuple elements are quite self-explanatory,
|
|
268 the `permanent` flag is used to distinguish collateral changes
|
|
269 that are not yet immutable (like attachments, currently).
|
|
270 """
|
|
271 db = self._get_db(db)
|
|
272 cursor = db.cursor()
|
|
273 if when:
|
|
274 cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 "
|
|
275 "FROM ticket_change WHERE ticket=%s AND time=%s "
|
|
276 "UNION "
|
|
277 "SELECT time,author,'attachment',null,filename,0 "
|
|
278 "FROM attachment WHERE id=%s AND time=%s "
|
|
279 "UNION "
|
|
280 "SELECT time,author,'comment',null,description,0 "
|
|
281 "FROM attachment WHERE id=%s AND time=%s "
|
|
282 "ORDER BY time",
|
|
283 (self.id, when, str(self.id), when, self.id, when))
|
|
284 else:
|
|
285 cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 "
|
|
286 "FROM ticket_change WHERE ticket=%s "
|
|
287 "UNION "
|
|
288 "SELECT time,author,'attachment',null,filename,0 "
|
|
289 "FROM attachment WHERE id=%s "
|
|
290 "UNION "
|
|
291 "SELECT time,author,'comment',null,description,0 "
|
|
292 "FROM attachment WHERE id=%s "
|
|
293 "ORDER BY time", (self.id, str(self.id), self.id))
|
|
294 log = []
|
|
295 for t, author, field, oldvalue, newvalue, permanent in cursor:
|
|
296 log.append((int(t), author, field, oldvalue or '', newvalue or '',
|
|
297 permanent))
|
|
298 return log
|
|
299
|
|
300 def delete(self, db=None):
|
|
301 db, handle_ta = self._get_db_for_write(db)
|
|
302 cursor = db.cursor()
|
|
303 cursor.execute("DELETE FROM ticket WHERE id=%s", (self.id,))
|
|
304 cursor.execute("DELETE FROM ticket_change WHERE ticket=%s", (self.id,))
|
|
305 cursor.execute("DELETE FROM attachment "
|
|
306 " WHERE type='ticket' and id=%s", (self.id,))
|
|
307 cursor.execute("DELETE FROM ticket_custom WHERE ticket=%s", (self.id,))
|
|
308
|
|
309 if handle_ta:
|
|
310 db.commit()
|
|
311
|
|
312 for listener in TicketSystem(self.env).change_listeners:
|
|
313 listener.ticket_deleted(self)
|
|
314
|
|
315
|
|
316 class AbstractEnum(object):
|
|
317 type = None
|
|
318 ticket_col = None
|
|
319
|
|
320 def __init__(self, env, name=None, db=None):
|
|
321 if not self.ticket_col:
|
|
322 self.ticket_col = self.type
|
|
323 self.env = env
|
|
324 if name:
|
|
325 if not db:
|
|
326 db = self.env.get_db_cnx()
|
|
327 cursor = db.cursor()
|
|
328 cursor.execute("SELECT value FROM enum WHERE type=%s AND name=%s",
|
|
329 (self.type, name))
|
|
330 row = cursor.fetchone()
|
|
331 if not row:
|
|
332 raise TracError, '%s %s does not exist.' % (self.type, name)
|
|
333 self.value = self._old_value = row[0]
|
|
334 self.name = self._old_name = name
|
|
335 else:
|
|
336 self.value = self._old_value = None
|
|
337 self.name = self._old_name = None
|
|
338
|
|
339 exists = property(fget=lambda self: self._old_value is not None)
|
|
340
|
|
341 def delete(self, db=None):
|
|
342 assert self.exists, 'Cannot deleting non-existent %s' % self.type
|
|
343 if not db:
|
|
344 db = self.env.get_db_cnx()
|
|
345 handle_ta = True
|
|
346 else:
|
|
347 handle_ta = False
|
|
348
|
|
349 cursor = db.cursor()
|
|
350 self.env.log.info('Deleting %s %s' % (self.type, self.name))
|
|
351 cursor.execute("DELETE FROM enum WHERE type=%s AND value=%s",
|
|
352 (self.type, self._old_value))
|
|
353
|
|
354 if handle_ta:
|
|
355 db.commit()
|
|
356 self.value = self._old_value = None
|
|
357 self.name = self._old_name = None
|
|
358
|
|
359 def insert(self, db=None):
|
|
360 assert not self.exists, 'Cannot insert existing %s' % self.type
|
|
361 assert self.name, 'Cannot create %s with no name' % self.type
|
|
362 self.name = self.name.strip()
|
|
363 if not db:
|
|
364 db = self.env.get_db_cnx()
|
|
365 handle_ta = True
|
|
366 else:
|
|
367 handle_ta = False
|
|
368
|
|
369 cursor = db.cursor()
|
|
370 self.env.log.debug("Creating new %s '%s'" % (self.type, self.name))
|
|
371 if not self.value:
|
|
372 cursor.execute(("SELECT COALESCE(MAX(%s),0) FROM enum "
|
|
373 "WHERE type=%%s") % db.cast('value', 'int'),
|
|
374 (self.type,))
|
|
375 self.value = int(float(cursor.fetchone()[0])) + 1
|
|
376 cursor.execute("INSERT INTO enum (type,name,value) VALUES (%s,%s,%s)",
|
|
377 (self.type, self.name, self.value))
|
|
378
|
|
379 if handle_ta:
|
|
380 db.commit()
|
|
381 self._old_name = self.name
|
|
382 self._old_value = self.value
|
|
383
|
|
384 def update(self, db=None):
|
|
385 assert self.exists, 'Cannot update non-existent %s' % self.type
|
|
386 assert self.name, 'Cannot update %s with no name' % self.type
|
|
387 self.name = self.name.strip()
|
|
388 if not db:
|
|
389 db = self.env.get_db_cnx()
|
|
390 handle_ta = True
|
|
391 else:
|
|
392 handle_ta = False
|
|
393
|
|
394 cursor = db.cursor()
|
|
395 self.env.log.info('Updating %s "%s"' % (self.type, self.name))
|
|
396 cursor.execute("UPDATE enum SET name=%s,value=%s "
|
|
397 "WHERE type=%s AND name=%s",
|
|
398 (self.name, self.value, self.type, self._old_name))
|
|
399 if self.name != self._old_name:
|
|
400 # Update tickets
|
|
401 cursor.execute("UPDATE ticket SET %s=%%s WHERE %s=%%s" %
|
|
402 (self.ticket_col, self.ticket_col),
|
|
403 (self.name, self._old_name))
|
|
404
|
|
405 if handle_ta:
|
|
406 db.commit()
|
|
407 self._old_name = self.name
|
|
408 self._old_value = self.value
|
|
409
|
|
410 def select(cls, env, db=None):
|
|
411 if not db:
|
|
412 db = env.get_db_cnx()
|
|
413 cursor = db.cursor()
|
|
414 cursor.execute("SELECT name,value FROM enum WHERE type=%s "
|
|
415 "ORDER BY value", (cls.type,))
|
|
416 for name, value in cursor:
|
|
417 obj = cls(env)
|
|
418 obj.name = obj._old_name = name
|
|
419 obj.value = obj._old_value = value
|
|
420 yield obj
|
|
421 select = classmethod(select)
|
|
422
|
|
423
|
|
424 class Type(AbstractEnum):
|
|
425 type = 'ticket_type'
|
|
426 ticket_col = 'type'
|
|
427
|
|
428
|
|
429 class Status(AbstractEnum):
|
|
430 type = 'status'
|
|
431
|
|
432
|
|
433 class Resolution(AbstractEnum):
|
|
434 type = 'resolution'
|
|
435
|
|
436
|
|
437 class Priority(AbstractEnum):
|
|
438 type = 'priority'
|
|
439
|
|
440
|
|
441 class Severity(AbstractEnum):
|
|
442 type = 'severity'
|
|
443
|
|
444
|
|
445 class Component(object):
|
|
446
|
|
447 def __init__(self, env, name=None, db=None):
|
|
448 self.env = env
|
|
449 if name:
|
|
450 if not db:
|
|
451 db = self.env.get_db_cnx()
|
|
452 cursor = db.cursor()
|
|
453 cursor.execute("SELECT owner,description FROM component "
|
|
454 "WHERE name=%s", (name,))
|
|
455 row = cursor.fetchone()
|
|
456 if not row:
|
|
457 raise TracError, 'Component %s does not exist.' % name
|
|
458 self.name = self._old_name = name
|
|
459 self.owner = row[0] or None
|
|
460 self.description = row[1] or ''
|
|
461 else:
|
|
462 self.name = self._old_name = None
|
|
463 self.owner = None
|
|
464 self.description = None
|
|
465
|
|
466 exists = property(fget=lambda self: self._old_name is not None)
|
|
467
|
|
468 def delete(self, db=None):
|
|
469 assert self.exists, 'Cannot deleting non-existent component'
|
|
470 if not db:
|
|
471 db = self.env.get_db_cnx()
|
|
472 handle_ta = True
|
|
473 else:
|
|
474 handle_ta = False
|
|
475
|
|
476 cursor = db.cursor()
|
|
477 self.env.log.info('Deleting component %s' % self.name)
|
|
478 cursor.execute("DELETE FROM component WHERE name=%s", (self.name,))
|
|
479
|
|
480 self.name = self._old_name = None
|
|
481
|
|
482 if handle_ta:
|
|
483 db.commit()
|
|
484
|
|
485 def insert(self, db=None):
|
|
486 assert not self.exists, 'Cannot insert existing component'
|
|
487 assert self.name, 'Cannot create component with no name'
|
|
488 self.name = self.name.strip()
|
|
489 if not db:
|
|
490 db = self.env.get_db_cnx()
|
|
491 handle_ta = True
|
|
492 else:
|
|
493 handle_ta = False
|
|
494
|
|
495 cursor = db.cursor()
|
|
496 self.env.log.debug("Creating new component '%s'" % self.name)
|
|
497 cursor.execute("INSERT INTO component (name,owner,description) "
|
|
498 "VALUES (%s,%s,%s)",
|
|
499 (self.name, self.owner, self.description))
|
|
500
|
|
501 if handle_ta:
|
|
502 db.commit()
|
|
503
|
|
504 def update(self, db=None):
|
|
505 assert self.exists, 'Cannot update non-existent component'
|
|
506 assert self.name, 'Cannot update component with no name'
|
|
507 self.name = self.name.strip()
|
|
508 if not db:
|
|
509 db = self.env.get_db_cnx()
|
|
510 handle_ta = True
|
|
511 else:
|
|
512 handle_ta = False
|
|
513
|
|
514 cursor = db.cursor()
|
|
515 self.env.log.info('Updating component "%s"' % self.name)
|
|
516 cursor.execute("UPDATE component SET name=%s,owner=%s,description=%s "
|
|
517 "WHERE name=%s",
|
|
518 (self.name, self.owner, self.description,
|
|
519 self._old_name))
|
|
520 if self.name != self._old_name:
|
|
521 # Update tickets
|
|
522 cursor.execute("UPDATE ticket SET component=%s WHERE component=%s",
|
|
523 (self.name, self._old_name))
|
|
524 self._old_name = self.name
|
|
525
|
|
526 if handle_ta:
|
|
527 db.commit()
|
|
528
|
|
529 def select(cls, env, db=None):
|
|
530 if not db:
|
|
531 db = env.get_db_cnx()
|
|
532 cursor = db.cursor()
|
|
533 cursor.execute("SELECT name,owner,description FROM component "
|
|
534 "ORDER BY name")
|
|
535 for name, owner, description in cursor:
|
|
536 component = cls(env)
|
|
537 component.name = name
|
|
538 component.owner = owner or None
|
|
539 component.description = description or ''
|
|
540 yield component
|
|
541 select = classmethod(select)
|
|
542
|
|
543
|
|
544 class Milestone(object):
|
|
545
|
|
546 def __init__(self, env, name=None, db=None):
|
|
547 self.env = env
|
|
548 if name:
|
|
549 self._fetch(name, db)
|
|
550 self._old_name = name
|
|
551 else:
|
|
552 self.name = self._old_name = None
|
|
553 self.due = self.completed = 0
|
|
554 self.description = ''
|
|
555
|
|
556 def _fetch(self, name, db=None):
|
|
557 if not db:
|
|
558 db = self.env.get_db_cnx()
|
|
559 cursor = db.cursor()
|
|
560 cursor.execute("SELECT name,due,completed,description "
|
|
561 "FROM milestone WHERE name=%s", (name,))
|
|
562 row = cursor.fetchone()
|
|
563 if not row:
|
|
564 raise TracError('Milestone %s does not exist.' % name,
|
|
565 'Invalid Milestone Name')
|
|
566 self.name = row[0]
|
|
567 self.due = row[1] and int(row[1]) or 0
|
|
568 self.completed = row[2] and int(row[2]) or 0
|
|
569 self.description = row[3] or ''
|
|
570
|
|
571 exists = property(fget=lambda self: self._old_name is not None)
|
|
572 is_completed = property(fget=lambda self: self.completed != 0)
|
|
573 is_late = property(fget=lambda self: self.due and \
|
|
574 self.due < time.time() - 86400)
|
|
575
|
|
576 def delete(self, retarget_to=None, author=None, db=None):
|
|
577 if not db:
|
|
578 db = self.env.get_db_cnx()
|
|
579 handle_ta = True
|
|
580 else:
|
|
581 handle_ta = False
|
|
582
|
|
583 cursor = db.cursor()
|
|
584 self.env.log.info('Deleting milestone %s' % self.name)
|
|
585 cursor.execute("DELETE FROM milestone WHERE name=%s", (self.name,))
|
|
586
|
|
587 # Retarget/reset tickets associated with this milestone
|
|
588 now = time.time()
|
|
589 cursor.execute("SELECT id FROM ticket WHERE milestone=%s", (self.name,))
|
|
590 tkt_ids = [int(row[0]) for row in cursor]
|
|
591 for tkt_id in tkt_ids:
|
|
592 ticket = Ticket(self.env, tkt_id, db)
|
|
593 ticket['milestone'] = retarget_to
|
|
594 ticket.save_changes(author, 'Milestone %s deleted' % self.name,
|
|
595 now, db=db)
|
|
596
|
|
597 if handle_ta:
|
|
598 db.commit()
|
|
599
|
|
600 def insert(self, db=None):
|
|
601 assert self.name, 'Cannot create milestone with no name'
|
|
602 self.name = self.name.strip()
|
|
603 if not db:
|
|
604 db = self.env.get_db_cnx()
|
|
605 handle_ta = True
|
|
606 else:
|
|
607 handle_ta = False
|
|
608
|
|
609 cursor = db.cursor()
|
|
610 self.env.log.debug("Creating new milestone '%s'" % self.name)
|
|
611 cursor.execute("INSERT INTO milestone (name,due,completed,description) "
|
|
612 "VALUES (%s,%s,%s,%s)",
|
|
613 (self.name, self.due, self.completed, self.description))
|
|
614
|
|
615 if handle_ta:
|
|
616 db.commit()
|
|
617
|
|
618 def update(self, db=None):
|
|
619 assert self.name, 'Cannot update milestone with no name'
|
|
620 self.name = self.name.strip()
|
|
621 if not db:
|
|
622 db = self.env.get_db_cnx()
|
|
623 handle_ta = True
|
|
624 else:
|
|
625 handle_ta = False
|
|
626
|
|
627 cursor = db.cursor()
|
|
628 self.env.log.info('Updating milestone "%s"' % self.name)
|
|
629 cursor.execute("UPDATE milestone SET name=%s,due=%s,"
|
|
630 "completed=%s,description=%s WHERE name=%s",
|
|
631 (self.name, self.due, self.completed, self.description,
|
|
632 self._old_name))
|
|
633 self.env.log.info('Updating milestone field of all tickets '
|
|
634 'associated with milestone "%s"' % self.name)
|
|
635 cursor.execute("UPDATE ticket SET milestone=%s WHERE milestone=%s",
|
|
636 (self.name, self._old_name))
|
|
637 self._old_name = self.name
|
|
638
|
|
639 if handle_ta:
|
|
640 db.commit()
|
|
641
|
|
642 def select(cls, env, include_completed=True, db=None):
|
|
643 if not db:
|
|
644 db = env.get_db_cnx()
|
|
645 sql = "SELECT name,due,completed,description FROM milestone "
|
|
646 if not include_completed:
|
|
647 sql += "WHERE COALESCE(completed,0)=0 "
|
|
648 cursor = db.cursor()
|
|
649 cursor.execute(sql)
|
|
650 milestones = []
|
|
651 for name,due,completed,description in cursor:
|
|
652 milestone = Milestone(env)
|
|
653 milestone.name = milestone._old_name = name
|
|
654 milestone.due = due and int(due) or 0
|
|
655 milestone.completed = completed and int(completed) or 0
|
|
656 milestone.description = description or ''
|
|
657 milestones.append(milestone)
|
|
658 def milestone_order(m):
|
|
659 return (m.completed or sys.maxint,
|
|
660 m.due or sys.maxint,
|
|
661 embedded_numbers(m.name))
|
|
662 return sorted(milestones, key=milestone_order)
|
|
663 select = classmethod(select)
|
|
664
|
|
665
|
|
666 class Version(object):
|
|
667
|
|
668 def __init__(self, env, name=None, db=None):
|
|
669 self.env = env
|
|
670 if name:
|
|
671 if not db:
|
|
672 db = self.env.get_db_cnx()
|
|
673 cursor = db.cursor()
|
|
674 cursor.execute("SELECT time,description FROM version "
|
|
675 "WHERE name=%s", (name,))
|
|
676 row = cursor.fetchone()
|
|
677 if not row:
|
|
678 raise TracError, 'Version %s does not exist.' % name
|
|
679 self.name = self._old_name = name
|
|
680 self.time = row[0] and int(row[0]) or None
|
|
681 self.description = row[1] or ''
|
|
682 else:
|
|
683 self.name = self._old_name = None
|
|
684 self.time = None
|
|
685 self.description = None
|
|
686
|
|
687 exists = property(fget=lambda self: self._old_name is not None)
|
|
688
|
|
689 def delete(self, db=None):
|
|
690 assert self.exists, 'Cannot deleting non-existent version'
|
|
691 if not db:
|
|
692 db = self.env.get_db_cnx()
|
|
693 handle_ta = True
|
|
694 else:
|
|
695 handle_ta = False
|
|
696
|
|
697 cursor = db.cursor()
|
|
698 self.env.log.info('Deleting version %s' % self.name)
|
|
699 cursor.execute("DELETE FROM version WHERE name=%s", (self.name,))
|
|
700
|
|
701 self.name = self._old_name = None
|
|
702
|
|
703 if handle_ta:
|
|
704 db.commit()
|
|
705
|
|
706 def insert(self, db=None):
|
|
707 assert not self.exists, 'Cannot insert existing version'
|
|
708 assert self.name, 'Cannot create version with no name'
|
|
709 self.name = self.name.strip()
|
|
710 if not db:
|
|
711 db = self.env.get_db_cnx()
|
|
712 handle_ta = True
|
|
713 else:
|
|
714 handle_ta = False
|
|
715
|
|
716 cursor = db.cursor()
|
|
717 self.env.log.debug("Creating new version '%s'" % self.name)
|
|
718 cursor.execute("INSERT INTO version (name,time,description) "
|
|
719 "VALUES (%s,%s,%s)",
|
|
720 (self.name, self.time, self.description))
|
|
721
|
|
722 if handle_ta:
|
|
723 db.commit()
|
|
724
|
|
725 def update(self, db=None):
|
|
726 assert self.exists, 'Cannot update non-existent version'
|
|
727 assert self.name, 'Cannot update version with no name'
|
|
728 self.name = self.name.strip()
|
|
729 if not db:
|
|
730 db = self.env.get_db_cnx()
|
|
731 handle_ta = True
|
|
732 else:
|
|
733 handle_ta = False
|
|
734
|
|
735 cursor = db.cursor()
|
|
736 self.env.log.info('Updating version "%s"' % self.name)
|
|
737 cursor.execute("UPDATE version SET name=%s,time=%s,description=%s "
|
|
738 "WHERE name=%s",
|
|
739 (self.name, self.time, self.description,
|
|
740 self._old_name))
|
|
741 if self.name != self._old_name:
|
|
742 # Update tickets
|
|
743 cursor.execute("UPDATE ticket SET version=%s WHERE version=%s",
|
|
744 (self.name, self._old_name))
|
|
745 self._old_name = self.name
|
|
746
|
|
747 if handle_ta:
|
|
748 db.commit()
|
|
749
|
|
750 def select(cls, env, db=None):
|
|
751 if not db:
|
|
752 db = env.get_db_cnx()
|
|
753 cursor = db.cursor()
|
|
754 cursor.execute("SELECT name,time,description FROM version")
|
|
755 versions = []
|
|
756 for name, time, description in cursor:
|
|
757 version = cls(env)
|
|
758 version.name = name
|
|
759 version.time = time and int(time) or None
|
|
760 version.description = description or ''
|
|
761 versions.append(version)
|
|
762 def version_order(v):
|
|
763 return (v.time or sys.maxint, embedded_numbers(v.name))
|
|
764 return sorted(versions, key=version_order, reverse=True)
|
|
765 select = classmethod(select)
|