diff options
author | W. Trevor King <wking@drexel.edu> | 2008-11-22 16:14:04 -0500 |
---|---|---|
committer | W. Trevor King <wking@drexel.edu> | 2008-11-22 16:14:04 -0500 |
commit | 93801606303d79cfe9c1483cd6627cb1b93dedc7 (patch) | |
tree | a2a60422a8ce46be21a23de2f69df758255f12e1 /libbe/comment.py | |
parent | 23179f50092d91dbeab97ad2b88cdaadb79b615f (diff) | |
download | bugseverywhere-93801606303d79cfe9c1483cd6627cb1b93dedc7.tar.gz |
Oops, these new submods are used by the new, classified Bug & BugDir.
I'd forgotten tell bzr...
Diffstat (limited to 'libbe/comment.py')
-rw-r--r-- | libbe/comment.py | 382 |
1 files changed, 382 insertions, 0 deletions
diff --git a/libbe/comment.py b/libbe/comment.py new file mode 100644 index 0000000..95dade6 --- /dev/null +++ b/libbe/comment.py @@ -0,0 +1,382 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc. +# <abentley@panoramicfeedback.com> +# +# 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 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. +# +# 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 +import os +import os.path +import time +import textwrap +import doctest + +from beuuid import uuid_gen +import mapfile +from tree import Tree +import utility + +INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" + +def _list_to_root(comments, bug): + """ + Convert a raw list of comments to single (dummy) root comment. We + use a dummy root comment, 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. + + No Comment method should use the dummy comment. + """ + root_comments = [] + uuid_map = {} + for comment in comments: + assert comment.uuid != None + uuid_map[comment.uuid] = comment + for comm in comments: + if comm.in_reply_to == None: + root_comments.append(comm) + else: + parentUUID = comm.in_reply_to + parent = uuid_map[parentUUID] + parent.add_reply(comm) + dummy_root = Comment(bug, uuid=INVALID_UUID) + dummy_root.extend(root_comments) + return dummy_root + +def loadComments(bug): + path = bug.get_path("comments") + if not os.path.isdir(path): + return Comment(bug, uuid=INVALID_UUID) + comments = [] + for uuid in os.listdir(path): + if uuid.startswith('.'): + continue + comm = Comment(bug, uuid, loadNow=True) + comments.append(comm) + return _list_to_root(comments, bug) + +def saveComments(bug): + path = bug.get_path("comments") + bug.rcs.mkdir(path) + for comment in bug.comment_root.traverse(): + comment.save() + +class Comment(Tree): + def __init__(self, bug=None, uuid=None, loadNow=False, + in_reply_to=None, body=None): + """ + Set loadNow=True to load an old bug. + Set loadNow=False to create a new bug. + + The uuid option is required when loadNow==True. + + The in_reply_to and body options are only used if + loadNow==False (the default). When loadNow==True, they are + loaded from the bug database. + + in_reply_to should be the uuid string of the parent comment. + """ + Tree.__init__(self) + self.bug = bug + if bug != None: + self.rcs = bug.rcs + else: + self.rcs = None + if loadNow == True: + self.uuid = uuid + self.load() + else: + if uuid != None: + self.uuid = uuid + else: + self.uuid = uuid_gen() + self.time = time.time() + if self.rcs != None: + self.From = self.rcs.get_user_id() + else: + self.From = None + self.in_reply_to = in_reply_to + self.content_type = "text/plain" + self.body = body + + def traverse(self, *args, **kwargs): + """Avoid working with the possible dummy root comment""" + for comment in Tree.traverse(self, *args, **kwargs): + if comment.uuid == INVALID_UUID: + continue + yield comment + + def _clean_string(self, value): + """ + >>> comm = Comment() + >>> comm._clean_string(None) + '' + >>> comm._clean_string("abc") + 'abc' + """ + if value == None: + return "" + return value + + def string(self, indent=0, shortname=None): + """ + >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") + >>> comm.time = utility.str_to_time("Thu, 20 Nov 2008 15:55:11 +0000") + >>> print comm.string(indent=2, shortname="com-1") + --------- Comment --------- + Name: com-1 + From: + Date: Thu, 20 Nov 2008 15:55:11 +0000 + <BLANKLINE> + Some insightful remarks + """ + if shortname == None: + shortname = self.uuid + lines = [] + lines.append("--------- Comment ---------") + lines.append("Name: %s" % shortname) + lines.append("From: %s" % self._clean_string(self.From)) + lines.append("Date: %s" % utility.time_to_str(self.time)) + lines.append("") + lines.append(textwrap.fill(self._clean_string(self.body), + width=(79-indent))) + + istring = ' '*indent + sep = '\n' + istring + return istring + sep.join(lines).rstrip('\n') + + def __str__(self): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> comm.uuid = "com-1" + >>> comm.time = utility.str_to_time("Thu, 20 Nov 2008 15:55:11 +0000") + >>> comm.From = "Jane Doe <jdoe@example.com>" + >>> print comm + --------- Comment --------- + Name: com-1 + From: Jane Doe <jdoe@example.com> + Date: Thu, 20 Nov 2008 15:55:11 +0000 + <BLANKLINE> + Some insightful remarks + """ + return self.string() + + def get_path(self, name=None): + my_dir = os.path.join(self.bug.get_path("comments"), self.uuid) + if name is None: + return my_dir + assert name in ["values", "body"] + return os.path.join(my_dir, name) + + def load(self): + map = mapfile.map_load(self.get_path("values")) + self.time = utility.str_to_time(map["Date"]) + self.From = map["From"] + self.in_reply_to = map.get("In-reply-to") + self.content_type = map.get("Content-type", "text/plain") + self.body = self.rcs.get_file_contents(self.get_path("body")) + + def save(self): + assert self.rcs != None + map_file = {"Date": utility.time_to_str(self.time)} + self._add_headers(map_file, ("From", "in_reply_to", "content_type")) + self.rcs.mkdir(self.get_path()) + mapfile.map_save(self.rcs, self.get_path("values"), map_file) + self.rcs.set_file_contents(self.get_path("body"), self.body) + + def _add_headers(self, map, names): + map_names = {} + for name in names: + map_names[name] = self._pyname_to_header(name) + self._add_attrs(map, map_names) + + def _pyname_to_header(self, name): + return name.capitalize().replace('_', '-') + + def _add_attrs(self, map, map_names): + for name in map_names.keys(): + value = getattr(self, name) + if value is not None: + map[map_names[name]] = value + + def remove(self): + for comment in self.traverse(): + path = comment.get_path() + self.rcs.recursive_remove(path) + + def add_reply(self, reply): + if reply.time != None and self.time != None: + assert reply.time > self.time + if self.uuid != INVALID_UUID: + reply.in_reply_to = self.uuid + self.append(reply) + + def new_reply(self, body=None): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> repA = comm.new_reply("Critique original comment") + >>> repB = repA.new_reply("Begin flamewar :p") + """ + reply = Comment(self.bug, body=body) + self.add_reply(reply) + return reply + + def string_thread(self, name_map={}, indent=0, + auto_name_map=False, bug_shortname=None): + """ + Return a sting displaying a thread of comments. + bug_shortname is only used if auto_name_map == True. + + >>> a = Comment(bug=None, uuid="a", body="Insightful remarks") + >>> a.time = utility.str_to_time("Thu, 20 Nov 2008 01:00:00 +0000") + >>> b = a.new_reply("Critique original comment") + >>> b.uuid = "b" + >>> b.time = utility.str_to_time("Thu, 20 Nov 2008 02:00:00 +0000") + >>> c = b.new_reply("Begin flamewar :p") + >>> c.uuid = "c" + >>> c.time = utility.str_to_time("Thu, 20 Nov 2008 03:00:00 +0000") + >>> d = a.new_reply("Useful examples") + >>> d.uuid = "d" + >>> d.time = utility.str_to_time("Thu, 20 Nov 2008 04:00:00 +0000") + >>> a.sort(key=lambda comm : comm.time) + >>> print a.string_thread() + --------- Comment --------- + Name: a + From: + Date: Thu, 20 Nov 2008 01:00:00 +0000 + <BLANKLINE> + Insightful remarks + --------- Comment --------- + Name: b + From: + Date: Thu, 20 Nov 2008 02:00:00 +0000 + <BLANKLINE> + Critique original comment + --------- Comment --------- + Name: c + From: + Date: Thu, 20 Nov 2008 03:00:00 +0000 + <BLANKLINE> + Begin flamewar :p + --------- Comment --------- + Name: d + From: + Date: Thu, 20 Nov 2008 04:00:00 +0000 + <BLANKLINE> + Useful examples + >>> print a.string_thread(auto_name_map=True, bug_shortname="bug-1") + --------- Comment --------- + Name: bug-1:1 + From: + Date: Thu, 20 Nov 2008 01:00:00 +0000 + <BLANKLINE> + Insightful remarks + --------- Comment --------- + Name: bug-1:2 + From: + Date: Thu, 20 Nov 2008 02:00:00 +0000 + <BLANKLINE> + Critique original comment + --------- Comment --------- + Name: bug-1:3 + From: + Date: Thu, 20 Nov 2008 03:00:00 +0000 + <BLANKLINE> + Begin flamewar :p + --------- Comment --------- + Name: bug-1:4 + From: + Date: Thu, 20 Nov 2008 04:00:00 +0000 + <BLANKLINE> + Useful examples + """ + if auto_name_map == True: + name_map = {} + for shortname,comment in self.comment_shortnames(bug_shortname): + name_map[comment.uuid] = shortname + stringlist = [] + for depth,comment in self.thread(flatten=True): + ind = 2*depth+indent + if comment.uuid in name_map: + sname = name_map[comment.uuid] + else: + sname = None + stringlist.append(comment.string(indent=ind, shortname=sname)) + return '\n'.join(stringlist) + + def comment_shortnames(self, bug_shortname=""): + """ + Iterate through (id, comment) pairs, in time order. + (This is a user-friendly id, not the comment uuid). + + SIDE-EFFECT : will sort the comment tree by comment.time + + >>> a = Comment(bug=None, uuid="a") + >>> b = a.new_reply() + >>> b.uuid = "b" + >>> c = b.new_reply() + >>> c.uuid = "c" + >>> d = a.new_reply() + >>> d.uuid = "d" + >>> for id,name in a.comment_shortnames("bug-1"): + ... print id, name.uuid + bug-1:1 a + bug-1:2 b + bug-1:3 c + bug-1:4 d + """ + self.sort(key=lambda comm : comm.time) + for num,comment in enumerate(self.traverse()): + yield ("%s:%d" % (bug_shortname, num+1), comment) + + def comment_from_shortname(self, comment_shortname, *args, **kwargs): + """ + Use a comment shortname to look up a comment. + >>> a = Comment(bug=None, uuid="a") + >>> b = a.new_reply() + >>> b.uuid = "b" + >>> c = b.new_reply() + >>> c.uuid = "c" + >>> d = a.new_reply() + >>> d.uuid = "d" + >>> comm = a.comment_from_shortname("bug-1:3", bug_shortname="bug-1") + >>> id(comm) == id(c) + True + """ + for cur_name, comment in self.comment_shortnames(*args, **kwargs): + if comment_shortname == cur_name: + return comment + raise KeyError(comment_shortname) + + def comment_from_uuid(self, uuid): + """ + Use a comment shortname to look up a comment. + >>> a = Comment(bug=None, uuid="a") + >>> b = a.new_reply() + >>> b.uuid = "b" + >>> c = b.new_reply() + >>> c.uuid = "c" + >>> d = a.new_reply() + >>> d.uuid = "d" + >>> comm = a.comment_from_uuid("d") + >>> id(comm) == id(d) + True + """ + for comment in self.traverse(): + if comment.uuid == uuid: + return comment + raise KeyError(uuid) + +suite = doctest.DocTestSuite() |