1 # Copyright (C) 2008-2012 Chris Ball <cjb@laptop.org>
2 # Gianluca Montecchi <gian@grys.it>
3 # Niall Douglas (s_sourceforge@nedprod.com) <spam@spamtrap.com>
4 # Robert Lehmann <mail@robertlehmann.de>
5 # Thomas Habets <thomas@habets.pp.se>
6 # Valtteri Kokkoniemi <rvk@iki.fi>
7 # W. Trevor King <wking@tremily.us>
9 # This file is part of Bugs Everywhere.
11 # Bugs Everywhere is free software: you can redistribute it and/or modify it
12 # under the terms of the GNU General Public License as published by the Free
13 # Software Foundation, either version 2 of the License, or (at your option) any
16 # Bugs Everywhere is distributed in the hope that it will be useful, but
17 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
18 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
21 # You should have received a copy of the GNU General Public License along with
22 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
24 """Define :py:class:`Bug` for representing bugs.
34 try: # import core module, Python >= 2.5
35 from xml.etree import ElementTree
36 except ImportError: # look for non-core module
37 from elementtree import ElementTree
38 import xml.sax.saxutils
42 from libbe.storage.util.properties import Property, doc_property, \
43 local_property, defaulting_property, checked_property, cached_property, \
44 primed_property, change_hook_property, settings_property
45 import libbe.storage.util.settings_object as settings_object
46 import libbe.storage.util.mapfile as mapfile
47 import libbe.comment as comment
48 import libbe.util.utility as utility
50 if libbe.TESTING == True:
54 ### Define and describe valid bug categories
55 # Use a tuple of (category, description) tuples since we don't have
56 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
58 # in order of increasing severity. (name, description) pairs
60 ("target", "The issue is a target or milestone, not a bug."),
61 ("wishlist","A feature that could improve usefulness, but not a bug."),
62 ("minor","The standard bug level."),
63 ("serious","A bug that requires workarounds."),
64 ("critical","A bug that prevents some features from working at all."),
65 ("fatal","A bug that makes the package unusable."))
67 # in order of increasing resolution
68 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
70 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
71 ("open","A working bug that has not been assigned to a developer."),
72 ("assigned","A working bug that has been assigned to a developer."),
73 ("test","The code has been adjusted, but the fix is still being tested."))
74 inactive_status_def = (
75 ("closed", "The bug is no longer relevant."),
76 ("fixed", "The bug should no longer occur."),
77 ("wontfix","It's not a bug, it's a feature."))
80 ### Convert the description tuples to more useful formats
83 severity_description = {}
85 def load_severities(severity_def):
86 global severity_values
87 global severity_description
89 if severity_def == None:
91 severity_values = tuple([val for val,description in severity_def])
92 severity_description = dict(severity_def)
94 for i,severity in enumerate(severity_values):
95 severity_index[severity] = i
96 load_severities(severity_def)
98 active_status_values = []
99 inactive_status_values = []
101 status_description = {}
103 def load_status(active_status_def, inactive_status_def):
104 global active_status_values
105 global inactive_status_values
107 global status_description
109 if active_status_def == None:
110 active_status_def = globals()["active_status_def"]
111 if inactive_status_def == None:
112 inactive_status_def = globals()["inactive_status_def"]
113 active_status_values = tuple([val for val,description in active_status_def])
114 inactive_status_values = tuple([val for val,description in inactive_status_def])
115 status_values = active_status_values + inactive_status_values
116 status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
118 for i,status in enumerate(status_values):
119 status_index[status] = i
120 load_status(active_status_def, inactive_status_def)
123 class Bug (settings_object.SavedSettingsObject):
124 """A bug (or issue) is a place to store attributes and attach
125 :py:class:`~libbe.comment.Comment`\s. In mailing-list terms, a bug is
126 analogous to a thread. Bugs are normally stored in
127 :py:class:`~libbe.bugdir.BugDir`\s.
135 There are two formats for time, int and string. Setting either
136 one will adjust the other appropriately. The string form is the
137 one stored in the bug's settings file on disk.
139 >>> print type(b.time)
141 >>> print type(b.time_string)
144 >>> print b.time_string
145 Thu, 01 Jan 1970 00:00:00 +0000
146 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
149 >>> print b.settings["time"]
150 Thu, 01 Jan 1970 00:01:00 +0000
152 settings_properties = []
153 required_saved_properties = []
154 _prop_save_settings = settings_object.prop_save_settings
155 _prop_load_settings = settings_object.prop_load_settings
156 def _versioned_property(settings_properties=settings_properties,
157 required_saved_properties=required_saved_properties,
159 if "settings_properties" not in kwargs:
160 kwargs["settings_properties"] = settings_properties
161 if "required_saved_properties" not in kwargs:
162 kwargs["required_saved_properties"]=required_saved_properties
163 return settings_object.versioned_property(**kwargs)
165 @_versioned_property(name="severity",
166 doc="A measure of the bug's importance",
168 check_fn=lambda s: s in severity_values,
170 def severity(): return {}
172 @_versioned_property(name="status",
173 doc="The bug's current status",
175 check_fn=lambda s: s in status_values,
177 def status(): return {}
181 return self.status in active_status_values
183 @_versioned_property(name="creator",
184 doc="The user who entered the bug into the system")
185 def creator(): return {}
187 @_versioned_property(name="reporter",
188 doc="The user who reported the bug")
189 def reporter(): return {}
191 @_versioned_property(name="assigned",
192 doc="The developer in charge of the bug")
193 def assigned(): return {}
195 @_versioned_property(name="time",
196 doc="An RFC 2822 timestamp for bug creation")
197 def time_string(): return {}
200 if self.time_string == None:
201 self._cached_time_string = None
202 self._cached_time = None
204 if (not hasattr(self, '_cached_time_string')
205 or self.time_string != self._cached_time_string):
206 self._cached_time_string = self.time_string
207 self._cached_time = utility.str_to_time(self.time_string)
208 return self._cached_time
209 def _set_time(self, value):
210 if not hasattr(self, '_cached_time') or value != self._cached_time:
211 self.time_string = utility.time_to_str(value)
212 self._cached_time_string = self.time_string
213 self._cached_time = value
214 time = property(fget=_get_time,
216 doc="An integer version of .time_string")
218 def _extra_strings_check_fn(value):
219 return utility.iterable_full_of_strings(value, \
220 alternative=settings_object.EMPTY)
221 def _extra_strings_change_hook(self, old, new):
222 self.extra_strings.sort() # to make merging easier
223 self._prop_save_settings(old, new)
224 @_versioned_property(name="extra_strings",
225 doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
227 check_fn=_extra_strings_check_fn,
228 change_hook=_extra_strings_change_hook,
230 def extra_strings(): return {}
232 @_versioned_property(name="summary",
233 doc="A one-line bug description")
234 def summary(): return {}
236 def _get_comment_root(self, load_full=False):
237 if self.storage != None and self.storage.is_readable():
238 return comment.load_comments(self, load_full=load_full)
240 return comment.Comment(self, uuid=comment.INVALID_UUID)
243 @cached_property(generator=_get_comment_root)
244 @local_property("comment_root")
245 @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.")
246 def comment_root(): return {}
248 def __init__(self, bugdir=None, uuid=None, from_storage=False,
249 load_comments=False, summary=None):
250 settings_object.SavedSettingsObject.__init__(self)
254 self.id = libbe.util.id.ID(self, 'bug')
255 if from_storage == False:
257 self.uuid = libbe.util.id.uuid_gen()
258 self.time = int(time.time()) # only save to second precision
259 self.summary = summary
260 dummy = self.comment_root
261 if self.bugdir != None:
262 self.storage = self.bugdir.storage
263 if from_storage == False:
264 if self.storage != None and self.storage.is_writeable():
268 return "Bug(uuid=%r)" % self.uuid
271 return self.string(shortlist=True)
273 def __cmp__(self, other):
274 return cmp_full(self, other)
276 # serializing methods
278 def _setting_attr_string(self, setting):
279 value = getattr(self, setting)
282 if type(value) not in types.StringTypes:
286 def string(self, shortlist=False, show_comments=False):
287 if shortlist == False:
288 if self.time == None:
291 htime = utility.handy_time(self.time)
292 timestring = "%s (%s)" % (htime, self.time_string)
293 info = [("ID", self.uuid),
294 ("Short name", self.id.user()),
295 ("Severity", self.severity),
296 ("Status", self.status),
297 ("Assigned", self._setting_attr_string("assigned")),
298 ("Reporter", self._setting_attr_string("reporter")),
299 ("Creator", self._setting_attr_string("creator")),
300 ("Created", timestring)]
301 for estr in self.extra_strings:
302 info.append(('Extra string', estr))
303 longest_key_len = max([len(k) for k,v in info])
304 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
305 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
307 statuschar = self.status[0]
308 severitychar = self.severity[0]
309 chars = "%c%c" % (statuschar, severitychar)
310 bugout = "%s:%s: %s" % (self.id.user(),chars,self.summary.rstrip('\n'))
312 if show_comments == True:
313 self.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
314 comout = self.comment_root.string_thread(flatten=False)
315 output = bugout + '\n' + comout.rstrip('\n')
320 def xml(self, indent=0, show_comments=False):
322 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.xml()')
323 >>> bugA.uuid = 'bugA'
324 >>> bugA.time_string = 'Thu, 01 Jan 1970 00:00:00 +0000'
325 >>> bugA.creator = u'Frank'
326 >>> bugA.extra_strings += ['TAG: very helpful']
327 >>> commA = bugA.comment_root.new_reply(body='comment A')
328 >>> commA.uuid = 'commA'
329 >>> commA.date = 'Thu, 01 Jan 1970 00:01:00 +0000'
330 >>> commB = commA.new_reply(body='comment B')
331 >>> commB.uuid = 'commB'
332 >>> commB.date = 'Thu, 01 Jan 1970 00:02:00 +0000'
333 >>> commC = commB.new_reply(body='comment C')
334 >>> commC.uuid = 'commC'
335 >>> commC.date = 'Thu, 01 Jan 1970 00:03:00 +0000'
336 >>> print(bugA.xml(show_comments=True)) # doctest: +REPORT_UDIFF
339 <short-name>/bug</short-name>
340 <severity>minor</severity>
341 <status>open</status>
342 <creator>Frank</creator>
343 <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
344 <summary>Need to test Bug.xml()</summary>
345 <extra-string>TAG: very helpful</extra-string>
348 <short-name>/bug/commA</short-name>
350 <date>Thu, 01 Jan 1970 00:01:00 +0000</date>
351 <content-type>text/plain</content-type>
352 <body>comment A</body>
356 <short-name>/bug/commB</short-name>
357 <in-reply-to>commA</in-reply-to>
359 <date>Thu, 01 Jan 1970 00:02:00 +0000</date>
360 <content-type>text/plain</content-type>
361 <body>comment B</body>
365 <short-name>/bug/commC</short-name>
366 <in-reply-to>commB</in-reply-to>
368 <date>Thu, 01 Jan 1970 00:03:00 +0000</date>
369 <content-type>text/plain</content-type>
370 <body>comment C</body>
373 >>> print(bugA.xml(show_comments=True, indent=2))
374 ... # doctest: +REPORT_UDIFF
377 <short-name>/bug</short-name>
378 <severity>minor</severity>
379 <status>open</status>
380 <creator>Frank</creator>
381 <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
382 <summary>Need to test Bug.xml()</summary>
383 <extra-string>TAG: very helpful</extra-string>
386 <short-name>/bug/commA</short-name>
388 <date>Thu, 01 Jan 1970 00:01:00 +0000</date>
389 <content-type>text/plain</content-type>
390 <body>comment A</body>
394 <short-name>/bug/commB</short-name>
395 <in-reply-to>commA</in-reply-to>
397 <date>Thu, 01 Jan 1970 00:02:00 +0000</date>
398 <content-type>text/plain</content-type>
399 <body>comment B</body>
403 <short-name>/bug/commC</short-name>
404 <in-reply-to>commB</in-reply-to>
406 <date>Thu, 01 Jan 1970 00:03:00 +0000</date>
407 <content-type>text/plain</content-type>
408 <body>comment C</body>
412 if self.time == None:
415 timestring = utility.time_to_str(self.time)
417 info = [('uuid', self.uuid),
418 ('short-name', self.id.user()),
419 ('severity', self.severity),
420 ('status', self.status),
421 ('assigned', self.assigned),
422 ('reporter', self.reporter),
423 ('creator', self.creator),
424 ('created', timestring),
425 ('summary', self.summary)]
429 lines.append(' <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
430 for estr in self.extra_strings:
431 lines.append(' <extra-string>%s</extra-string>' % estr)
432 if show_comments == True:
433 comout = self.comment_root.xml_thread(indent=indent+2)
435 comout = comout[indent:] # strip leading indent spaces
437 lines.append('</bug>')
440 return istring + sep.join(lines).rstrip('\n')
442 def from_xml(self, xml_string, preserve_uuids=False):
444 Note: If a bug uuid is given, set .alt_id to it's value.
445 >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
446 >>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000"
447 >>> bugA.creator = u'Fran\xe7ois'
448 >>> bugA.extra_strings += ['TAG: very helpful']
449 >>> commA = bugA.comment_root.new_reply(body='comment A')
450 >>> commB = bugA.comment_root.new_reply(body='comment B')
451 >>> commC = commA.new_reply(body='comment C')
452 >>> xml = bugA.xml(show_comments=True)
454 >>> bugB.from_xml(xml)
455 >>> bugB.xml(show_comments=True) == xml
457 >>> bugB.uuid = bugB.alt_id
458 >>> for comm in bugB.comments():
459 ... comm.uuid = comm.alt_id
460 ... comm.alt_id = None
461 >>> bugB.xml(show_comments=True) == xml
463 >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE
464 ['severity', 'status', 'creator', 'time', 'summary']
465 >>> len(list(bugB.comments()))
468 >>> bugC.from_xml(xml, preserve_uuids=True)
469 >>> bugC.uuid == bugA.uuid
472 if type(xml_string) == types.UnicodeType:
473 xml_string = xml_string.strip().encode('unicode_escape')
474 if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
477 bug = ElementTree.XML(xml_string)
479 raise utility.InvalidXML( \
480 'bug', bug, 'root element must be <bug>')
481 tags=['uuid','short-name','severity','status','assigned',
482 'reporter', 'creator','created','summary','extra-string']
483 self.explicit_attrs = []
487 for child in bug.getchildren():
488 if child.tag == 'short-name':
490 elif child.tag == 'comment':
491 comm = comment.Comment(bug=self)
492 comm.from_xml(child, preserve_uuids=preserve_uuids)
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)
519 'ignoring unknown tag {0} in {1}'.format(
520 child.tag, comment.tag))
521 if uuid != self.uuid:
522 if not hasattr(self, 'alt_id') or self.alt_id == None:
524 self.extra_strings = estrs
525 self.add_comments(comments, ignore_missing_references=True)
527 def add_comment(self, comment, *args, **kwargs):
529 Add a comment too the current bug, under the parent specified
530 by comment.in_reply_to.
531 Note: If a bug uuid is given, set .alt_id to it's value.
533 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.add_comment()')
534 >>> bugA.creator = 'Jack'
535 >>> commA = bugA.comment_root.new_reply(body='comment A')
536 >>> commA.uuid = 'commA'
537 >>> commB = comment.Comment(body='comment B')
538 >>> commB.uuid = 'commB'
539 >>> bugA.add_comment(commB)
540 >>> commC = comment.Comment(body='comment C')
541 >>> commC.uuid = 'commC'
542 >>> commC.in_reply_to = commA.uuid
543 >>> bugA.add_comment(commC)
544 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
547 <short-name>/012</short-name>
548 <severity>minor</severity>
549 <status>open</status>
550 <creator>Jack</creator>
551 <created>...</created>
552 <summary>Need to test Bug.add_comment()</summary>
555 <short-name>/012/commA</short-name>
558 <content-type>text/plain</content-type>
559 <body>comment A</body>
563 <short-name>/012/commC</short-name>
564 <in-reply-to>commA</in-reply-to>
567 <content-type>text/plain</content-type>
568 <body>comment C</body>
572 <short-name>/012/commB</short-name>
575 <content-type>text/plain</content-type>
576 <body>comment B</body>
580 self.add_comments([comment], **kwargs)
582 def add_comments(self, comments, default_parent=None,
583 ignore_missing_references=False):
585 Convert a raw list of comments to single root comment. If a
586 comment does not specify a parent with .in_reply_to, the
587 parent defaults to .comment_root, but you can specify another
588 default parent via default_parent.
591 if default_parent == None:
592 default_parent = self.comment_root
593 for c in list(self.comments()) + comments:
594 assert c.uuid != None
595 assert c.uuid not in uuid_map
598 uuid_map[c.alt_id] = c
599 uuid_map[None] = self.comment_root
600 uuid_map[comment.INVALID_UUID] = self.comment_root
601 if default_parent != self.comment_root:
602 assert default_parent.uuid in uuid_map, default_parent.uuid
604 if c.in_reply_to == None \
605 and default_parent.uuid != comment.INVALID_UUID:
606 c.in_reply_to = default_parent.uuid
607 elif c.in_reply_to == comment.INVALID_UUID:
610 parent = uuid_map[c.in_reply_to]
612 if ignore_missing_references == True:
614 'ignoring missing reference to {0}'.format(
616 parent = default_parent
617 if parent.uuid != comment.INVALID_UUID:
618 c.in_reply_to = parent.uuid
620 raise comment.MissingReference(c)
624 def merge(self, other, accept_changes=True,
625 accept_extra_strings=True, accept_comments=True,
626 change_exception=False):
628 Merge info from other into this bug. Overrides any attributes
629 in self that are listed in other.explicit_attrs.
631 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()')
632 >>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
633 >>> bugA.creator = 'Frank'
634 >>> bugA.extra_strings += ['TAG: very helpful']
635 >>> bugA.extra_strings += ['TAG: favorite']
636 >>> commA = bugA.comment_root.new_reply(body='comment A')
637 >>> commA.uuid = 'uuid-commA'
638 >>> bugB = Bug(uuid='3210', summary='More tests for Bug.merge()')
639 >>> bugB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
640 >>> bugB.creator = 'John'
641 >>> bugB.explicit_attrs = ['creator', 'summary']
642 >>> bugB.extra_strings += ['TAG: very helpful']
643 >>> bugB.extra_strings += ['TAG: useful']
644 >>> commB = bugB.comment_root.new_reply(body='comment B')
645 >>> commB.uuid = 'uuid-commB'
646 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
647 ... accept_comments=False, change_exception=False)
648 >>> print bugA.creator
650 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
651 ... accept_comments=False, change_exception=True)
652 Traceback (most recent call last):
654 ValueError: Merge would change creator "Frank"->"John" for bug 0123
655 >>> print bugA.creator
657 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=False,
658 ... accept_comments=False, change_exception=True)
659 Traceback (most recent call last):
661 ValueError: Merge would add extra string "TAG: useful" for bug 0123
662 >>> print bugA.creator
664 >>> print bugA.extra_strings
665 ['TAG: favorite', 'TAG: very helpful']
666 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
667 ... accept_comments=False, change_exception=True)
668 Traceback (most recent call last):
670 ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123
671 >>> print bugA.extra_strings
672 ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
673 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
674 ... accept_comments=True, change_exception=True)
675 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
678 <short-name>/012</short-name>
679 <severity>minor</severity>
680 <status>open</status>
681 <creator>John</creator>
682 <created>...</created>
683 <summary>More tests for Bug.merge()</summary>
684 <extra-string>TAG: favorite</extra-string>
685 <extra-string>TAG: useful</extra-string>
686 <extra-string>TAG: very helpful</extra-string>
688 <uuid>uuid-commA</uuid>
689 <short-name>/012/uuid-commA</short-name>
692 <content-type>text/plain</content-type>
693 <body>comment A</body>
696 <uuid>uuid-commB</uuid>
697 <short-name>/012/uuid-commB</short-name>
700 <content-type>text/plain</content-type>
701 <body>comment B</body>
705 if hasattr(other, 'explicit_attrs'):
706 for attr in other.explicit_attrs:
707 old = getattr(self, attr)
708 new = getattr(other, attr)
711 setattr(self, attr, new)
712 elif change_exception:
714 ('Merge would change {} "{}"->"{}" for bug {}'
715 ).format(attr, old, new, self.uuid))
716 for estr in other.extra_strings:
717 if not estr in self.extra_strings:
718 if accept_extra_strings == True:
719 self.extra_strings += [estr]
720 elif change_exception == True:
722 'Merge would add extra string "%s" for bug %s' \
724 for o_comm in other.comments():
726 s_comm = self.comment_root.comment_from_uuid(o_comm.uuid)
729 s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id)
733 if accept_comments == True:
734 o_comm_copy = copy.copy(o_comm)
735 o_comm_copy.bug = self
736 o_comm_copy.id = libbe.util.id.ID(o_comm_copy, 'comment')
737 self.comment_root.add_reply(o_comm_copy)
738 elif change_exception == True:
740 'Merge would add comment %s (alt: %s) to bug %s' \
741 % (o_comm.uuid, o_comm.alt_id, self.uuid)
743 s_comm.merge(o_comm, accept_changes=accept_changes,
744 accept_extra_strings=accept_extra_strings,
745 change_exception=change_exception)
747 # methods for saving/loading/acessing settings and properties.
749 def load_settings(self, settings_mapfile=None):
750 if settings_mapfile == None:
751 settings_mapfile = self.storage.get(
752 self.id.storage('values'), '{}\n')
754 settings = mapfile.parse(settings_mapfile)
755 except mapfile.InvalidMapfileContents, e:
756 raise Exception('Invalid settings file for bug %s\n'
757 '(BE version missmatch?)' % self.id.user())
758 self._setup_saved_settings(settings)
760 def save_settings(self):
761 mf = mapfile.generate(self._get_saved_settings())
762 self.storage.set(self.id.storage('values'), mf)
766 Save any loaded contents to storage. Because of lazy loading
767 of comments, this is actually not too inefficient.
769 However, if self.storage.is_writeable() == True, then any
770 changes are automatically written to storage as soon as they
771 happen, so calling this method will just waste time (unless
772 something else has been messing with your stored files).
774 assert self.storage != None, "Can't save without storage"
775 if self.bugdir != None:
776 parent = self.bugdir.id.storage()
779 self.storage.add(self.id.storage(), parent=parent, directory=True)
780 self.storage.add(self.id.storage('values'), parent=self.id.storage(),
783 if len(self.comment_root) > 0:
784 comment.save_comments(self)
786 def load_comments(self, load_full=True):
787 if load_full == True:
788 # Force a complete load of the whole comment tree
789 self.comment_root = self._get_comment_root(load_full=True)
791 # Setup for fresh lazy-loading. Clear _comment_root, so
792 # next _get_comment_root returns a fresh version. Turn of
793 # writing temporarily so we don't write our blank comment
795 w = self.storage.writeable
796 self.storage.writeable = False
797 self.comment_root = None
798 self.storage.writeable = w
801 self.storage.recursive_remove(self.id.storage())
803 # methods for managing comments
806 for comment in self.comments():
810 for comment in self.comment_root.traverse():
813 def new_comment(self, body=None):
814 comm = self.comment_root.new_reply(body=body)
817 def comment_from_uuid(self, uuid, *args, **kwargs):
818 return self.comment_root.comment_from_uuid(uuid, *args, **kwargs)
820 # methods for id generation
822 def sibling_uuids(self):
823 if self.bugdir != None:
824 return self.bugdir.uuids()
828 # The general rule for bug sorting is that "more important" bugs are
829 # less than "less important" bugs. This way sorting a list of bugs
830 # will put the most important bugs first in the list. When relative
831 # importance is unclear, the sorting follows some arbitrary convention
832 # (i.e. dictionary order).
834 def cmp_severity(bug_1, bug_2):
836 Compare the severity levels of two bugs, with more severe bugs
841 >>> bugA.severity = bugB.severity = "wishlist"
842 >>> cmp_severity(bugA, bugB) == 0
844 >>> bugB.severity = "minor"
845 >>> cmp_severity(bugA, bugB) > 0
847 >>> bugA.severity = "critical"
848 >>> cmp_severity(bugA, bugB) < 0
851 if not hasattr(bug_2, "severity") :
853 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
855 def cmp_status(bug_1, bug_2):
857 Compare the status levels of two bugs, with more "open" bugs
862 >>> bugA.status = bugB.status = "open"
863 >>> cmp_status(bugA, bugB) == 0
865 >>> bugB.status = "closed"
866 >>> cmp_status(bugA, bugB) < 0
868 >>> bugA.status = "fixed"
869 >>> cmp_status(bugA, bugB) > 0
872 if not hasattr(bug_2, "status") :
874 val_2 = status_index[bug_2.status]
875 return cmp(status_index[bug_1.status], status_index[bug_2.status])
877 def cmp_attr(bug_1, bug_2, attr, invert=False):
879 Compare a general attribute between two bugs using the
880 conventional comparison rule for that attribute type. If
881 ``invert==True``, sort *against* that convention.
886 >>> bugA.severity = "critical"
887 >>> bugB.severity = "wishlist"
888 >>> cmp_attr(bugA, bugB, attr) < 0
890 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
892 >>> bugB.severity = "critical"
893 >>> cmp_attr(bugA, bugB, attr) == 0
896 if not hasattr(bug_2, attr) :
898 val_1 = getattr(bug_1, attr)
899 val_2 = getattr(bug_2, attr)
900 if val_1 == None: val_1 = None
901 if val_2 == None: val_2 = None
904 return -cmp(val_1, val_2)
906 return cmp(val_1, val_2)
908 # alphabetical rankings (a < z)
909 cmp_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid")
910 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
911 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
912 cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter")
913 cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary")
914 cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings")
915 # chronological rankings (newer < older)
916 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
918 def cmp_mine(bug_1, bug_2):
919 user_id = libbe.ui.util.user.get_user_id(bug_1.storage)
920 mine_1 = bug_1.assigned != user_id
921 mine_2 = bug_2.assigned != user_id
922 return cmp(mine_1, mine_2)
924 def cmp_comments(bug_1, bug_2):
926 Compare two bugs' comments lists. Doesn't load any new comments,
927 so you should call each bug's .load_comments() first if you want a
930 comms_1 = sorted(bug_1.comments(), key = lambda comm : comm.uuid)
931 comms_2 = sorted(bug_2.comments(), key = lambda comm : comm.uuid)
932 result = cmp(len(comms_1), len(comms_2))
935 for c_1,c_2 in zip(comms_1, comms_2):
936 result = cmp(c_1, c_2)
941 DEFAULT_CMP_FULL_CMP_LIST = \
942 (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator,
943 cmp_reporter, cmp_comments, cmp_summary, cmp_uuid, cmp_extra_strings)
945 class BugCompoundComparator (object):
946 def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
947 self.cmp_list = cmp_list
948 def __call__(self, bug_1, bug_2):
949 for comparison in self.cmp_list :
950 val = comparison(bug_1, bug_2)
955 cmp_full = BugCompoundComparator()
958 # define some bonus cmp_* functions
959 def cmp_last_modified(bug_1, bug_2):
961 Like cmp_time(), but use most recent comment instead of bug
962 creation for the timestamp.
964 def last_modified(bug):
966 for comment in bug.comment_root.traverse():
967 if comment.time > time:
970 val_1 = last_modified(bug_1)
971 val_2 = last_modified(bug_2)
972 return -cmp(val_1, val_2)
975 if libbe.TESTING == True:
976 suite = doctest.DocTestSuite()