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
25 from rcs import rcs_by_name
28 ### Define and describe valid bug categories
29 # Use a tuple of (category, description) tuples since we don't have
30 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
32 # in order of increasing severity
33 severity_level_def = (
34 ("wishlist","A feature that could improve usefullness, but not a bug."),
35 ("minor","The standard bug level."),
36 ("serious","A bug that requires workarounds."),
37 ("critical","A bug that prevents some features from working at all."),
38 ("fatal","A bug that makes the package unusable."))
40 # in order of increasing resolution
41 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
43 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
44 ("open","A working bug that has not been assigned to a developer."),
45 ("assigned","A working bug that has been assigned to a developer."),
46 ("test","The code has been adjusted, but the fix is still being tested."))
47 inactive_status_def = (
48 ("closed", "The bug is no longer relevant."),
49 ("fixed", "The bug should no longer occur."),
50 ("wontfix","It's not a bug, it's a feature."),
54 ### Convert the description tuples to more useful formats
56 severity_values = tuple([val for val,description in severity_level_def])
57 severity_description = dict(severity_level_def)
59 for i in range(len(severity_values)):
60 severity_index[severity_values[i]] = i
62 active_status_values = tuple(val for val,description in active_status_def)
63 inactive_status_values = tuple(val for val,description in inactive_status_def)
64 status_values = active_status_values + inactive_status_values
65 status_description = dict(active_status_def+inactive_status_def)
67 for i in range(len(status_values)):
68 status_index[status_values[i]] = i
71 def checked_property(name, valid):
73 Provide access to an attribute name, testing for valid values.
76 value = getattr(self, "_"+name)
77 if value not in valid:
78 raise InvalidValue(name, value)
81 def setter(self, value):
82 if value not in valid:
83 raise InvalidValue(name, value)
84 return setattr(self, "_"+name, value)
85 return property(getter, setter)
89 severity = checked_property("severity", severity_values)
90 status = checked_property("status", status_values)
92 def __init__(self, path, uuid, rcs_name, bugdir):
96 dict = mapfile.map_load(self.get_path("values"))
100 self.rcs_name = rcs_name
103 self.summary = dict.get("summary")
104 self.creator = dict.get("creator")
105 self.target = dict.get("target")
106 self.status = dict.get("status", "open")
107 self.severity = dict.get("severity", "minor")
108 self.assigned = dict.get("assigned")
109 self.time = dict.get("time")
110 if self.time is not None:
111 self.time = utility.str_to_time(self.time)
114 return "Bug(uuid=%r)" % self.uuid
116 def string(self, bugs=None, shortlist=False):
118 bugs = list(self.bugdir.list())
119 short_name = cmdutil.unique_name(self, bugs)
120 if shortlist == False:
121 htime = utility.handy_time(self.time)
122 ftime = utility.time_to_str(self.time)
123 info = [("ID", self.uuid),
124 ("Short name", short_name),
125 ("Severity", self.severity),
126 ("Status", self.status),
127 ("Assigned", self.assigned),
128 ("Target", self.target),
129 ("Creator", self.creator),
130 ("Created", "%s (%s)" % (htime, ftime))]
134 newinfo.append((k,""))
136 newinfo.append((k,v))
138 longest_key_len = max([len(k) for k,v in info])
139 infolines = [" %*s : %s\n" % (longest_key_len,k,v) for k,v in info]
140 return "".join(infolines) + "%s\n" % self.summary
142 statuschar = self.status[0]
143 severitychar = self.severity[0]
144 chars = "%c%c" % (statuschar, severitychar)
145 return "%s:%s: %s\n" % (short_name, chars, self.summary)
147 return self.string(shortlist=True)
148 def get_path(self, file):
149 return os.path.join(self.path, self.uuid, file)
151 def _get_active(self):
152 return self.status in active_status_values
154 active = property(_get_active)
156 def add_attr(self, map, name):
157 value = getattr(self, name)
158 if value is not None:
163 self.add_attr(map, "assigned")
164 self.add_attr(map, "summary")
165 self.add_attr(map, "creator")
166 self.add_attr(map, "target")
167 self.add_attr(map, "status")
168 self.add_attr(map, "severity")
169 if self.time is not None:
170 map["time"] = utility.time_to_str(self.time)
171 path = self.get_path("values")
172 mapfile.map_save(rcs_by_name(self.rcs_name), path, map)
175 return rcs_by_name(self.rcs_name)
177 rcs = property(_get_rcs)
179 def new_comment(self):
180 if not os.path.exists(self.get_path("comments")):
181 self.rcs.mkdir(self.get_path("comments"))
182 comm = Comment(None, self)
183 comm.uuid = names.uuid()
186 def get_comment(self, uuid):
187 return Comment(uuid, self)
189 def iter_comment_ids(self):
190 path = self.get_path("comments")
191 if not os.path.isdir(path):
194 for uuid in os.listdir(path):
195 if (uuid.startswith('.')):
199 if e.errno != errno.ENOENT:
203 def list_comments(self):
204 comments = [Comment(id, self) for id in self.iter_comment_ids()]
205 comments.sort(cmp_time)
208 def new_bug(dir, uuid=None):
209 bug = dir.new_bug(uuid)
210 bug.creator = names.creator()
211 bug.severity = "minor"
213 bug.time = time.time()
216 def new_comment(bug, body=None):
217 comm = bug.new_comment()
218 comm.From = names.creator()
219 comm.time = time.time()
223 def add_headers(obj, map, names):
226 map_names[name] = pyname_to_header(name)
227 add_attrs(obj, map, names, map_names)
229 def add_attrs(obj, map, names, map_names=None):
230 if map_names is None:
233 map_names[name] = name
236 value = getattr(obj, name)
237 if value is not None:
238 map[map_names[name]] = value
241 class Comment(object):
242 def __init__(self, uuid, bug):
243 object.__init__(self)
246 if self.uuid is not None and self.bug is not None:
247 map = mapfile.map_load(self.get_path("values"))
248 self.time = utility.str_to_time(map["Date"])
249 self.From = map["From"]
250 self.in_reply_to = map.get("In-reply-to")
251 self.content_type = map.get("Content-type", "text/plain")
252 self.body = file(self.get_path("body")).read().decode("utf-8")
256 self.in_reply_to = None
257 self.content_type = "text/plain"
261 map_file = {"Date": utility.time_to_str(self.time)}
262 add_headers(self, map_file, ("From", "in_reply_to", "content_type"))
263 if not os.path.exists(self.get_path(None)):
264 self.bug.rcs.mkdir(self.get_path(None))
265 mapfile.map_save(self.bug.rcs, self.get_path("values"), map_file)
266 self.bug.rcs.set_file_contents(self.get_path("body"),
267 self.body.encode('utf-8'))
270 def get_path(self, name):
271 my_dir = os.path.join(self.bug.get_path("comments"), self.uuid)
274 return os.path.join(my_dir, name)
277 def thread_comments(comments):
280 for comment in comments:
281 child_map[comment.uuid] = []
282 for comment in comments:
283 if comment.in_reply_to is None or comment.in_reply_to not in child_map:
284 top_comments.append(comment)
286 child_map[comment.in_reply_to].append(comment)
288 def recurse_children(comment):
290 for child in child_map[comment.uuid]:
291 child_list.append(recurse_children(child))
292 return (comment, child_list)
293 return [recurse_children(c) for c in top_comments]
295 def pyname_to_header(name):
296 return name.capitalize().replace('_', '-')
301 def __init__(self, attr, value):
302 setattr(self, attr, value)
304 # the general rule for bug sorting is that "more important" bugs are
305 # less than "less important" bugs. This way sorting a list of bugs
306 # will put the most important bugs first in the list. When relative
307 # importance is unclear, the sorting follows some arbitrary convention
308 # (i.e. dictionary order).
310 def cmp_severity(bug_1, bug_2):
312 Compare the severity levels of two bugs, with more severe bugs comparing
316 >>> cmp_severity(MockBug(attr,"wishlist"), MockBug(attr,"wishlist")) == 0
318 >>> cmp_severity(MockBug(attr,"wishlist"), MockBug(attr,"minor")) > 0
320 >>> cmp_severity(MockBug(attr,"critical"), MockBug(attr,"wishlist")) < 0
323 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
325 def cmp_status(bug_1, bug_2):
327 Compare the status levels of two bugs, with more 'open' bugs
331 >>> cmp_status(MockBug(attr,"open"), MockBug(attr,"open")) == 0
333 >>> cmp_status(MockBug(attr,"open"), MockBug(attr,"closed")) < 0
335 >>> cmp_status(MockBug(attr,"closed"), MockBug(attr,"open")) > 0
338 val_2 = status_index[bug_2.status]
339 return cmp(status_index[bug_1.status], status_index[bug_2.status])
341 def cmp_attr(bug_1, bug_2, attr, invert=False):
343 Compare a general attribute between two bugs using the conventional
344 comparison rule for that attribute type. If invert == True, sort
345 *against* that convention.
347 >>> cmp_attr(MockBug(attr,1), MockBug(attr,2), attr, invert=False) < 0
349 >>> cmp_attr(MockBug(attr,1), MockBug(attr,2), attr, invert=True) > 0
351 >>> cmp_attr(MockBug(attr,1), MockBug(attr,1), attr) == 0
355 return -cmp(getattr(bug_1, attr), getattr(bug_2, attr))
357 return cmp(getattr(bug_1, attr), getattr(bug_2, attr))
359 # alphabetical rankings (a < z)
360 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
361 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
362 # chronological rankings (newer < older)
363 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
365 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
366 cmp_time,cmp_creator)):
367 for comparison in cmp_list :
368 val = comparison(bug_1, bug_2)
373 class InvalidValue(ValueError):
374 def __init__(self, name, value):
375 msg = "Cannot assign value %s to %s" % (value, name)
376 Exception.__init__(self, msg)