1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import re
21
22 from trac import core
23 from trac.attachment import Attachment
24 from trac.cache import cached
25 from trac.core import TracError
26 from trac.resource import Resource, ResourceExistsError, ResourceNotFound
27 from trac.ticket.api import TicketSystem
28 from trac.util import as_int, embedded_numbers, to_list
29 from trac.util.datefmt import (datetime_now, from_utimestamp, parse_date,
30 to_utimestamp, utc, utcmax)
31 from trac.util.text import empty, stripws
32 from trac.util.translation import _, N_, gettext
33
34 __all__ = ['Ticket', 'Type', 'Status', 'Resolution', 'Priority', 'Severity',
35 'Component', 'Milestone', 'Version']
39 """Fix up cc list separators and remove duplicates."""
40 cclist = []
41 for cc in to_list(cc_value, r'[;,\s]+'):
42 if cc not in cclist:
43 cclist.append(cc)
44 return ', '.join(cclist)
45
48 if value is None:
49 return None
50 try:
51 return from_utimestamp(int(value))
52 except ValueError:
53 pass
54 try:
55 return parse_date(value.strip(), utc, 'datetime')
56 except Exception:
57 return None
58
61 if not dt:
62 return None
63 ts = to_utimestamp(dt)
64 if is_custom_field:
65
66 fmt = '%018d' if ts >= 0 else '%+017d'
67 return fmt % ts
68 else:
69 return ts
70
74
77 return value if value else None
78
81 return value if value else empty
82
85 """Strip spaces and remove duplicate spaces within names"""
86 if name:
87 return ' '.join(name.split())
88 return name
89
92 with env.db_query as db:
93 tickets = [int(id_) for id_ in ids]
94 holders = ','.join(['%s'] * len(tickets))
95 rows = db("""
96 SELECT id FROM ticket AS t
97 LEFT OUTER JOIN enum p
98 ON p.type='priority' AND p.name=t.priority
99 WHERE t.id IN (%s)
100 ORDER BY COALESCE(p.value,'')='', %s, t.id
101 """ % (holders, db.cast('p.value', 'int')), tickets)
102 return [row[0] for row in rows]
103
106
107 realm = 'ticket'
108
109
110
111 protected_fields = 'resolution', 'status', 'time', 'changetime'
112
113 @staticmethod
115 try:
116 return 0 < int(num) <= 1 << 31
117 except (ValueError, TypeError):
118 return False
119
120 @property
123
124 - def __init__(self, env, tkt_id=None, version=None):
146
148 return '<%s %r>' % (self.__class__.__name__, self.id)
149
150 exists = property(lambda self: self.id is not None)
151
165
186
188 row = None
189 if self.id_is_valid(tkt_id):
190
191 tkt_id = int(tkt_id)
192 for row in self.env.db_query("""
193 SELECT %s FROM ticket WHERE id=%%s
194 """ % ','.join(self.std_fields), (tkt_id,)):
195 break
196 if not row:
197 raise ResourceNotFound(_("Ticket %(id)s does not exist.",
198 id=tkt_id), _("Invalid ticket number"))
199
200 self.id = tkt_id
201 for i, field in enumerate(self.std_fields):
202 value = row[i]
203 if field in self.time_fields:
204 self.values[field] = from_utimestamp(value)
205 elif value is None:
206 self.values[field] = empty
207 else:
208 self.values[field] = value
209
210
211 for name, value in self.env.db_query("""
212 SELECT name, value FROM ticket_custom WHERE ticket=%s
213 """, (tkt_id,)):
214 if name in self.custom_fields:
215 if name in self.time_fields:
216 self.values[name] = _db_str_to_datetime(value)
217 elif value is None:
218 self.values[name] = empty
219 else:
220 self.values[name] = value
221
222
223 for field in self.fields:
224 name = field['name']
225 if field.get('custom') and name not in self.values:
226 default = self._custom_field_default(field)
227 if default:
228 self[name] = default
229
232
234 """Log ticket modifications so the table ticket_change can be updated
235 """
236 if value and name not in self.time_fields:
237 if isinstance(value, list):
238 raise TracError(_("Multi-values fields not supported yet"))
239 if self.fields.by_name(name, {}).get('type') != 'textarea':
240 value = value.strip()
241 if name in self.values and self.values[name] == value:
242 return
243 if name not in self._old:
244 self._old[name] = self.values.get(name)
245 elif self._old[name] == value:
246 del self._old[name]
247 self.values[name] = value
248
250 return item in self.values
251
253 """Return the value of a field or the default value if it is undefined
254 """
255 try:
256 value = self.values[name]
257 return value if value is not empty else self.get_default(name)
258 except KeyError:
259 pass
260
262 """Return the default value of a field."""
263 return self.fields.by_name(name, {}).get('value', '')
264
276
278 """Add ticket to database.
279 """
280 assert not self.exists, 'Cannot insert an existing ticket'
281
282 if 'cc' in self.values:
283 self['cc'] = _fixup_cc_list(self.values['cc'])
284
285
286 if when is None:
287 when = datetime_now(utc)
288 self.values['time'] = self.values['changetime'] = when
289
290
291 db_values = self._to_db_types(self.values)
292
293
294 std_fields = []
295 custom_fields = []
296 for f in self.fields:
297 fname = f['name']
298 if fname in self.values:
299 if f.get('custom'):
300 custom_fields.append(fname)
301 else:
302 std_fields.append(fname)
303 with self.env.db_transaction as db:
304 cursor = db.cursor()
305 cursor.execute("INSERT INTO ticket (%s) VALUES (%s)"
306 % (','.join(std_fields),
307 ','.join(['%s'] * len(std_fields))),
308 [db_values.get(name) for name in std_fields])
309 tkt_id = db.get_last_id(cursor, 'ticket')
310
311
312 if custom_fields:
313 db.executemany(
314 """INSERT INTO ticket_custom (ticket, name, value)
315 VALUES (%s, %s, %s)
316 """, [(tkt_id, c, db_values.get(c))
317 for c in custom_fields])
318
319 self.id = int(tkt_id)
320 self._old = {}
321
322 for listener in TicketSystem(self.env).change_listeners:
323 listener.ticket_created(self)
324
325 return self.id
326
338
339 - def save_changes(self, author=None, comment=None, when=None, replyto=None):
340 """
341 Store ticket changes in the database. The ticket must already exist in
342 the database. Returns False if there were no changes to save, True
343 otherwise.
344 """
345 assert self.exists, "Cannot update a new ticket"
346
347 if 'cc' in self.values:
348 self['cc'] = _fixup_cc_list(self.values['cc'])
349
350 props_unchanged = all(self.values.get(k) == v
351 for k, v in self._old.iteritems())
352 if (not comment or not stripws(comment)) and props_unchanged:
353 return False
354
355 if when is None:
356 when = datetime_now(utc)
357 self.values['changetime'] = when
358
359
360 db_values = self._to_db_types(self.values)
361 old_db_values = self._to_db_types(self._old)
362
363 with self.env.db_transaction as db:
364 db("UPDATE ticket SET changetime=%s WHERE id=%s",
365 (db_values['changetime'], self.id))
366
367 num = 0
368 for ts, old in db("""
369 SELECT DISTINCT tc1.time, COALESCE(tc2.oldvalue,'')
370 FROM ticket_change AS tc1
371 LEFT OUTER JOIN ticket_change AS tc2
372 ON tc2.ticket=%s AND tc2.time=tc1.time
373 AND tc2.field='comment'
374 WHERE tc1.ticket=%s ORDER BY tc1.time DESC
375 """, (self.id, self.id)):
376
377 try:
378 num += int(old.rsplit('.', 1)[-1])
379 break
380 except ValueError:
381 num += 1
382 cnum = str(num + 1)
383 if replyto:
384 cnum = '%s.%s' % (replyto, cnum)
385
386
387 for name in self._old:
388 db_val = db_values.get(name)
389 old_db_val = old_db_values.get(name)
390 if name in self.custom_fields:
391 for row in db("""SELECT * FROM ticket_custom
392 WHERE ticket=%s and name=%s
393 """, (self.id, name)):
394 db("""UPDATE ticket_custom SET value=%s
395 WHERE ticket=%s AND name=%s
396 """, (db_val, self.id, name))
397 break
398 else:
399 db("""INSERT INTO ticket_custom (ticket,name,value)
400 VALUES(%s,%s,%s)
401 """, (self.id, name, db_val))
402
403
404 if old_db_val is None:
405 field = self.fields.by_name(name)
406 default = self._custom_field_default(field)
407 if self.values.get(name) == default:
408 continue
409 else:
410 db("UPDATE ticket SET %s=%%s WHERE id=%%s"
411 % name, (db_val, self.id))
412 db("""INSERT INTO ticket_change
413 (ticket,time,author,field,oldvalue,newvalue)
414 VALUES (%s, %s, %s, %s, %s, %s)
415 """, (self.id, db_values['changetime'], author, name,
416 old_db_val, db_val))
417
418
419
420 db("""INSERT INTO ticket_change
421 (ticket,time,author,field,oldvalue,newvalue)
422 VALUES (%s,%s,%s,'comment',%s,%s)
423 """, (self.id, db_values['changetime'], author, cnum,
424 comment))
425
426 old_values = self._old
427 self._old = {}
428
429 for listener in TicketSystem(self.env).change_listeners:
430 listener.ticket_changed(self, comment, author, old_values)
431 return int(cnum.rsplit('.', 1)[-1])
432
434 values = values.copy()
435 for field, value in values.iteritems():
436 if field in self.time_fields:
437 is_custom_field = field in self.custom_fields
438 values[field] = _datetime_to_db_str(value, is_custom_field)
439 else:
440 values[field] = _to_null(value)
441 return values
442
444 """Return the changelog as a list of tuples of the form
445 (time, author, field, oldvalue, newvalue, permanent).
446
447 While the other tuple elements are quite self-explanatory,
448 the `permanent` flag is used to distinguish collateral changes
449 that are not yet immutable (like attachments, currently).
450 """
451 sid = str(self.id)
452 when_ts = to_utimestamp(when)
453 if when_ts:
454 sql = """
455 SELECT time, author, field, oldvalue, newvalue, 1 AS permanent
456 FROM ticket_change WHERE ticket=%s AND time=%s
457 UNION
458 SELECT time, author, 'attachment', null, filename,
459 0 AS permanent
460 FROM attachment WHERE type='ticket' AND id=%s AND time=%s
461 UNION
462 SELECT time, author, 'comment', null, description,
463 0 AS permanent
464 FROM attachment WHERE type='ticket' AND id=%s AND time=%s
465 ORDER BY time,permanent,author,field
466 """
467 args = (self.id, when_ts, sid, when_ts, sid, when_ts)
468 else:
469 sql = """
470 SELECT time, author, field, oldvalue, newvalue, 1 AS permanent
471 FROM ticket_change WHERE ticket=%s
472 UNION
473 SELECT time, author, 'attachment', null, filename,
474 0 AS permanent
475 FROM attachment WHERE type='ticket' AND id=%s
476 UNION
477 SELECT time, author, 'comment', null, description,
478 0 AS permanent
479 FROM attachment WHERE type='ticket' AND id=%s
480 ORDER BY time,permanent,author,field
481 """
482 args = (self.id, sid, sid)
483 log = []
484 for t, author, field, oldvalue, newvalue, permanent \
485 in self.env.db_query(sql, args):
486 if field in self.time_fields:
487 oldvalue = _db_str_to_datetime(oldvalue)
488 newvalue = _db_str_to_datetime(newvalue)
489 log.append((from_utimestamp(t), author, field,
490 oldvalue or '', newvalue or '', permanent))
491 return log
492
504
506 """Return a ticket change by its number or date.
507 """
508 if cdate is None:
509 row = self._find_change(cnum)
510 if not row:
511 return
512 cdate = from_utimestamp(row[0])