1 # Copyright (C) 2008-2012 Chris Ball <cjb@laptop.org>
2 # Gianluca Montecchi <gian@grys.it>
3 # Robert Lehmann <mail@robertlehmann.de>
4 # Thomas Habets <thomas@habets.pp.se>
5 # Valtteri Kokkoniemi <rvk@iki.fi>
6 # W. Trevor King <wking@drexel.edu>
8 # This file is part of Bugs Everywhere.
10 # Bugs Everywhere is free software: you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by the Free
12 # Software Foundation, either version 2 of the License, or (at your option) any
15 # Bugs Everywhere is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
20 # You should have received a copy of the GNU General Public License along with
21 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
23 """Define the :class:`Bug` class for representing bugs.
33 try: # import core module, Python >= 2.5
34 from xml.etree import ElementTree
35 except ImportError: # look for non-core module
36 from elementtree import ElementTree
37 import xml.sax.saxutils
41 from libbe.storage.util.properties import Property, doc_property, \
42 local_property, defaulting_property, checked_property, cached_property, \
43 primed_property, change_hook_property, settings_property
44 import libbe.storage.util.settings_object as settings_object
45 import libbe.storage.util.mapfile as mapfile
46 import libbe.comment as comment
47 import libbe.util.utility as utility
49 if libbe.TESTING == True:
53 ### Define and describe valid bug categories
54 # Use a tuple of (category, description) tuples since we don't have
55 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
57 # in order of increasing severity. (name, description) pairs
59 ("target", "The issue is a target or milestone, not a bug."),
60 ("wishlist","A feature that could improve usefulness, but not a bug."),
61 ("minor","The standard bug level."),
62 ("serious","A bug that requires workarounds."),
63 ("critical","A bug that prevents some features from working at all."),
64 ("fatal","A bug that makes the package unusable."))
66 # in order of increasing resolution
67 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
69 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
70 ("open","A working bug that has not been assigned to a developer."),
71 ("assigned","A working bug that has been assigned to a developer."),
72 ("test","The code has been adjusted, but the fix is still being tested."))
73 inactive_status_def = (
74 ("closed", "The bug is no longer relevant."),
75 ("fixed", "The bug should no longer occur."),
76 ("wontfix","It's not a bug, it's a feature."))
79 ### Convert the description tuples to more useful formats
82 severity_description = {}
84 def load_severities(severity_def):
85 global severity_values
86 global severity_description
88 if severity_def == None:
90 severity_values = tuple([val for val,description in severity_def])
91 severity_description = dict(severity_def)
93 for i,severity in enumerate(severity_values):
94 severity_index[severity] = i
95 load_severities(severity_def)
97 active_status_values = []
98 inactive_status_values = []
100 status_description = {}
102 def load_status(active_status_def, inactive_status_def):
103 global active_status_values
104 global inactive_status_values
106 global status_description
108 if active_status_def == None:
109 active_status_def = globals()["active_status_def"]
110 if inactive_status_def == None:
111 inactive_status_def = globals()["inactive_status_def"]
112 active_status_values = tuple([val for val,description in active_status_def])
113 inactive_status_values = tuple([val for val,description in inactive_status_def])
114 status_values = active_status_values + inactive_status_values
115 status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
117 for i,status in enumerate(status_values):
118 status_index[status] = i
119 load_status(active_status_def, inactive_status_def)
122 class Bug (settings_object.SavedSettingsObject):
123 """A bug (or issue) is a place to store attributes and attach
124 :class:`~libbe.comment.Comment`\s. In mailing-list terms, a bug is
125 analogous to a thread. Bugs are normally stored in
126 :class:`~libbe.bugdir.BugDir`\s.
134 There are two formats for time, int and string. Setting either
135 one will adjust the other appropriately. The string form is the
136 one stored in the bug's settings file on disk.
138 >>> print type(b.time)
140 >>> print type(b.time_string)
143 >>> print b.time_string
144 Thu, 01 Jan 1970 00:00:00 +0000
145 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
148 >>> print b.settings["time"]
149 Thu, 01 Jan 1970 00:01:00 +0000
151 settings_properties = []
152 required_saved_properties = []
153 _prop_save_settings = settings_object.prop_save_settings
154 _prop_load_settings = settings_object.prop_load_settings
155 def _versioned_property(settings_properties=settings_properties,
156 required_saved_properties=required_saved_properties,
158 if "settings_properties" not in kwargs:
159 kwargs["settings_properties"] = settings_properties
160 if "required_saved_properties" not in kwargs:
161 kwargs["required_saved_properties"]=required_saved_properties
162 return settings_object.versioned_property(**kwargs)
164 @_versioned_property(name="severity",
165 doc="A measure of the bug's importance",
167 check_fn=lambda s: s in severity_values,
169 def severity(): return {}
171 @_versioned_property(name="status",
172 doc="The bug's current status",
174 check_fn=lambda s: s in status_values,
176 def status(): return {}
180 return self.status in active_status_values
182 @_versioned_property(name="creator",
183 doc="The user who entered the bug into the system")
184 def creator(): return {}
186 @_versioned_property(name="reporter",
187 doc="The user who reported the bug")
188 def reporter(): return {}
190 @_versioned_property(name="assigned",
191 doc="The developer in charge of the bug")
192 def assigned(): return {}
194 @_versioned_property(name="time",
195 doc="An RFC 2822 timestamp for bug creation")
196 def time_string(): return {}
199 if self.time_string == None:
200 self._cached_time_string = None
201 self._cached_time = None
203 if (not hasattr(self, '_cached_time_string')
204 or self.time_string != self._cached_time_string):
205 self._cached_time_string = self.time_string
206 self._cached_time = utility.str_to_time(self.time_string)
207 return self._cached_time
208 def _set_time(self, value):
209 if not hasattr(self, '_cached_time') or value != self._cached_time:
210 self.time_string = utility.time_to_str(value)
211 self._cached_time_string = self.time_string
212 self._cached_time = value
213 time = property(fget=_get_time,
215 doc="An integer version of .time_string")
217 def _extra_strings_check_fn(value):
218 return utility.iterable_full_of_strings(value, \
219 alternative=settings_object.EMPTY)
220 def _extra_strings_change_hook(self, old, new):
221 self.extra_strings.sort() # to make merging easier
222 self._prop_save_settings(old, new)
223 @_versioned_property(name="extra_strings",
224 doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
226 check_fn=_extra_strings_check_fn,
227 change_hook=_extra_strings_change_hook,
229 def extra_strings(): return {}
231 @_versioned_property(name="summary",
232 doc="A one-line bug description")
233 def summary(): return {}
235 def _get_comment_root(self, load_full=False):
236 if self.storage != None and self.storage.is_readable():
237 return comment.load_comments(self, load_full=load_full)
239 return comment.Comment(self, uuid=comment.INVALID_UUID)
242 @cached_property(generator=_get_comment_root)
243 @local_property("comment_root")
244 @doc_property(doc="The trunk of the comment tree. We use a dummy root comment by default, because there can be several comment threads rooted on the same parent bug. To simplify comment interaction, we condense these threads into a single thread with a Comment dummy root.")
245 def comment_root(): return {}
247 def __init__(self, bugdir=None, uuid=None, from_storage=False,
248 load_comments=False, summary=None):
249 settings_object.SavedSettingsObject.__init__(self)
253 self.id = libbe.util.id.ID(self, 'bug')
254 if from_storage == False:
256 self.uuid = libbe.util.id.uuid_gen()
257 self.time = int(time.time()) # only save to second precision
258 self.summary = summary
259 dummy = self.comment_root
260 if self.bugdir != None:
261 self.storage = self.bugdir.storage
262 if from_storage == False:
263 if self.storage != None and self.storage.is_writeable():
267 return "Bug(uuid=%r)" % self.uuid
270 return self.string(shortlist=True)
272 def __cmp__(self, other):
273 return cmp_full(self, other)
275 # serializing methods
277 def _setting_attr_string(self, setting):
278 value = getattr(self, setting)
281 if type(value) not in types.StringTypes:
285 def string(self, shortlist=False, show_comments=False):
286 if shortlist == False:
287 if self.time == None:
290 htime = utility.handy_time(self.time)
291 timestring = "%s (%s)" % (htime, self.time_string)
292 info = [("ID", self.uuid),
293 ("Short name", self.id.user()),
294 ("Severity", self.severity),
295 ("Status", self.status),
296 ("Assigned", self._setting_attr_string("assigned")),
297 ("Reporter", self._setting_attr_string("reporter")),
298 ("Creator", self._setting_attr_string("creator")),
299 ("Created", timestring)]
300 for estr in self.extra_strings:
301 info.append(('Extra string', estr))
302 longest_key_len = max([len(k) for k,v in info])
303 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
304 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
306 statuschar = self.status[0]
307 severitychar = self.severity[0]
308 chars = "%c%c" % (statuschar, severitychar)
309 bugout = "%s:%s: %s" % (self.id.user(),chars,self.summary.rstrip('\n'))
311 if show_comments == True:
312 self.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
313 comout = self.comment_root.string_thread(flatten=False)
314 output = bugout + '\n' + comout.rstrip('\n')
319 def xml(self, indent=0, show_comments=False):
321 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.xml()')
322 >>> bugA.uuid = 'bugA'
323 >>> bugA.time_string = 'Thu, 01 Jan 1970 00:00:00 +0000'
324 >>> bugA.creator = u'Frank'
325 >>> bugA.extra_strings += ['TAG: very helpful']
326 >>> commA = bugA.comment_root.new_reply(body='comment A')
327 >>> commA.uuid = 'commA'
328 >>> commA.date = 'Thu, 01 Jan 1970 00:01:00 +0000'
329 >>> commB = commA.new_reply(body='comment B')
330 >>> commB.uuid = 'commB'
331 >>> commB.date = 'Thu, 01 Jan 1970 00:02:00 +0000'
332 >>> commC = commB.new_reply(body='comment C')
333 >>> commC.uuid = 'commC'
334 >>> commC.date = 'Thu, 01 Jan 1970 00:03:00 +0000'
335 >>> print(bugA.xml(show_comments=True)) # doctest: +REPORT_UDIFF
338 <short-name>/bug</short-name>
339 <severity>minor</severity>
340 <status>open</status>
341 <creator>Frank</creator>
342 <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
343 <summary>Need to test Bug.xml()</summary>
344 <extra-string>TAG: very helpful</extra-string>
347 <short-name>/bug/commA</short-name>
349 <date>Thu, 01 Jan 1970 00:01:00 +0000</date>
350 <content-type>text/plain</content-type>
351 <body>comment A</body>
355 <short-name>/bug/commB</short-name>
356 <in-reply-to>commA</in-reply-to>
358 <date>Thu, 01 Jan 1970 00:02:00 +0000</date>
359 <content-type>text/plain</content-type>
360 <body>comment B</body>
364 <short-name>/bug/commC</short-name>
365 <in-reply-to>commB</in-reply-to>
367 <date>Thu, 01 Jan 1970 00:03:00 +0000</date>
368 <content-type>text/plain</content-type>
369 <body>comment C</body>
372 >>> print(bugA.xml(show_comments=True, indent=2))
373 ... # doctest: +REPORT_UDIFF
376 <short-name>/bug</short-name>
377 <severity>minor</severity>
378 <status>open</status>
379 <creator>Frank</creator>
380 <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
381 <summary>Need to test Bug.xml()</summary>
382 <extra-string>TAG: very helpful</extra-string>
385 <short-name>/bug/commA</short-name>
387 <date>Thu, 01 Jan 1970 00:01:00 +0000</date>
388 <content-type>text/plain</content-type>
389 <body>comment A</body>
393 <short-name>/bug/commB</short-name>
394 <in-reply-to>commA</in-reply-to>
396 <date>Thu, 01 Jan 1970 00:02:00 +0000</date>
397 <content-type>text/plain</content-type>
398 <body>comment B</body>
402 <short-name>/bug/commC</short-name>
403 <in-reply-to>commB</in-reply-to>
405 <date>Thu, 01 Jan 1970 00:03:00 +0000</date>
406 <content-type>text/plain</content-type>
407 <body>comment C</body>
411 if self.time == None:
414 timestring = utility.time_to_str(self.time)
416 info = [('uuid', self.uuid),
417 ('short-name', self.id.user()),
418 ('severity', self.severity),
419 ('status', self.status),
420 ('assigned', self.assigned),
421 ('reporter', self.reporter),
422 ('creator', self.creator),
423 ('created', timestring),
424 ('summary', self.summary)]
428 lines.append(' <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
429 for estr in self.extra_strings:
430 lines.append(' <extra-string>%s</extra-string>' % estr)
431 if show_comments == True:
432 comout = self.comment_root.xml_thread(indent=indent+2)
434 comout = comout[indent:] # strip leading indent spaces
436 lines.append('</bug>')
439 return istring + sep.join(lines).rstrip('\n')
441 def from_xml(self, xml_string, preserve_uuids=False, verbose=True):
443 Note: If a bug uuid is given, set .alt_id to it's value.
444 >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
445 >>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000"
446 >>> bugA.creator = u'Fran\xe7ois'
447 >>> bugA.extra_strings += ['TAG: very helpful']
448 >>> commA = bugA.comment_root.new_reply(body='comment A')
449 >>> commB = bugA.comment_root.new_reply(body='comment B')
450 >>> commC = commA.new_reply(body='comment C')
451 >>> xml = bugA.xml(show_comments=True)
453 >>> bugB.from_xml(xml, verbose=True)
454 >>> bugB.xml(show_comments=True) == xml
456 >>> bugB.uuid = bugB.alt_id
457 >>> for comm in bugB.comments():
458 ... comm.uuid = comm.alt_id
459 ... comm.alt_id = None
460 >>> bugB.xml(show_comments=True) == xml
462 >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE
463 ['severity', 'status', 'creator', 'time', 'summary']
464 >>> len(list(bugB.comments()))
467 >>> bugC.from_xml(xml, preserve_uuids=True)
468 >>> bugC.uuid == bugA.uuid
471 if type(xml_string) == types.UnicodeType:
472 xml_string = xml_string.strip().encode('unicode_escape')
473 if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
476 bug = ElementTree.XML(xml_string)
478 raise utility.InvalidXML( \
479 'bug', bug, 'root element must be <bug>')
480 tags=['uuid','short-name','severity','status','assigned',
481 'reporter', 'creator','created','summary','extra-string']
482 self.explicit_attrs = []
486 for child in bug.getchildren():
487 if child.tag == 'short-name':
489 elif child.tag == 'comment':
490 comm = comment.Comment(bug=self)
492 child, preserve_uuids=preserve_uuids, verbose=verbose)
493 comments.append(comm)
495 elif child.tag in tags:
496 if child.text == None or len(child.text) == 0:
497 text = settings_object.EMPTY
499 text = xml.sax.saxutils.unescape(child.text)
500 if not isinstance(text, unicode):
501 text = text.decode('unicode_escape')
503 if child.tag == 'uuid' and not preserve_uuids:
505 continue # don't set the bug's uuid tag.
506 elif child.tag == 'created':
507 if text is not settings_object.EMPTY:
508 self.time = utility.str_to_time(text)
509 self.explicit_attrs.append('time')
511 elif child.tag == 'extra-string':
513 continue # don't set the bug's extra_string yet.
514 attr_name = child.tag.replace('-','_')
515 self.explicit_attrs.append(attr_name)
516 setattr(self, attr_name, text)
517 elif verbose == True:
518 print >> sys.stderr, 'Ignoring unknown tag %s in %s' \
519 % (child.tag, comment.tag)
520 if uuid != self.uuid:
521 if not hasattr(self, 'alt_id') or self.alt_id == None:
523 self.extra_strings = estrs
524 self.add_comments(comments, ignore_missing_references=True)
526 def add_comment(self, comment, *args, **kwargs):
528 Add a comment too the current bug, under the parent specified
529 by comment.in_reply_to.
530 Note: If a bug uuid is given, set .alt_id to it's value.
532 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.add_comment()')
533 >>> bugA.creator = 'Jack'
534 >>> commA = bugA.comment_root.new_reply(body='comment A')
535 >>> commA.uuid = 'commA'
536 >>> commB = comment.Comment(body='comment B')
537 >>> commB.uuid = 'commB'
538 >>> bugA.add_comment(commB)
539 >>> commC = comment.Comment(body='comment C')
540 >>> commC.uuid = 'commC'
541 >>> commC.in_reply_to = commA.uuid
542 >>> bugA.add_comment(commC)
543 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
546 <short-name>/012</short-name>
547 <severity>minor</severity>
548 <status>open</status>
549 <creator>Jack</creator>
550 <created>...</created>
551 <summary>Need to test Bug.add_comment()</summary>
554 <short-name>/012/commA</short-name>
557 <content-type>text/plain</content-type>
558 <body>comment A</body>
562 <short-name>/012/commC</short-name>
563 <in-reply-to>commA</in-reply-to>
566 <content-type>text/plain</content-type>
567 <body>comment C</body>
571 <short-name>/012/commB</short-name>
574 <content-type>text/plain</content-type>
575 <body>comment B</body>
579 self.add_comments([comment], **kwargs)
581 def add_comments(self, comments, default_parent=None,
582 ignore_missing_references=False):
584 Convert a raw list of comments to single root comment. If a
585 comment does not specify a parent with .in_reply_to, the
586 parent defaults to .comment_root, but you can specify another
587 default parent via default_parent.
590 if default_parent == None:
591 default_parent = self.comment_root
592 for c in list(self.comments()) + comments:
593 assert c.uuid != None
594 assert c.uuid not in uuid_map
597 uuid_map[c.alt_id] = c
598 uuid_map[None] = self.comment_root
599 uuid_map[comment.INVALID_UUID] = self.comment_root
600 if default_parent != self.comment_root:
601 assert default_parent.uuid in uuid_map, default_parent.uuid
603 if c.in_reply_to == None \
604 and default_parent.uuid != comment.INVALID_UUID:
605 c.in_reply_to = default_parent.uuid
606 elif c.in_reply_to == comment.INVALID_UUID:
609 parent = uuid_map[c.in_reply_to]
611 if ignore_missing_references == True:
612 print >> sys.stderr, \
613 'Ignoring missing reference to %s' % c.in_reply_to
614 parent = default_parent
615 if parent.uuid != comment.INVALID_UUID:
616 c.in_reply_to = parent.uuid
618 raise comment.MissingReference(c)
622 def merge(self, other, accept_changes=True,
623 accept_extra_strings=True, accept_comments=True,
624 change_exception=False):
626 Merge info from other into this bug. Overrides any attributes
627 in self that are listed in other.explicit_attrs.
629 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()')
630 >>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
631 >>> bugA.creator = 'Frank'
632 >>> bugA.extra_strings += ['TAG: very helpful']
633 >>> bugA.extra_strings += ['TAG: favorite']
634 >>> commA = bugA.comment_root.new_reply(body='comment A')
635 >>> commA.uuid = 'uuid-commA'
636 >>> bugB = Bug(uuid='3210', summary='More tests for Bug.merge()')
637 >>> bugB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
638 >>> bugB.creator = 'John'
639 >>> bugB.explicit_attrs = ['creator', 'summary']
640 >>> bugB.extra_strings += ['TAG: very helpful']
641 >>> bugB.extra_strings += ['TAG: useful']
642 >>> commB = bugB.comment_root.new_reply(body='comment B')
643 >>> commB.uuid = 'uuid-commB'
644 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
645 ... accept_comments=False, change_exception=False)
646 >>> print bugA.creator
648 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
649 ... accept_comments=False, change_exception=True)
650 Traceback (most recent call last):
652 ValueError: Merge would change creator "Frank"->"John" for bug 0123
653 >>> print bugA.creator
655 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=False,
656 ... accept_comments=False, change_exception=True)
657 Traceback (most recent call last):
659 ValueError: Merge would add extra string "TAG: useful" for bug 0123
660 >>> print bugA.creator
662 >>> print bugA.extra_strings
663 ['TAG: favorite', 'TAG: very helpful']
664 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
665 ... accept_comments=False, change_exception=True)
666 Traceback (most recent call last):
668 ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123
669 >>> print bugA.extra_strings
670 ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
671 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
672 ... accept_comments=True, change_exception=True)
673 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
676 <short-name>/012</short-name>
677 <severity>minor</severity>
678 <status>open</status>
679 <creator>John</creator>
680 <created>...</created>
681 <summary>More tests for Bug.merge()</summary>
682 <extra-string>TAG: favorite</extra-string>
683 <extra-string>TAG: useful</extra-string>
684 <extra-string>TAG: very helpful</extra-string>
686 <uuid>uuid-commA</uuid>
687 <short-name>/012/uuid-commA</short-name>
690 <content-type>text/plain</content-type>
691 <body>comment A</body>
694 <uuid>uuid-commB</uuid>
695 <short-name>/012/uuid-commB</short-name>
698 <content-type>text/plain</content-type>
699 <body>comment B</body>
703 if hasattr(other, 'explicit_attrs'):
704 for attr in other.explicit_attrs:
705 old = getattr(self, attr)
706 new = getattr(other, attr)
709 setattr(self, attr, new)
710 elif change_exception:
712 ('Merge would change {} "{}"->"{}" for bug {}'
713 ).format(attr, old, new, self.uuid))
714 for estr in other.extra_strings:
715 if not estr in self.extra_strings:
716 if accept_extra_strings == True:
717 self.extra_strings += [estr]
718 elif change_exception == True:
720 'Merge would add extra string "%s" for bug %s' \
722 for o_comm in other.comments():
724 s_comm = self.comment_root.comment_from_uuid(o_comm.uuid)
727 s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id)
731 if accept_comments == True:
732 o_comm_copy = copy.copy(o_comm)
733 o_comm_copy.bug = self
734 o_comm_copy.id = libbe.util.id.ID(o_comm_copy, 'comment')
735 self.comment_root.add_reply(o_comm_copy)
736 elif change_exception == True:
738 'Merge would add comment %s (alt: %s) to bug %s' \
739 % (o_comm.uuid, o_comm.alt_id, self.uuid)
741 s_comm.merge(o_comm, accept_changes=accept_changes,
742 accept_extra_strings=accept_extra_strings,
743 change_exception=change_exception)
745 # methods for saving/loading/acessing settings and properties.
747 def load_settings(self, settings_mapfile=None):
748 if settings_mapfile == None:
749 settings_mapfile = self.storage.get(
750 self.id.storage('values'), '\n')
752 settings = mapfile.parse(settings_mapfile)
753 except mapfile.InvalidMapfileContents, e:
754 raise Exception('Invalid settings file for bug %s\n'
755 '(BE version missmatch?)' % self.id.user())
756 self._setup_saved_settings(settings)
758 def save_settings(self):
759 mf = mapfile.generate(self._get_saved_settings())
760 self.storage.set(self.id.storage('values'), mf)
764 Save any loaded contents to storage. Because of lazy loading
765 of comments, this is actually not too inefficient.
767 However, if self.storage.is_writeable() == True, then any
768 changes are automatically written to storage as soon as they
769 happen, so calling this method will just waste time (unless
770 something else has been messing with your stored files).
772 assert self.storage != None, "Can't save without storage"
773 if self.bugdir != None:
774 parent = self.bugdir.id.storage()
777 self.storage.add(self.id.storage(), parent=parent, directory=True)
778 self.storage.add(self.id.storage('values'), parent=self.id.storage(),
781 if len(self.comment_root) > 0:
782 comment.save_comments(self)
784 def load_comments(self, load_full=True):
785 if load_full == True:
786 # Force a complete load of the whole comment tree
787 self.comment_root = self._get_comment_root(load_full=True)
789 # Setup for fresh lazy-loading. Clear _comment_root, so
790 # next _get_comment_root returns a fresh version. Turn of
791 # writing temporarily so we don't write our blank comment
793 w = self.storage.writeable
794 self.storage.writeable = False
795 self.comment_root = None
796 self.storage.writeable = w
799 self.storage.recursive_remove(self.id.storage())
801 # methods for managing comments
804 for comment in self.comments():
808 for comment in self.comment_root.traverse():
811 def new_comment(self, body=None):
812 comm = self.comment_root.new_reply(body=body)
815 def comment_from_uuid(self, uuid, *args, **kwargs):
816 return self.comment_root.comment_from_uuid(uuid, *args, **kwargs)
818 # methods for id generation
820 def sibling_uuids(self):
821 if self.bugdir != None:
822 return self.bugdir.uuids()
826 # The general rule for bug sorting is that "more important" bugs are
827 # less than "less important" bugs. This way sorting a list of bugs
828 # will put the most important bugs first in the list. When relative
829 # importance is unclear, the sorting follows some arbitrary convention
830 # (i.e. dictionary order).
832 def cmp_severity(bug_1, bug_2):
834 Compare the severity levels of two bugs, with more severe bugs
839 >>> bugA.severity = bugB.severity = "wishlist"
840 >>> cmp_severity(bugA, bugB) == 0
842 >>> bugB.severity = "minor"
843 >>> cmp_severity(bugA, bugB) > 0
845 >>> bugA.severity = "critical"
846 >>> cmp_severity(bugA, bugB) < 0
849 if not hasattr(bug_2, "severity") :
851 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
853 def cmp_status(bug_1, bug_2):
855 Compare the status levels of two bugs, with more "open" bugs
860 >>> bugA.status = bugB.status = "open"
861 >>> cmp_status(bugA, bugB) == 0
863 >>> bugB.status = "closed"
864 >>> cmp_status(bugA, bugB) < 0
866 >>> bugA.status = "fixed"
867 >>> cmp_status(bugA, bugB) > 0
870 if not hasattr(bug_2, "status") :
872 val_2 = status_index[bug_2.status]
873 return cmp(status_index[bug_1.status], status_index[bug_2.status])
875 def cmp_attr(bug_1, bug_2, attr, invert=False):
877 Compare a general attribute between two bugs using the
878 conventional comparison rule for that attribute type. If
879 ``invert==True``, sort *against* that convention.
884 >>> bugA.severity = "critical"
885 >>> bugB.severity = "wishlist"
886 >>> cmp_attr(bugA, bugB, attr) < 0
888 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
890 >>> bugB.severity = "critical"
891 >>> cmp_attr(bugA, bugB, attr) == 0
894 if not hasattr(bug_2, attr) :
896 val_1 = getattr(bug_1, attr)
897 val_2 = getattr(bug_2, attr)
898 if val_1 == None: val_1 = None
899 if val_2 == None: val_2 = None
902 return -cmp(val_1, val_2)
904 return cmp(val_1, val_2)
906 # alphabetical rankings (a < z)
907 cmp_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid")
908 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
909 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
910 cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter")
911 cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary")
912 cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings")
913 # chronological rankings (newer < older)
914 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
916 def cmp_mine(bug_1, bug_2):
917 user_id = libbe.ui.util.user.get_user_id(bug_1.storage)
918 mine_1 = bug_1.assigned != user_id
919 mine_2 = bug_2.assigned != user_id
920 return cmp(mine_1, mine_2)
922 def cmp_comments(bug_1, bug_2):
924 Compare two bugs' comments lists. Doesn't load any new comments,
925 so you should call each bug's .load_comments() first if you want a
928 comms_1 = sorted(bug_1.comments(), key = lambda comm : comm.uuid)
929 comms_2 = sorted(bug_2.comments(), key = lambda comm : comm.uuid)
930 result = cmp(len(comms_1), len(comms_2))
933 for c_1,c_2 in zip(comms_1, comms_2):
934 result = cmp(c_1, c_2)
939 DEFAULT_CMP_FULL_CMP_LIST = \
940 (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator,
941 cmp_reporter, cmp_comments, cmp_summary, cmp_uuid, cmp_extra_strings)
943 class BugCompoundComparator (object):
944 def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
945 self.cmp_list = cmp_list
946 def __call__(self, bug_1, bug_2):
947 for comparison in self.cmp_list :
948 val = comparison(bug_1, bug_2)
953 cmp_full = BugCompoundComparator()
956 # define some bonus cmp_* functions
957 def cmp_last_modified(bug_1, bug_2):
959 Like cmp_time(), but use most recent comment instead of bug
960 creation for the timestamp.
962 def last_modified(bug):
964 for comment in bug.comment_root.traverse():
965 if comment.time > time:
968 val_1 = last_modified(bug_1)
969 val_2 = last_modified(bug_2)
970 return -cmp(val_1, val_2)
973 if libbe.TESTING == True:
974 suite = doctest.DocTestSuite()