Package trac ::
Package ticket ::
Module 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']
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
46
47 # Fields that must not be modified directly by the user
48 protected_fields = ('resolution', 'status', 'time', 'changetime')
49
50 @staticmethod
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
76
77 exists = property(lambda self: self.id is not None)
78
100
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
140
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
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
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