-# Copyright (C) 2008-2009 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>
# Thomas Habets <thomas@habets.pp.se>
-# W. Trevor King <wking@drexel.edu>
+# 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 Comment class for representing bug comments.
+"""Define :py:class:`Comment` for representing bug comments.
"""
import base64
import sys
import time
import types
+try:
+ from email.mime.base import MIMEBase
+ from email.encoders import encode_base64
+except ImportError:
+ # adjust to old python 2.4
+ from email.MIMEBase import MIMEBase
+ from email.Encoders import encode_base64
try: # import core module, Python >= 2.5
from xml.etree import ElementTree
except ImportError: # look for non-core module
import doctest
-class InvalidShortname(KeyError):
- def __init__(self, shortname, shortnames):
- msg = "Invalid shortname %s\n%s" % (shortname, shortnames)
- KeyError.__init__(self, msg)
- self.shortname = shortname
- self.shortnames = shortnames
-
class MissingReference(ValueError):
def __init__(self, comment):
msg = "Missing reference to %s" % (comment.in_reply_to)
self.reference = comment.in_reply_to
self.comment = comment
-class DiskAccessRequired (Exception):
- def __init__(self, goal):
- msg = "Cannot %s without accessing the disk" % goal
- Exception.__init__(self, msg)
-
INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!"
def load_comments(bug, load_full=False):
from disk *now*, rather than waiting and lazy loading as required.
"""
uuids = []
- for id in libbe.util.id.child_uuids(bug.storage.children()):
+ for id in libbe.util.id.child_uuids(
+ bug.storage.children(
+ bug.id.storage())):
uuids.append(id)
comments = []
for uuid in uuids:
dummy = comm.body # force the body to load
comments.append(comm)
bug.comment_root = Comment(bug, uuid=INVALID_UUID)
- bug.add_comments(comments)
+ bug.add_comments(comments, ignore_missing_references=True)
return bug.comment_root
def save_comments(bug):
for comment in bug.comment_root.traverse():
+ comment.bug = bug
+ comment.storage = bug.storage
comment.save()
-class Comment(Tree, settings_object.SavedSettingsObject):
- """
+class Comment (Tree, settings_object.SavedSettingsObject):
+ """Comments are a notes that attach to :py:class:`~libbe.bug.Bug`\s in
+ threaded trees. In mailing-list terms, a comment is analogous to
+ a single part of an email.
+
>>> c = Comment()
>>> c.uuid != None
True
doc="Alternate ID for linking imported comments. Internally comments are linked (via In-reply-to) to the parent's UUID. However, these UUIDs are generated internally, so Alt-id is provided as a user-controlled linking target.")
def alt_id(): return {}
- def _get_user_id(self):
- if self.bug != None:
- return self.bug._get_user_id()
- return None
-
@_versioned_property(name="Author",
- doc="The author of the comment",
- generator=_get_user_id)
+ doc="The author of the comment")
def author(): return {}
@_versioned_property(name="In-reply-to",
decode=self.content_type.startswith("text/"))
def _set_comment_body(self, old=None, new=None, force=False):
assert self.uuid != INVALID_UUID, self
+ if self.content_type.startswith('text/') \
+ and self.bug != None and self.bug.bugdir != None:
+ new = libbe.util.id.short_to_long_text(
+ {self.bug.bugdir.uuid: self.bug.bugdir}, new)
if (self.storage != None and self.storage.writeable == True) \
or force==True:
assert new != None, "Can't save empty comment"
def extra_strings(): return {}
def __init__(self, bug=None, uuid=None, from_storage=False,
- in_reply_to=None, body=None):
+ in_reply_to=None, body=None, content_type=None):
"""
- Set from_storage=True to load an old comment.
- Set from_storage=False to create a new comment.
-
- The uuid option is required when from_storage==True.
-
- The in_reply_to and body options are only used if
- from_storage==False (the default). When from_storage==True,
- they are loaded from the bug database.
-
- in_reply_to should be the uuid string of the parent comment.
+ Set ``from_storage=True`` to load an old comment.
+ Set ``from_storage=False`` to create a new comment.
+
+ The ``uuid`` option is required when ``from_storage==True``.
+
+ The in_reply_to, body, and content_type options are only used
+ if ``from_storage==False`` (the default). When
+ ``from_storage==True``, they are loaded from the bug database.
+ ``content_type`` decides if the body should be run through
+ :py:func:`util.id.short_to_long_text` before saving. See
+ :py:meth:`_set_comment_body` for details.
+
+ ``in_reply_to`` should be the uuid string of the parent comment.
"""
Tree.__init__(self)
settings_object.SavedSettingsObject.__init__(self)
self.bug = bug
self.storage = None
- self.uuid = uuid
+ self.uuid = uuid
self.id = libbe.util.id.ID(self, 'comment')
if from_storage == False:
if uuid == None:
self.uuid = libbe.util.id.uuid_gen()
- self.settings = {}
- self._setup_saved_settings()
self.time = int(time.time()) # only save to second precision
self.in_reply_to = in_reply_to
+ if content_type != None:
+ self.content_type = content_type
self.body = body
if self.bug != None:
self.storage = self.bug.storage
if from_storage == False:
- if self.storage != None and self.storage.is_writeable():
+ if self.storage != None and self.storage.is_writeable():
self.save()
def __cmp__(self, other):
return str(value)
return value
+ def safe_in_reply_to(self):
+ """
+ Return self.in_reply_to, except...
+
+ * if no comment matches that id, in which case return None.
+ * if that id matches another comments .alt_id, in which case
+ return the matching comments .uuid.
+ """
+ if self.in_reply_to == None:
+ return None
+ else:
+ try:
+ irt_comment = self.bug.comment_from_uuid(
+ self.in_reply_to, match_alt_id=True)
+ return irt_comment.uuid
+ except KeyError:
+ return None
+
def xml(self, indent=0):
"""
>>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
insightful
remarks</body>
</comment>
+ >>> comm.content_type = 'image/png'
+ >>> print comm.xml()
+ <comment>
+ <uuid>0123</uuid>
+ <short-name>//012</short-name>
+ <author></author>
+ <date>Thu, 01 Jan 1970 00:00:00 +0000</date>
+ <content-type>image/png</content-type>
+ <body>U29tZQppbnNpZ2h0ZnVsCnJlbWFya3MK
+ </body>
+ </comment>
"""
if self.content_type.startswith('text/'):
body = (self.body or '').rstrip('\n')
else:
maintype,subtype = self.content_type.split('/',1)
- msg = email.mime.base.MIMEBase(maintype, subtype)
+ msg = MIMEBase(maintype, subtype)
msg.set_payload(self.body or '')
- email.encoders.encode_base64(msg)
+ encode_base64(msg)
body = base64.encodestring(self.body or '')
info = [('uuid', self.uuid),
('alt-id', self.alt_id),
('short-name', self.id.user()),
- ('in-reply-to', self.in_reply_to),
+ ('in-reply-to', self.safe_in_reply_to()),
('author', self._setting_attr_string('author')),
('date', self.date),
('content-type', self.content_type),
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, verbose=True):
+ u"""
Note: If alt-id is not given, translates any <uuid> fields to
<alt-id> fields.
>>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
>>> commB.alt_id = None
>>> commB.xml() == xml
True
+ >>> commC = Comment()
+ >>> commC.from_xml(xml, preserve_uuids=True)
+ >>> commC.uuid == commA.uuid
+ True
"""
if type(xml_string) == types.UnicodeType:
xml_string = xml_string.strip().encode('unicode_escape')
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 comment's uuid tag.
elif child.tag == 'body':
self.body = base64.decodestring(body)
self.extra_strings = estrs
- def merge(self, other, accept_changes=True,
+ def merge(self, other, accept_changes=True,
accept_extra_strings=True, change_exception=False):
"""
Merge info from other into this comment. Overrides any
attributes in self that are listed in other.explicit_attrs.
+
>>> commA = Comment(bug=None, body='Some insightful remarks')
>>> commA.uuid = '0123'
>>> commA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
<extra-string>TAG: very helpful</extra-string>
</comment>
"""
- 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 comment %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 comment {}'
+ ).format(attr, old, new, self.uuid))
if self.alt_id == self.uuid:
self.alt_id = None
for estr in other.extra_strings:
lines.append("Date: %s" % self.date)
lines.append("")
if self.content_type.startswith("text/"):
- lines.extend((self.body or "").splitlines())
+ body = (self.body or "")
+ if self.bug != None and self.bug.bugdir != None:
+ body = libbe.util.id.long_to_short_text(
+ {self.bug.bugdir.uuid: self.bug.bugdir}, body)
+ lines.extend(body.splitlines())
else:
lines.append("Content type %s not printable. Try XML output instead" % self.content_type)
-
+
istring = ' '*indent
sep = '\n' + istring
return istring + sep.join(lines).rstrip('\n')
"""
Return a string displaying a thread of comments.
bug_shortname is only used if auto_name_map == True.
-
+
string_method_name (defaults to "string") is the name of the
Comment method used to generate the output string for each
Comment in the thread. The method must take the arguments
indent and shortname.
-
- SIDE-EFFECT: if auto_name_map==True, calls comment_shortnames()
- which will sort the tree by comment.time. Avoid by calling
- name_map = {}
- for shortname,comment in comm.comment_shortnames(bug_shortname):
- name_map[comment.uuid] = shortname
- comm.sort(key=lambda c : c.author) # your sort
- comm.string_thread(name_map=name_map)
>>> a = Comment(bug=None, uuid="a", body="Insightful remarks")
>>> a.time = utility.str_to_time("Thu, 20 Nov 2008 01:00:00 +0000")
# methods for saving/loading/acessing settings and properties.
def load_settings(self, settings_mapfile=None):
+ if self.uuid == INVALID_UUID:
+ return
if settings_mapfile == None:
- settings_mapfile = \
- self.storage.get(self.id.storage("values"), default="\n")
- self.settings = mapfile.parse(settings_mapfile)
- self._setup_saved_settings()
+ settings_mapfile = self.storage.get(
+ self.id.storage('values'), '{}\n')
+ try:
+ settings = mapfile.parse(settings_mapfile)
+ except mapfile.InvalidMapfileContents, e:
+ raise Exception('Invalid settings file for comment %s\n'
+ '(BE version missmatch?)' % self.id.user())
+ self._setup_saved_settings(settings)
def save_settings(self):
+ if self.uuid == INVALID_UUID:
+ return
mf = mapfile.generate(self._get_saved_settings())
self.storage.set(self.id.storage("values"), mf)
def save(self):
"""
Save any loaded contents to storage.
-
- However, if self.storage.is_writeable() == True, then any
+
+ However, if ``self.storage.is_writeable() == True``, then any
changes are automatically written to storage as soon as they
happen, so calling this method will just waste time (unless
something else has been messing with your stored files).
parent = self.bug.id.storage()
else:
parent = None
- self.storage.add(self.id.storage(), parent=parent)
- self.storage.add(self.id.storage('values'), parent=self.id.storage())
- self.storage.add(self.id.storage('body'), parent=self.id.storage())
+ self.storage.add(self.id.storage(), parent=parent, directory=True)
+ self.storage.add(self.id.storage('values'), parent=self.id.storage(),
+ directory=False)
+ self.storage.add(self.id.storage('body'), parent=self.id.storage(),
+ directory=False)
self.save_settings()
self._set_comment_body(new=self.body, force=True)
>>> repB.in_reply_to == repA.uuid
True
"""
- reply = Comment(self.bug, body=body)
- if content_type != None: # set before saving body to decide binary format
- reply.content_type = content_type
- if reply.storage != None and reply.storage.is_writeable():
- reply.save()
+ reply = Comment(self.bug, body=body, content_type=content_type)
self.add_reply(reply)
return reply
def comment_from_uuid(self, uuid, match_alt_id=True):
- """
- Use a uuid to look up a comment.
+ """Use a uuid to look up a comment.
+
>>> a = Comment(bug=None, uuid="a")
>>> b = a.new_reply()
>>> b.uuid = "b"
Compare a general attribute between two comments using the conventional
comparison rule for that attribute type. If invert == True, sort
*against* that convention.
+
>>> attr="author"
>>> commentA = Comment()
>>> commentB = Comment()
val_2 = getattr(comment_2, attr)
if val_1 == None: val_1 = None
if val_2 == None: val_2 = None
-
+
if invert == True :
return -cmp(val_1, val_2)
else :
if val != 0 :
return val
return 0
-
+
cmp_full = CommentCompoundComparator()
if libbe.TESTING == True: