Transition to libbe.LOG for logging
[be.git] / libbe / bug.py
index 8bf32dd45705fbb4984826f44ef6fbdf414c86cc..bb7a37da963a8db0bead90bf4f3ab1dd47a8eca9 100644 (file)
@@ -1,22 +1,27 @@
-# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2012 Chris Ball <cjb@laptop.org>
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Niall Douglas (s_sourceforge@nedprod.com) <spam@spamtrap.com>
+#                         Robert Lehmann <mail@robertlehmann.de>
 #                         Thomas Habets <thomas@habets.pp.se>
-#                         W. Trevor King <wking@drexel.edu>
+#                         Valtteri Kokkoniemi <rvk@iki.fi>
+#                         W. Trevor King <wking@tremily.us>
 #
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
+# This file is part of Bugs Everywhere.
 #
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
+# Bugs Everywhere is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 2 of the License, or (at your option) any
+# later version.
 #
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# Bugs Everywhere is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.
 
-"""Define the :class:`Bug` class for representing bugs.
+"""Define :py:class:`Bug` for representing bugs.
 """
 
 import copy
@@ -46,11 +51,6 @@ if libbe.TESTING == True:
     import doctest
 
 
-class DiskAccessRequired (Exception):
-    def __init__(self, goal):
-        msg = "Cannot %s without accessing the disk" % goal
-        Exception.__init__(self, msg)
-
 ### Define and describe valid bug categories
 # Use a tuple of (category, description) tuples since we don't have
 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
@@ -122,9 +122,9 @@ load_status(active_status_def, inactive_status_def)
 
 class Bug (settings_object.SavedSettingsObject):
     """A bug (or issue) is a place to store attributes and attach
-    :class:`~libbe.comment.Comment`\s.  In mailing-list terms, a bug is
+    :py:class:`~libbe.comment.Comment`\s.  In mailing-list terms, a bug is
     analogous to a thread.  Bugs are normally stored in
-    :class:`~libbe.bugdir.BugDir`\s.
+    :py:class:`~libbe.bugdir.BugDir`\s.
 
     >>> b = Bug()
     >>> print b.status
@@ -198,10 +198,19 @@ class Bug (settings_object.SavedSettingsObject):
 
     def _get_time(self):
         if self.time_string == None:
+            self._cached_time_string = None
+            self._cached_time = None
             return None
-        return utility.str_to_time(self.time_string)
+        if (not hasattr(self, '_cached_time_string')
+            or self.time_string != self._cached_time_string):
+            self._cached_time_string = self.time_string
+            self._cached_time = utility.str_to_time(self.time_string)
+        return self._cached_time
     def _set_time(self, value):
-        self.time_string = utility.time_to_str(value)
+        if not hasattr(self, '_cached_time') or value != self._cached_time:
+            self.time_string = utility.time_to_str(value)
+            self._cached_time_string = self.time_string
+            self._cached_time = value
     time = property(fget=_get_time,
                     fset=_set_time,
                     doc="An integer version of .time_string")
@@ -289,6 +298,8 @@ class Bug (settings_object.SavedSettingsObject):
                     ("Reporter", self._setting_attr_string("reporter")),
                     ("Creator", self._setting_attr_string("creator")),
                     ("Created", timestring)]
