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
21 import xml.sax.saxutils
24 from beuuid import uuid_gen
25 from properties import Property, doc_property, local_property, \
26 defaulting_property, checked_property, cached_property, \
27 primed_property, change_hook_property, settings_property
28 import settings_object
34 ### Define and describe valid bug categories
35 # Use a tuple of (category, description) tuples since we don't have
36 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
38 # in order of increasing severity. (name, description) pairs
40 ("wishlist","A feature that could improve usefulness, but not a bug."),
41 ("minor","The standard bug level."),
42 ("serious","A bug that requires workarounds."),
43 ("critical","A bug that prevents some features from working at all."),
44 ("fatal","A bug that makes the package unusable."))
46 # in order of increasing resolution
47 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
49 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
50 ("open","A working bug that has not been assigned to a developer."),
51 ("assigned","A working bug that has been assigned to a developer."),
52 ("test","The code has been adjusted, but the fix is still being tested."))
53 inactive_status_def = (
54 ("closed", "The bug is no longer relevant."),
55 ("fixed", "The bug should no longer occur."),
56 ("wontfix","It's not a bug, it's a feature."))
59 ### Convert the description tuples to more useful formats
62 severity_description = {}
64 def load_severities(severity_def):
65 global severity_values
66 global severity_description
68 if severity_def == settings_object.EMPTY:
70 severity_values = tuple([val for val,description in severity_def])
71 severity_description = dict(severity_def)
73 for i,severity in enumerate(severity_values):
74 severity_index[severity] = i
75 load_severities(severity_def)
77 active_status_values = []
78 inactive_status_values = []
80 status_description = {}
82 def load_status(active_status_def, inactive_status_def):
83 global active_status_values
84 global inactive_status_values
86 global status_description
88 if active_status_def == settings_object.EMPTY:
89 active_status_def = globals()["active_status_def"]
90 if inactive_status_def == settings_object.EMPTY:
91 inactive_status_def = globals()["inactive_status_def"]
92 active_status_values = tuple([val for val,description in active_status_def])
93 inactive_status_values = tuple([val for val,description in inactive_status_def])
94 status_values = active_status_values + inactive_status_values
95 status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
97 for i,status in enumerate(status_values):
98 status_index[status] = i
99 load_status(active_status_def, inactive_status_def)
102 class Bug(settings_object.SavedSettingsObject):
110 There are two formats for time, int and string. Setting either
111 one will adjust the other appropriately. The string form is the
112 one stored in the bug's settings file on disk.
113 >>> print type(b.time)
115 >>> print type(b.time_string)
118 >>> print b.time_string
119 Thu, 01 Jan 1970 00:00:00 +0000
120 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
123 >>> print b.settings["time"]
124 Thu, 01 Jan 1970 00:01:00 +0000
126 settings_properties = []
127 required_saved_properties = []
128 _prop_save_settings = settings_object.prop_save_settings
129 _prop_load_settings = settings_object.prop_load_settings
130 def _versioned_property(settings_properties=settings_properties,
131 required_saved_properties=required_saved_properties,
133 if "settings_properties" not in kwargs:
134 kwargs["settings_properties"] = settings_properties
135 if "required_saved_properties" not in kwargs:
136 kwargs["required_saved_properties"]=required_saved_properties
137 return settings_object.versioned_property(**kwargs)
139 @_versioned_property(name="severity",
140 doc="A measure of the bug's importance",
142 check_fn=lambda s: s in severity_values,
144 def severity(): return {}
146 @_versioned_property(name="status",
147 doc="The bug's current status",
149 check_fn=lambda s: s in status_values,
151 def status(): return {}
155 return self.status in active_status_values
157 @_versioned_property(name="target",
158 doc="The deadline for fixing this bug")
159 def target(): return {}
161 @_versioned_property(name="creator",
162 doc="The user who entered the bug into the system")
163 def creator(): return {}
165 @_versioned_property(name="reporter",
166 doc="The user who reported the bug")
167 def reporter(): return {}
169 @_versioned_property(name="assigned",
170 doc="The developer in charge of the bug")
171 def assigned(): return {}
173 @_versioned_property(name="time",
174 doc="An RFC 2822 timestamp for bug creation")
175 def time_string(): return {}
178 if self.time_string == None or self.time_string == settings_object.EMPTY:
180 return utility.str_to_time(self.time_string)
181 def _set_time(self, value):
182 self.time_string = utility.time_to_str(value)
183 time = property(fget=_get_time,
185 doc="An integer version of .time_string")
187 @_versioned_property(name="summary",
188 doc="A one-line bug description")
189 def summary(): return {}
191 def _get_comment_root(self, load_full=False):
192 if self.sync_with_disk:
193 return comment.loadComments(self, load_full=load_full)
195 return comment.Comment(self, uuid=comment.INVALID_UUID)
198 @cached_property(generator=_get_comment_root)
199 @local_property("comment_root")
200 @doc_property(doc="The trunk of the comment tree")
201 def comment_root(): return {}
204 if hasattr(self.bugdir, "rcs"):
205 return self.bugdir.rcs
208 @cached_property(generator=_get_rcs)
209 @local_property("rcs")
210 @doc_property(doc="A revision control system instance.")
213 def __init__(self, bugdir=None, uuid=None, from_disk=False,
214 load_comments=False, summary=None):
215 settings_object.SavedSettingsObject.__init__(self)
218 if from_disk == True:
219 self.sync_with_disk = True
221 self.sync_with_disk = False
223 self.uuid = uuid_gen()
224 self.time = int(time.time()) # only save to second precision
226 self.creator = self.rcs.get_user_id()
227 self.summary = summary
230 return "Bug(uuid=%r)" % self.uuid
232 def _setting_attr_string(self, setting):
233 value = getattr(self, setting)
234 if value == settings_object.EMPTY:
239 def xml(self, show_comments=False):
240 if self.bugdir == None:
241 shortname = self.uuid
243 shortname = self.bugdir.bug_shortname(self)
245 if self.time == None:
248 timestring = utility.time_to_str(self.time)
250 info = [("uuid", self.uuid),
251 ("short-name", shortname),
252 ("severity", self.severity),
253 ("status", self.status),
254 ("assigned", self.assigned),
255 ("target", self.target),
256 ("reporter", self.reporter),
257 ("creator", self.creator),
258 ("created", timestring),
259 ("summary", self.summary)]
262 if v is not settings_object.EMPTY:
263 ret += ' <%s>%s</%s>\n' % (k,xml.sax.saxutils.escape(v),k)
265 if show_comments == True:
266 comout = self.comment_root.xml_thread(auto_name_map=True,
267 bug_shortname=shortname)
273 def string(self, shortlist=False, show_comments=False):
274 if self.bugdir == None:
275 shortname = self.uuid
277 shortname = self.bugdir.bug_shortname(self)
278 if shortlist == False:
279 if self.time_string == "":
280 timestring = self.time_string
282 htime = utility.handy_time(self.time)
283 timestring = "%s (%s)" % (htime, self.time_string)
284 info = [("ID", self.uuid),
285 ("Short name", shortname),
286 ("Severity", self.severity),
287 ("Status", self.status),
288 ("Assigned", self._setting_attr_string("assigned")),
289 ("Target", self._setting_attr_string("target")),
290 ("Reporter", self._setting_attr_string("reporter")),
291 ("Creator", self._setting_attr_string("creator")),
292 ("Created", timestring)]
293 longest_key_len = max([len(k) for k,v in info])
294 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
295 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
297 statuschar = self.status[0]
298 severitychar = self.severity[0]
299 chars = "%c%c" % (statuschar, severitychar)
300 bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
302 if show_comments == True:
303 # take advantage of the string_thread(auto_name_map=True)
304 # SIDE-EFFECT of sorting by comment time.
305 comout = self.comment_root.string_thread(flatten=False,
307 bug_shortname=shortname)
308 output = bugout + '\n' + comout.rstrip('\n')
314 return self.string(shortlist=True)
316 def __cmp__(self, other):
317 return cmp_full(self, other)
319 def get_path(self, name=None):
320 my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
323 assert name in ["values", "comments"]
324 return os.path.join(my_dir, name)
326 def load_settings(self):
327 self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
328 self._setup_saved_settings()
330 def load_comments(self, load_full=True):
331 if load_full == True:
332 # Force a complete load of the whole comment tree
333 self.comment_root = self._get_comment_root(load_full=True)
335 # Setup for fresh lazy-loading. Clear _comment_root, so
336 # _get_comment_root returns a fresh version. Turn of
337 # syncing temporarily so we don't write our blank comment
339 self.sync_with_disk = False
340 self.comment_root = None
341 self.sync_with_disk = True
343 def save_settings(self):
344 assert self.summary != None, "Can't save blank bug"
346 self.rcs.mkdir(self.get_path())
347 path = self.get_path("values")
348 mapfile.map_save(self.rcs, path, self._get_saved_settings())
353 if len(self.comment_root) > 0:
354 self.rcs.mkdir(self.get_path("comments"))
355 comment.saveComments(self)
358 self.comment_root.remove()
359 path = self.get_path()
360 self.rcs.recursive_remove(path)
363 for comment in self.comment_root.traverse():
366 def new_comment(self, body=None):
367 comm = self.comment_root.new_reply(body=body)
370 def comment_from_shortname(self, shortname, *args, **kwargs):
371 return self.comment_root.comment_from_shortname(shortname,
374 def comment_from_uuid(self, uuid):
375 return self.comment_root.comment_from_uuid(uuid)
377 def comment_shortnames(self, shortname=None):
379 SIDE-EFFECT : Comment.comment_shortnames will sort the comment
382 for id, comment in self.comment_root.comment_shortnames(shortname):
386 # The general rule for bug sorting is that "more important" bugs are
387 # less than "less important" bugs. This way sorting a list of bugs
388 # will put the most important bugs first in the list. When relative
389 # importance is unclear, the sorting follows some arbitrary convention
390 # (i.e. dictionary order).
392 def cmp_severity(bug_1, bug_2):
394 Compare the severity levels of two bugs, with more severe bugs
398 >>> bugA.severity = bugB.severity = "wishlist"
399 >>> cmp_severity(bugA, bugB) == 0
401 >>> bugB.severity = "minor"
402 >>> cmp_severity(bugA, bugB) > 0
404 >>> bugA.severity = "critical"
405 >>> cmp_severity(bugA, bugB) < 0
408 if not hasattr(bug_2, "severity") :
410 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
412 def cmp_status(bug_1, bug_2):
414 Compare the status levels of two bugs, with more 'open' bugs
418 >>> bugA.status = bugB.status = "open"
419 >>> cmp_status(bugA, bugB) == 0
421 >>> bugB.status = "closed"
422 >>> cmp_status(bugA, bugB) < 0
424 >>> bugA.status = "fixed"
425 >>> cmp_status(bugA, bugB) > 0
428 if not hasattr(bug_2, "status") :
430 val_2 = status_index[bug_2.status]
431 return cmp(status_index[bug_1.status], status_index[bug_2.status])
433 def cmp_attr(bug_1, bug_2, attr, invert=False):
435 Compare a general attribute between two bugs using the conventional
436 comparison rule for that attribute type. If invert == True, sort
437 *against* that convention.
441 >>> bugA.severity = "critical"
442 >>> bugB.severity = "wishlist"
443 >>> cmp_attr(bugA, bugB, attr) < 0
445 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
447 >>> bugB.severity = "critical"
448 >>> cmp_attr(bugA, bugB, attr) == 0
451 if not hasattr(bug_2, attr) :
453 val_1 = getattr(bug_1, attr)
454 val_2 = getattr(bug_2, attr)
455 if val_1 == settings_object.EMPTY: val_1 = None
456 if val_2 == settings_object.EMPTY: val_2 = None
459 return -cmp(val_1, val_2)
461 return cmp(val_1, val_2)
463 # alphabetical rankings (a < z)
464 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
465 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
466 # chronological rankings (newer < older)
467 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
469 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
470 cmp_time,cmp_creator)):
471 for comparison in cmp_list :
472 val = comparison(bug_1, bug_2)
477 class InvalidValue(ValueError):
478 def __init__(self, name, value):
479 msg = "Cannot assign value %s to %s" % (value, name)
480 Exception.__init__(self, msg)
484 suite = doctest.DocTestSuite()