1 # Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
2 # <abentley@panoramicfeedback.com>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 from beuuid import uuid_gen
29 ### Define and describe valid bug categories
30 # Use a tuple of (category, description) tuples since we don't have
31 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
33 # in order of increasing severity
34 severity_level_def = (
35 ("wishlist","A feature that could improve usefullness, but not a bug."),
36 ("minor","The standard bug level."),
37 ("serious","A bug that requires workarounds."),
38 ("critical","A bug that prevents some features from working at all."),
39 ("fatal","A bug that makes the package unusable."))
41 # in order of increasing resolution
42 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
44 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
45 ("open","A working bug that has not been assigned to a developer."),
46 ("assigned","A working bug that has been assigned to a developer."),
47 ("test","The code has been adjusted, but the fix is still being tested."))
48 inactive_status_def = (
49 ("closed", "The bug is no longer relevant."),
50 ("fixed", "The bug should no longer occur."),
51 ("wontfix","It's not a bug, it's a feature."),
55 ### Convert the description tuples to more useful formats
57 severity_values = tuple([val for val,description in severity_level_def])
58 severity_description = dict(severity_level_def)
60 for i in range(len(severity_values)):
61 severity_index[severity_values[i]] = i
63 active_status_values = tuple(val for val,description in active_status_def)
64 inactive_status_values = tuple(val for val,description in inactive_status_def)
65 status_values = active_status_values + inactive_status_values
66 status_description = dict(active_status_def+inactive_status_def)
68 for i in range(len(status_values)):
69 status_index[status_values[i]] = i
72 def checked_property(name, valid):
74 Provide access to an attribute name, testing for valid values.
77 value = getattr(self, "_"+name)
78 if value not in valid:
79 raise InvalidValue(name, value)
82 def setter(self, value):
83 if value not in valid:
84 raise InvalidValue(name, value)
85 return setattr(self, "_"+name, value)
86 return property(getter, setter)
90 severity = checked_property("severity", severity_values)
91 status = checked_property("status", status_values)
93 def _get_active(self):
94 return self.status in active_status_values
96 active = property(_get_active)
98 def __init__(self, bugdir=None, uuid=None, from_disk=False,
99 load_comments=False, summary=None):
102 self.rcs = bugdir.rcs
105 if from_disk == True:
106 self._comments_loaded = False
108 self.load(load_comments=load_comments)
110 # Note: defaults should match those in Bug.load()
111 self._comments_loaded = True
115 self.uuid = uuid_gen()
116 self.summary = summary
118 self.creator = self.rcs.get_user_id()
123 self.severity = "minor"
125 self.time = int(time.time()) # only save to second precision
126 self.comment_root = comment.Comment(self, uuid=comment.INVALID_UUID)
129 return "Bug(uuid=%r)" % self.uuid
131 def xml(self, show_comments=False):
132 if self.bugdir == None:
133 shortname = self.uuid
135 shortname = self.bugdir.bug_shortname(self)
137 if self.time == None:
140 htime = utility.handy_time(self.time)
141 ftime = utility.time_to_str(self.time)
142 timestring = "%s (%s)" % (htime, ftime)
144 info = [("uuid", self.uuid),
145 ("short-name", shortname),
146 ("severity", self.severity),
147 ("status", self.status),
148 ("assigned", self.assigned),
149 ("target", self.target),
150 ("creator", self.creator),
151 ("created", timestring),
152 ("summary", self.summary)]
156 ret += ' <%s>%s</%s>\n' % (k,v,k)
159 if self._comments_loaded == False:
161 comout = self.comment_root.xml_thread(auto_name_map=True,
162 bug_shortname=shortname)
168 def string(self, shortlist=False, show_comments=False):
169 if self.bugdir == None:
170 shortname = self.uuid
172 shortname = self.bugdir.bug_shortname(self)
173 if shortlist == False:
174 if self.time == None:
177 htime = utility.handy_time(self.time)
178 ftime = utility.time_to_str(self.time)
179 timestring = "%s (%s)" % (htime, ftime)
180 info = [("ID", self.uuid),
181 ("Short name", shortname),
182 ("Severity", self.severity),
183 ("Status", self.status),
184 ("Assigned", self.assigned),
185 ("Target", self.target),
186 ("Creator", self.creator),
187 ("Created", timestring)]
191 newinfo.append((k,""))
193 newinfo.append((k,v))
195 longest_key_len = max([len(k) for k,v in info])
196 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
197 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
199 statuschar = self.status[0]
200 severitychar = self.severity[0]
201 chars = "%c%c" % (statuschar, severitychar)
202 bugout = "%s:%s: %s" % (shortname, chars, self.summary.rstrip('\n'))
204 if show_comments == True:
205 if self._comments_loaded == False:
207 comout = self.comment_root.string_thread(auto_name_map=True,
208 bug_shortname=shortname)
209 output = bugout + '\n' + comout.rstrip('\n')
215 return self.string(shortlist=True)
217 def __cmp__(self, other):
218 return cmp_full(self, other)
220 def get_path(self, name=None):
221 my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
224 assert name in ["values", "comments"]
225 return os.path.join(my_dir, name)
227 def load(self, load_comments=False):
228 map = mapfile.map_load(self.rcs, self.get_path("values"))
229 self.summary = map.get("summary")
230 self.creator = map.get("creator")
231 self.target = map.get("target")
232 self.status = map.get("status", "open")
233 self.severity = map.get("severity", "minor")
234 self.assigned = map.get("assigned")
235 self.time = map.get("time")
236 if self.time is not None:
237 self.time = utility.str_to_time(self.time)
239 if load_comments == True:
242 def load_comments(self):
243 self.comment_root = comment.loadComments(self)
244 self._comments_loaded = True
247 if self._comments_loaded == False:
249 for comment in self.comment_root.traverse():
252 def _add_attr(self, map, name):
253 value = getattr(self, name)
254 if value is not None:
258 assert self.summary != None, "Can't save blank bug"
260 self._add_attr(map, "assigned")
261 self._add_attr(map, "summary")
262 self._add_attr(map, "creator")
263 self._add_attr(map, "target")
264 self._add_attr(map, "status")
265 self._add_attr(map, "severity")
266 if self.time is not None:
267 map["time"] = utility.time_to_str(self.time)
269 self.rcs.mkdir(self.get_path())
270 path = self.get_path("values")
271 mapfile.map_save(self.rcs, path, map)
273 if self._comments_loaded:
274 if len(self.comment_root) > 0:
275 self.rcs.mkdir(self.get_path("comments"))
276 comment.saveComments(self)
280 self.comment_root.remove()
281 path = self.get_path()
282 self.rcs.recursive_remove(path)
284 def new_comment(self, body=None):
285 comm = comment.comment_root.new_reply(body=body)
288 def comment_from_shortname(self, shortname, *args, **kwargs):
289 return self.comment_root.comment_from_shortname(shortname, *args, **kwargs)
291 def comment_from_uuid(self, uuid):
292 return self.comment_root.comment_from_uuid(uuid)
295 # the general rule for bug sorting is that "more important" bugs are
296 # less than "less important" bugs. This way sorting a list of bugs
297 # will put the most important bugs first in the list. When relative
298 # importance is unclear, the sorting follows some arbitrary convention
299 # (i.e. dictionary order).
301 def cmp_severity(bug_1, bug_2):
303 Compare the severity levels of two bugs, with more severe bugs
307 >>> bugA.severity = bugB.severity = "wishlist"
308 >>> cmp_severity(bugA, bugB) == 0
310 >>> bugB.severity = "minor"
311 >>> cmp_severity(bugA, bugB) > 0
313 >>> bugA.severity = "critical"
314 >>> cmp_severity(bugA, bugB) < 0
317 if not hasattr(bug_2, "severity") :
319 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
321 def cmp_status(bug_1, bug_2):
323 Compare the status levels of two bugs, with more 'open' bugs
327 >>> bugA.status = bugB.status = "open"
328 >>> cmp_status(bugA, bugB) == 0
330 >>> bugB.status = "closed"
331 >>> cmp_status(bugA, bugB) < 0
333 >>> bugA.status = "fixed"
334 >>> cmp_status(bugA, bugB) > 0
337 if not hasattr(bug_2, "status") :
339 val_2 = status_index[bug_2.status]
340 return cmp(status_index[bug_1.status], status_index[bug_2.status])
342 def cmp_attr(bug_1, bug_2, attr, invert=False):
344 Compare a general attribute between two bugs using the conventional
345 comparison rule for that attribute type. If invert == True, sort
346 *against* that convention.
350 >>> bugA.severity = "critical"
351 >>> bugB.severity = "wishlist"
352 >>> cmp_attr(bugA, bugB, attr) < 0
354 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
356 >>> bugB.severity = "critical"
357 >>> cmp_attr(bugA, bugB, attr) == 0
360 if not hasattr(bug_2, attr) :
363 return -cmp(getattr(bug_1, attr), getattr(bug_2, attr))
365 return cmp(getattr(bug_1, attr), getattr(bug_2, attr))
367 # alphabetical rankings (a < z)
368 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
369 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
370 # chronological rankings (newer < older)
371 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
373 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
374 cmp_time,cmp_creator)):
375 for comparison in cmp_list :
376 val = comparison(bug_1, bug_2)
381 class InvalidValue(ValueError):
382 def __init__(self, name, value):
383 msg = "Cannot assign value %s to %s" % (value, name)
384 Exception.__init__(self, msg)
388 suite = doctest.DocTestSuite()