+            for estr in self.extra_strings:
+                info.append(('Extra string', estr))
             longest_key_len = max([len(k) for k,v in info])
             infolines = ["  %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
             bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
@@ -307,6 +318,97 @@ class Bug (settings_object.SavedSettingsObject):
         return output
 
     def xml(self, indent=0, show_comments=False):
+        """
+        >>> bugA = Bug(uuid='0123', summary='Need to test Bug.xml()')
+        >>> bugA.uuid = 'bugA'
+        >>> bugA.time_string = 'Thu, 01 Jan 1970 00:00:00 +0000'
+        >>> bugA.creator = u'Frank'
+        >>> bugA.extra_strings += ['TAG: very helpful']
+        >>> commA = bugA.comment_root.new_reply(body='comment A')
+        >>> commA.uuid = 'commA'
+        >>> commA.date = 'Thu, 01 Jan 1970 00:01:00 +0000'
+        >>> commB = commA.new_reply(body='comment B')
+        >>> commB.uuid = 'commB'
+        >>> commB.date = 'Thu, 01 Jan 1970 00:02:00 +0000'
+        >>> commC = commB.new_reply(body='comment C')
+        >>> commC.uuid = 'commC'
+        >>> commC.date = 'Thu, 01 Jan 1970 00:03:00 +0000'
+        >>> print(bugA.xml(show_comments=True))  # doctest: +REPORT_UDIFF
+        <bug>
+          <uuid>bugA</uuid>
+          <short-name>/bug</short-name>
+          <severity>minor</severity>
+          <status>open</status>
+          <creator>Frank</creator>
+          <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+          <summary>Need to test Bug.xml()</summary>
+          <extra-string>TAG: very helpful</extra-string>
+          <comment>
+            <uuid>commA</uuid>
+            <short-name>/bug/commA</short-name>
+            <author></author>
+            <date>Thu, 01 Jan 1970 00:01:00 +0000</date>
+            <content-type>text/plain</content-type>
+            <body>comment A</body>
+          </comment>
+          <comment>
+            <uuid>commB</uuid>
+            <short-name>/bug/commB</short-name>
+            <in-reply-to>commA</in-reply-to>
+            <author></author>
+            <date>Thu, 01 Jan 1970 00:02:00 +0000</date>
+            <content-type>text/plain</content-type>
+            <body>comment B</body>
+          </comment>
+          <comment>
+            <uuid>commC</uuid>
+            <short-name>/bug/commC</short-name>
+            <in-reply-to>commB</in-reply-to>
+            <author></author>
+            <date>Thu, 01 Jan 1970 00:03:00 +0000</date>
+            <content-type>text/plain</content-type>
+            <body>comment C</body>
+          </comment>
+        </bug>
+        >>> print(bugA.xml(show_comments=True, indent=2))
+        ... # doctest: +REPORT_UDIFF
+          <bug>
+            <uuid>bugA</uuid>
+            <short-name>/bug</short-name>
+            <severity>minor</severity>
+            <status>open</status>
+            <creator>Frank</creator>
+            <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+            <summary>Need to test Bug.xml()</summary>
+            <extra-string>TAG: very helpful</extra-string>
+            <comment>
+              <uuid>commA</uuid>
+              <short-name>/bug/commA</short-name>
+              <author></author>
+              <date>Thu, 01 Jan 1970 00:01:00 +0000</date>
+              <content-type>text/plain</content-type>
+              <body>comment A</body>
+            </comment>
+            <comment>
+              <uuid>commB</uuid>
+              <short-name>/bug/commB</short-name>
+              <in-reply-to>commA</in-reply-to>
+              <author></author>
+              <date>Thu, 01 Jan 1970 00:02:00 +0000</date>
+              <content-type>text/plain</content-type>
+              <body>comment B</body>
+            </comment>
+            <comment>
+              <uuid>commC</uuid>
+              <short-name>/bug/commC</short-name>
+              <in-reply-to>commB</in-reply-to>
+              <author></author>
+              <date>Thu, 01 Jan 1970 00:03:00 +0000</date>
+              <content-type>text/plain</content-type>
+              <body>comment C</body>
+            </comment>
+          </bug>
+        """
         if self.time == None:
             timestring = ""
         else:
@@ -329,14 +431,15 @@ class Bug (settings_object.SavedSettingsObject):
             lines.append('  <extra-string>%s</extra-string>' % estr)
         if show_comments == True:
             comout = self.comment_root.xml_thread(indent=indent+2)
-            if len(comout) > 0:
+            if comout:
+                comout = comout[indent:]  # strip leading indent spaces
                 lines.append(comout)
         lines.append('</bug>')
         istring = ' '*indent
         sep = '\n' + istring
         return istring + sep.join(lines).rstrip('\n')
 
-    def from_xml(self, xml_string, verbose=True):
+    def from_xml(self, xml_string, preserve_uuids=False):
         u"""
         Note: If a bug uuid is given, set .alt_id to it's value.
         >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
@@ -348,7 +451,7 @@ class Bug (settings_object.SavedSettingsObject):
         >>> commC = commA.new_reply(body='comment C')
         >>> xml = bugA.xml(show_comments=True)
         >>> bugB = Bug()
-        >>> bugB.from_xml(xml, verbose=True)
+        >>> bugB.from_xml(xml)
         >>> bugB.xml(show_comments=True) == xml
         False
         >>> bugB.uuid = bugB.alt_id
@@ -358,9 +461,13 @@ class Bug (settings_object.SavedSettingsObject):
         >>> bugB.xml(show_comments=True) == xml
         True
         >>> bugB.explicit_attrs  # doctest: +NORMALIZE_WHITESPACE
-        ['severity', 'status', 'creator', 'created', 'summary']
+        ['severity', 'status', 'creator', 'time', 'summary']
         >>> len(list(bugB.comments()))
         3
+        >>> bugC = Bug()
+        >>> bugC.from_xml(xml, preserve_uuids=True)
+        >>> bugC.uuid == bugA.uuid
+        True
         """
         if type(xml_string) == types.UnicodeType:
             xml_string = xml_string.strip().encode('unicode_escape')
@@ -370,7 +477,7 @@ class Bug (settings_object.SavedSettingsObject):
             bug = ElementTree.XML(xml_string)
         if bug.tag != 'bug':
             raise utility.InvalidXML( \
-                'bug', bug, 'root element must be <comment>')
+                'bug', bug, 'root element must be <bug>')
         tags=['uuid','short-name','severity','status','assigned',
               'reporter', 'creator','created','summary','extra-string']
         self.explicit_attrs = []
@@ -382,7 +489,7 @@ class Bug (settings_object.SavedSettingsObject):
                 pass
             elif child.tag == 'comment':
                 comm = comment.Comment(bug=self)
-                comm.from_xml(child)
+                comm.from_xml(child, preserve_uuids=preserve_uuids)
                 comments.append(comm)
                 continue
             elif child.tag in tags:
@@ -390,19 +497,27 @@ class Bug (settings_object.SavedSettingsObject):
                     text = settings_object.EMPTY
                 else:
                     text = xml.sax.saxutils.unescape(child.text)
-                    text = text.decode('unicode_escape').strip()
-                if child.tag == 'uuid':
+                    if not isinstance(text, unicode):
+                        text = text.decode('unicode_escape')
+                    text = text.strip()
+                if child.tag == 'uuid' and not preserve_uuids:
                     uuid = text
                     continue # don't set the bug's uuid tag.
+                elif child.tag == 'created':
+                    if text is not settings_object.EMPTY:
+                        self.time = utility.str_to_time(text)
+                        self.explicit_attrs.append('time')
+                    continue
                 elif child.tag == 'extra-string':
                     estrs.append(text)
                     continue # don't set the bug's extra_string yet.
                 attr_name = child.tag.replace('-','_')
                 self.explicit_attrs.append(attr_name)
                 setattr(self, attr_name, text)
-            elif verbose == True:
-                print >> sys.stderr, 'Ignoring unknown tag %s in %s' \
-                    % (child.tag, comment.tag)
+            else:
+                libbe.LOG.warning(
+                    'ignoring unknown tag {0} in {1}'.format(
+                        child.tag, comment.tag))
         if uuid != self.uuid:
             if not hasattr(self, 'alt_id') or self.alt_id == None:
                 self.alt_id = uuid
@@ -495,8 +610,9 @@ class Bug (settings_object.SavedSettingsObject):
                 parent = uuid_map[c.in_reply_to]
             except KeyError:
                 if ignore_missing_references == True:
-                    print >> sys.stderr, \
-                        'Ignoring missing reference to %s' % c.in_reply_to
+                    libbe.LOG.warning(
+                        'ignoring missing reference to {0}'.format(
+                            c.in_reply_to))
                     parent = default_parent
                     if parent.uuid != comment.INVALID_UUID:
                         c.in_reply_to = parent.uuid
@@ -586,20 +702,21 @@ class Bug (settings_object.SavedSettingsObject):
           </comment>
         </bug>
         """
-        for attr in other.explicit_attrs:
-            old = getattr(self, attr)
-            new = getattr(other, attr)
-            if old != new:
-                if accept_changes == True:
-                    setattr(self, attr, new)
-                elif change_exception == True:
-                    raise ValueError, \
-                        'Merge would change %s "%s"->"%s" for bug %s' \
-                        % (attr, old, new, self.uuid)
+        if hasattr(other, 'explicit_attrs'):
+            for attr in other.explicit_attrs:
+                old = getattr(self, attr)
+                new = getattr(other, attr)
+                if old != new:
+                    if accept_changes:
+                        setattr(self, attr, new)
+                    elif change_exception:
+                        raise ValueError(
+                            ('Merge would change {} "{}"->"{}" for bug {}'
+                             ).format(attr, old, new, self.uuid))
         for estr in other.extra_strings:
             if not estr in self.extra_strings:
                 if accept_extra_strings == True:
-                    self.extra_strings.append(estr)
+                    self.extra_strings += [estr]
                 elif change_exception == True:
                     raise ValueError, \
                         'Merge would add extra string "%s" for bug %s' \
@@ -631,8 +748,8 @@ class Bug (settings_object.SavedSettingsObject):
 
     def load_settings(self, settings_mapfile=None):
         if settings_mapfile == None:
-            settings_mapfile = \
-                self.storage.get(self.id.storage('values'), default='\n')
+            settings_mapfile = self.storage.get(
+                self.id.storage('values'), '{}\n')
         try:
             settings = mapfile.parse(settings_mapfile)
         except mapfile.InvalidMapfileContents, e:
@@ -798,6 +915,12 @@ cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings"
 # chronological rankings (newer < older)
 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
 
+def cmp_mine(bug_1, bug_2):
+    user_id = libbe.ui.util.user.get_user_id(bug_1.storage)
+    mine_1 = bug_1.assigned != user_id
+    mine_2 = bug_2.assigned != user_id
+    return cmp(mine_1, mine_2)
+
 def cmp_comments(bug_1, bug_2):
     """
     Compare two bugs' comments lists.  Doesn't load any new comments,