[Zope3-checkins] SVN: Zope3/trunk/ Revived the bugtracker from the dead (CVS). I updated the API calls and

Stephan Richter srichter at cosmos.phy.tufts.edu
Tue Jul 6 14:43:12 EDT 2004


Log message for revision 26117:
Revived the bugtracker from the dead (CVS). I updated the API calls and 
fixed the tests. I also modernized some of the code. 

The only new feature is that you can now choose the type of text you are 
entering in bug descriptions and comments, i.e. plain text, stx or 
rest. Also, principals are handled more sanely in the XML Export/Import.

After all, I was able to import the an old bug tracker export from last 
year, so I am sure things work again.




-=-
Added: Zope3/trunk/package-includes/bugtracker-configure.zcml
===================================================================
--- Zope3/trunk/package-includes/bugtracker-configure.zcml	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/package-includes/bugtracker-configure.zcml	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1 @@
+<include package="bugtracker" />

Added: Zope3/trunk/src/bugtracker/I18N.txt
===================================================================
--- Zope3/trunk/src/bugtracker/I18N.txt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/I18N.txt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,38 @@
+Internationalization (I18n) and Localalization (L10n)
+=====================================================
+
+  Crating/Updating Message Catalog Template (POT) Files
+  -----------------------------------------------------
+
+    0. Install 'bugtracker' in 'ZOPE3/src'. See INSTALL.txt for details.
+
+    1. Set the the Python path::
+       
+        export PYTHONPATH=ZOPE3/src
+  
+    2. Go into the 'locales' directory and execute extract.py::
+
+      python2.3 utilities/i18nextract.py -d bugtracker \
+                                         -p src/bugtracker/ \
+                                         -o locales
+
+
+  Updating Message Catalog (PO) Files
+  -----------------------------------
+
+    1. For each language do simply::
+
+        msgmerge -U de/LC_MESSAGES/bugtracker.po bugtracker.pot
+
+    2. Translate the updated PO file. 
+       Note: KBabel is a great tool for this task!
+
+
+  Compiling Message Catalogs (PO) to binary (MO) Files
+  ----------------------------------------------------
+
+    1. Go to the right directory, such as '<bugtracker>/locales/de/LC_MESSAGES'.
+
+    2. Run the following command::
+
+        msgfmt -o bugtracker.mo bugtracker.po

Added: Zope3/trunk/src/bugtracker/INSTALL.txt
===================================================================
--- Zope3/trunk/src/bugtracker/INSTALL.txt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/INSTALL.txt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,34 @@
+Installation
+============
+
+  1. Copy the 'bugtracker' folder to 'ZOPE3/src' in any other directory listed
+     in the PYTHONPATH. At the end you need to be able to import the bugtracker
+     without any prefixes:
+
+       >>> import bugtracker
+
+  2. Add a file named 'bugtracker-configure.zcml' in the
+     'ZOPE3/package-includes' directory containing the following line:
+
+       <include package='bugtracker' />
+
+  3. You need to define the following role declarations to your user in order
+     to use the bug tracker product effectively.
+
+      <grant role="bugtracker.Admin" principal="user" />
+      <grant role="bugtracker.Editor" principal="user" />
+      <grant role="bugtracker.User" principal="user" />
+
+      <grant role="bugtracker.User" principal="anybody" />
+
+
+Usage
+=====
+
+  1. To see a Bug Tracker in action, go into the management interface and add
+     a "Bug Tracker" object named 'tracker'. Leave the preselected option and
+     enter a title.
+
+  2. To get to the end user interface, enter::
+
+      http://localhost:8080/++skin++tracker/tracker

Added: Zope3/trunk/src/bugtracker/LICENSE.txt
===================================================================
--- Zope3/trunk/src/bugtracker/LICENSE.txt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/LICENSE.txt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,54 @@
+Zope Public License (ZPL) Version 2.1
+-------------------------------------
+
+A copyright notice accompanies this license document that
+identifies the copyright holders.
+
+This license has been certified as open source. It has also
+been designated as GPL compatible by the Free Software
+Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the
+following conditions are met:
+
+1. Redistributions in source code must retain the
+   accompanying copyright notice, this list of conditions,
+   and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the accompanying
+   copyright notice, this list of conditions, and the
+   following disclaimer in the documentation and/or other
+   materials provided with the distribution.
+
+3. Names of the copyright holders must not be used to
+   endorse or promote products derived from this software
+   without prior written permission from the copyright
+   holders.
+
+4. The right to distribute this software or to use it for
+   any purpose does not give you the right to use
+   Servicemarks (sm) or Trademarks (tm) of the copyright
+   holders. Use of them is covered by separate agreement
+   with the copyright holders.
+
+5. If any files are modified, you must cause the modified
+   files to carry prominent notices stating that you changed
+   the files and the date of any change.
+
+Disclaimer
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS''
+  AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+  NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN
+  NO EVENT SHALL THE COPYRIGHT HOLDERS BE
+  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+  DAMAGE.

Added: Zope3/trunk/src/bugtracker/README.txt
===================================================================
--- Zope3/trunk/src/bugtracker/README.txt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/README.txt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,69 @@
+Bug Tracker Product for Zope 3
+==============================
+
+  This product is an implementation of a bug tracker in Zope 3. 
+
+  Features
+  --------
+
+    Bug Tracker
+
+      - View list of bugs
+
+        o Filtering by status, type, release, priority and text
+
+        o Batching, when list of bugs becomes too long.
+
+        o Bug status and priority values are marked up based on value.
+
+      - Settings
+
+        o When creating a Bug Tracker, one can select the option to
+          automatically create a set of status, type, release and priority
+          choices.
+
+        o The choices for the status, type, release and priority are flexible
+          and can be changed.
+
+      - Mail Subscriptions
+
+        o These are Bug Tracker wide mail subscriptions that send the
+          recipients an E-mail about additions, changes and deletions of bugs.
+
+    Bug
+
+      - Overview
+
+        o This screen provides a comprehensive overview of all the available
+          information about the bug.
+
+        o The status and priority are marked up based on their value.
+
+        o The description and the comments are rendered using STX.
+
+        o Upload Files and Images
+
+        o Add new comments
+
+      - Edit
+
+        o To provide a familiar interface, the edit form is layed out in the
+          same way as the overview
+
+      - Dependencies
+
+        o In my opinion, one major improvement over the current collector is
+          the availability of a dependency feature, where I can say that this
+          bug depends on that one.
+
+        o Based on this information a dependency tree is generated using the 
+          markup rules for status and priority, so that a user can quickly 
+          recognize critical spots in the tree.
+
+        o There is also a Statistics section that tells you how many bugs are
+          completed, have not been viewed and are being fixed. 
+
+      - Mail Subscriptions
+
+        o These are specific bug mail subscriptions that send the recipients
+          an E-mail about additions, changes and deletions of the bug.

Added: Zope3/trunk/src/bugtracker/TODO.txt
===================================================================
--- Zope3/trunk/src/bugtracker/TODO.txt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/TODO.txt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,4 @@
+To Do
+=====
+
+   See http://www.zope3.org:8080/++skin++tracker/tracker/49

Added: Zope3/trunk/src/bugtracker/VERSION.txt
===================================================================
--- Zope3/trunk/src/bugtracker/VERSION.txt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/VERSION.txt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1 @@
+0.4
\ No newline at end of file

Added: Zope3/trunk/src/bugtracker/__init__.py
===================================================================
--- Zope3/trunk/src/bugtracker/__init__.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/__init__.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,19 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Package initialization
+
+$Id: __init__.py,v 1.2 2003/08/12 18:55:07 srichter Exp $
+"""
+from zope.i18n import MessageIDFactory
+TrackerMessageID = MessageIDFactory('bugtracker')

Added: Zope3/trunk/src/bugtracker/batching/__init__.py
===================================================================
--- Zope3/trunk/src/bugtracker/batching/__init__.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/batching/__init__.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,76 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Batching Support
+
+$Id$
+"""
+from zope.interface import implements
+from interfaces import IBatch
+
+class Batch(object):
+    implements(IBatch)
+
+    def __init__(self, list, start=0, size=20):
+        self.list = list
+        self.start = start
+        if len(list) == 0:
+            self.start = -1
+        elif start >= len(list):
+            raise IndexError, 'start index key out of range'
+        self.size = size
+        self.trueSize = size
+        if start+size >= len(list):
+            self.trueSize = len(list)-start
+        self.end = start+self.trueSize-1
+
+    def __len__(self):
+        return self.trueSize
+
+    def __getitem__(self, key):
+        if key >= self.trueSize:
+            raise IndexError, 'batch index out of range'
+        return self.list[self.start+key]
+
+    def __iter__(self): 
+        return iter(self.list[self.start:self.end+1])
+
+    def __contains__(self, item):
+        return item in self.__iter__()
+
+    def nextBatch(self):
+        start = self.start + self.size
+        if start >= len(self.list):
+            return None
+        return Batch(self.list, start, self.size)
+    
+    def prevBatch(self):
+        start = self.start - self.size
+        if start < 0:
+            return None
+        return Batch(self.list, start, self.size)
+
+    def first(self):
+        return self.list[self.start]
+
+    def last(self):
+        return self.list[self.end]
+
+    def total(self):
+        return len(self.list)
+
+    def startNumber(self):
+        return self.start+1
+
+    def endNumber(self):
+        return self.end+1

Added: Zope3/trunk/src/bugtracker/batching/interfaces.py
===================================================================
--- Zope3/trunk/src/bugtracker/batching/interfaces.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/batching/interfaces.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,62 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Batching Support
+
+$Id$
+"""
+from zope.interface.common.mapping import IItemMapping
+
+class IBatch(IItemMapping):
+    """A Batch represents a sub-list of the full enumeration.
+
+    The Batch constructor takes a list (or any list-like object) of elements,
+    a starting index and the size of the batch. From this information all
+    other values are calculated.
+    """
+
+    def __len__():
+        """Return the length of the batch. This might be different than the
+        passed in batch size, since we could be at the end of the list and
+        there are not enough elements left to fill the batch completely."""
+
+    def __iter__(): 
+        """Creates an iterator for the contents of the batch (not the entire
+        list)."""
+
+    def __contains__(key):
+        """Checks whether the key (in our case an index) exists."""
+
+    def nextBatch(self):
+        """Return the next batch. If there is no next batch, return None."""
+    
+    def prevBatch(self):
+        """Return the previous batch. If there is no previous batch, return
+        None."""
+
+    def first(self):
+        """Return the first element of the batch."""
+
+    def last(self):
+        """Return the last element of the batch."""
+
+    def total(self):
+        """Return the length of the list (not the batch)."""
+
+    def startNumber(self):
+        """Give the start **number** of the batch, which is 1 more than the
+        start index passed in."""
+
+    def endNumber(self):
+        """Give the end **number** of the batch, which is 1 more than the
+        final index."""

Added: Zope3/trunk/src/bugtracker/batching/tests.py
===================================================================
--- Zope3/trunk/src/bugtracker/batching/tests.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/batching/tests.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,125 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bug Tracker Mail Subscription and Mailer Tests
+
+$Id: tests.py,v 1.3 2004/04/29 15:43:47 fdrake dead $
+"""
+import unittest
+
+from bugtracker.batching import Batch
+from bugtracker.batching.interfaces import IBatch
+
+class BatchTest(unittest.TestCase):
+
+    def getData(self):
+        return ['one', 'two', 'three', 'four', 'five', 'six',
+                'seven', 'eight', 'nine', 'ten']
+
+    def test_Interface(self):
+        self.failUnless(IBatch.providedBy(Batch([], 0, 0)))
+
+    def test_constructor(self):
+        batch = Batch(self.getData(), 9, 3)
+        self.assertRaises(IndexError, Batch, self.getData(), start=10, size=3)
+
+    def test__len__(self):
+        batch = Batch(self.getData(), 0, 3)
+        self.assertEqual(len(batch), 3)
+        batch = Batch(self.getData(), 9, 3)
+        self.assertEqual(len(batch), 1)
+
+    def test__getitem__(self):
+        batch = Batch(self.getData(), 0, 3)
+        self.assertEqual(batch[0], 'one')
+        self.assertEqual(batch[1], 'two')
+        self.assertEqual(batch[2], 'three')
+        batch = Batch(self.getData(), 3, 3)
+        self.assertEqual(batch[0], 'four')
+        self.assertEqual(batch[1], 'five')
+        self.assertEqual(batch[2], 'six')
+        batch = Batch(self.getData(), 9, 3)
+        self.assertRaises(IndexError, batch.__getitem__, 3)
+        
+    def test__iter__(self):
+        batch = Batch(self.getData(), 0, 3)
+        self.assertEqual(list(iter(batch)), ['one', 'two', 'three'])
+        batch = Batch(self.getData(), 9, 3)
+        self.assertEqual(list(iter(batch)), ['ten'])
+
+    def test__contains__(self):
+        batch = Batch(self.getData(), 0, 3)
+        self.assert_(batch.__contains__('one'))
+        self.assert_(batch.__contains__('two'))
+        self.assert_(batch.__contains__('three'))
+        self.assert_(not batch.__contains__('four'))
+        batch = Batch(self.getData(), 6, 3)
+        self.assert_(not batch.__contains__('one'))
+        self.assert_(batch.__contains__('seven'))
+        self.assert_(not batch.__contains__('ten'))
+
+    def test_nextBatch(self):
+        next = Batch(self.getData(), 0, 3).nextBatch()
+        self.assertEqual(list(iter(next)), ['four', 'five', 'six'])
+        nextnext = next.nextBatch()
+        self.assertEqual(list(iter(nextnext)), ['seven', 'eight', 'nine'])
+        next = Batch(self.getData(), 9, 3).nextBatch()
+        self.assertEqual(next, None)
+
+    def test_prevBatch(self):
+        prev = Batch(self.getData(), 9, 3).prevBatch()
+        self.assertEqual(list(iter(prev)), ['seven', 'eight', 'nine'])
+        prevprev = prev.prevBatch()
+        self.assertEqual(list(iter(prevprev)), ['four', 'five', 'six'])
+        prev = Batch(self.getData(), 0, 3).prevBatch()
+        self.assertEqual(prev, None)
+
+    def test_batchRoundTrip(self):
+        batch = Batch(self.getData(), 0, 3).nextBatch()
+        self.assertEqual(list(iter(batch.nextBatch().prevBatch())),
+                         list(iter(batch)))
+
+    def test_first_last(self):
+        batch = Batch(self.getData(), 0, 3)
+        self.assertEqual(batch.first(), 'one')
+        self.assertEqual(batch.last(), 'three')
+        batch = Batch(self.getData(), 9, 3)
+        self.assertEqual(batch.first(), 'ten')
+        self.assertEqual(batch.last(), 'ten')
+        
+    def test_total(self):
+        batch = Batch(self.getData(), 0, 3)
+        self.assertEqual(batch.total(), 10)
+        batch = Batch(self.getData(), 6, 3)
+        self.assertEqual(batch.total(), 10)
+    
+    def test_startNumber(self):
+        batch = Batch(self.getData(), 0, 3)
+        self.assertEqual(batch.startNumber(), 1)
+        batch = Batch(self.getData(), 9, 3)
+        self.assertEqual(batch.startNumber(), 10)
+
+    def test_endNumber(self):
+        batch = Batch(self.getData(), 0, 3)
+        self.assertEqual(batch.endNumber(), 3)
+        batch = Batch(self.getData(), 9, 3)
+        self.assertEqual(batch.endNumber(), 10)
+        
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(BatchTest),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()

Added: Zope3/trunk/src/bugtracker/browser/__init__.py
===================================================================

Added: Zope3/trunk/src/bugtracker/browser/branchentry.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/branchentry.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/branchentry.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,9 @@
+<li>
+  <a class="" href="" 
+      tal:attributes="href string:../${view/name};
+                      class string:node ${context/status} ${context/priority}">
+    <span tal:replace="string:${context/title} (${view/name})">
+      Bug Title (bugid)
+    </span>
+  </a>
+</li>
\ No newline at end of file

Added: Zope3/trunk/src/bugtracker/browser/bug.py
===================================================================
--- Zope3/trunk/src/bugtracker/browser/bug.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/bug.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,314 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Browser View Components for Bugs
+
+$Id: bug.py,v 1.14 2004/03/18 18:04:54 philikon Exp $
+"""
+import docutils.core
+import re
+
+from zope.component.interfaces import IViewFactory
+from zope.exceptions import Unauthorized, ForbiddenAttribute, NotFoundError
+from zope.interface import implements
+from zope.proxy import removeAllProxies
+from zope.schema.vocabulary import getVocabularyRegistry
+from zope.security.checker import getChecker, ProxyFactory
+from zope.structuredtext.html import HTML
+
+from zope.app import zapi
+from zope.app.container.contained import contained
+from zope.app.dublincore.interfaces import IZopeDublinCore
+from zope.app.form import CustomWidgetFactory
+from zope.app.form.browser import TextWidget, TextAreaWidget, DropdownWidget
+from zope.app.size.interfaces import ISized
+from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
+
+from bugtracker.interfaces import IComment
+from bugtracker.interfaces import IBugDependencies
+from bugtracker.browser.comment import CommentViewBase
+
+
+class BugBaseView(object):
+    """Get's all the fancy expressions for the attribute values."""
+
+    def created(self):
+        dc = IZopeDublinCore(self.context)
+        formatter = self.request.locale.dates.getFormatter('dateTime', 'short')
+        return formatter.format(dc.created)
+
+    def modified(self):
+        dc = IZopeDublinCore(self.context)
+        formatter = self.request.locale.dates.getFormatter('dateTime', 'short')
+        if dc.modified is None:
+            return self.created()
+        return formatter.format(dc.modified)
+
+    def submitter(self):
+        registry = getVocabularyRegistry()
+        users = registry.get(self.context, 'Users')
+        id = self.context.submitter
+        try:
+            return users.getTerm(id).principal
+        except NotFoundError:
+            # There is no principal for this id, so let's just fake one.
+            return {'id': id, 'login': id, 'title': id, 'description': id}
+
+    def description(self):
+        ttype = getattr(self.context.description, 'ttype', None)
+        if ttype is not None:
+            source = zapi.createObject(None, self.context.description.ttype,
+                                       self.context.description)
+            view = zapi.getView(removeAllProxies(source), '', self.request)
+            html = view.render()
+        else:
+            html = self.context.description
+        return html
+
+    def status(self):
+        registry = getVocabularyRegistry()
+        types = registry.get(zapi.getParent(self.context), 'Stati')
+        return types.getTerm(self.context.status)
+
+    def type(self):        
+        registry = getVocabularyRegistry()
+        types = registry.get(zapi.getParent(self.context), 'BugTypes')
+        return types.getTerm(self.context.type)
+
+    def release(self):
+        if self.context.release is None:
+            return u'not specified'
+        registry = getVocabularyRegistry()
+        types = registry.get(zapi.getParent(self.context), 'Releases')
+        return types.getTerm(self.context.release)
+
+    def priority(self):
+        registry = getVocabularyRegistry()
+        types = registry.get(zapi.getParent(self.context), 'Priorities')
+        return types.getTerm(self.context.priority)
+
+    def owners(self):
+        registry = getVocabularyRegistry()
+        vocab = registry.get(self.context, 'Users')
+        userTerms = []
+        for id in self.context.owners:
+            try:
+                userTerms.append(vocab.getTerm(id).principal)
+            except NotFoundError:
+                # There is no principal for this id, so let's just fake one.
+                userTerms.append(
+                    {'id': id, 'login': id, 'title': id, 'description': id})
+        return userTerms
+
+
+# Make a custom widget for the vocabulary, so that default values are
+# retrieved from the vocabulary and not the field.
+class ManagableVocabularyWidget(DropdownWidget):
+
+    def _getDefault(self):
+        # Return the default value for this widget;
+        # may be overridden by subclasses.
+        return self.vocabulary.default.value
+    
+
+class AddBug(object):
+
+    def nextURL(self):
+        return '../'+self.context.contentName
+
+
+class AddDependentBug(object):
+    """Add a bug that is """
+
+    def __init__(self, context, request):
+        super(AddDependentBug, self).__init__(context, request)
+        self.dependent = context.context
+        context.context = zapi.getParent(self.dependent)
+        self.label = "Add Bug (Dependent: %s)" %self.dependent.title
+
+    def add(self, content):
+        content = super(AddDependentBug, self).add(content)
+        deps = IBugDependencies(self.dependent)
+        deps.dependencies += (self.context.contentName,)
+        return content
+
+    def nextURL(self):
+        return '../overview.html'
+
+
+class EditBug(BugBaseView):
+
+    def update(self):
+        status = super(EditBug, self).update()
+        if status.startswith('Updated'):
+            return self.request.response.redirect('./@@overview.html')
+        return status
+
+
+class Overview(BugBaseView):
+    """View class providing necessary methods for the bug overview."""
+
+    def comments(self):
+        """Get a list of all comments."""
+        comments = []
+        for name, obj in self.context.items():
+            if IComment.providedBy(obj):
+                comments.append(CommentViewBase(obj, self.request))
+        return comments
+
+    def attachments(self):
+        """Get a list of all attachments."""
+        attchs = []
+        for name, obj in self.context.items():
+            if not IComment.providedBy(obj):
+                size = ISized(obj)
+                attchs.append({'name': name, 'size': size.sizeForDisplay()})
+        return attchs
+        
+    def dependencies(self):
+        deps = IBugDependencies(self.context)
+        return deps.dependencies
+
+
+class Dependencies(object):
+
+    def __init__(self, context, request):
+        super(Dependencies, self).__init__(context, request)
+        # For efficiency get these values once and be done.
+        deps = IBugDependencies(self.context)
+        # If the two conditions are not fulfilled, we are not going to need
+        # the values.
+        if self.canChangeDependencies() and self.getShowDepsOptions():
+            self.dependencies = deps.dependencies
+            self.dependents = deps.dependents    
+
+    def dependencyValues(self):
+        deps = IBugDependencies(self.context)
+        dep_type = self.request.get('dep_type', 'dependencies')
+        if dep_type == 'dependencies':
+            return self.dependencies
+        else:
+            return self.dependents            
+
+    def availableBugs(self):
+        tracker = zapi.getParent(self.context)
+        bugs = []
+        for name, bug in tracker.items():
+            # Make sure we do not list the bug itself
+            if name != zapi.name(self.context):
+                bugs.append({'name': name, 'title': bug.title})
+        return bugs
+
+    def setDependencyValues(self):
+        dep_type = self.request.get('dep_type', 'dependencies')
+        deps = IBugDependencies(self.context)
+
+        if 'ADD' in self.request and 'add_deps' in self.request:
+            if dep_type == 'dependencies':
+                deps.addDependencies(tuple(self.request['add_deps']))
+            else:
+                deps.addDependents(tuple(self.request['add_deps']))
+
+        if 'DELETE' in self.request and 'del_deps' in self.request:
+            if dep_type == 'dependencies':
+                deps.deleteDependencies(tuple(self.request['del_deps']))
+            else:
+                deps.deleteDependents(tuple(self.request['del_deps']))
+
+        self.setShowDepsOptions()
+        self.setDepType()
+
+        return self.request.response.redirect(
+            './@@dependencies.html?dep_type=' + dep_type)
+
+    def _branchHTML(self, children):
+        html = '<ul>\n'
+        for child, subs in children:
+            html += DependencyEntry(child, self.request)()
+            if subs:
+                html += self._branchHTML(subs)
+        html += '</ul>\n'
+        return html
+
+    def branch(self):
+        deps = IBugDependencies(self.context)
+        children = deps.findChildren()
+        return self._branchHTML(children)
+
+    def _getAllSubs(self, children):
+        all = map(lambda c: c[0], children)
+        for child, subs in children:
+            all += self._getAllSubs(subs)
+        return all
+
+    def getStatistics(self):
+        deps = IBugDependencies(self.context)
+        children = deps.findChildren()
+        all = self._getAllSubs(children)
+        all_num = len(all)
+        if not all_num:
+            return {}
+        closed = len(filter(lambda b: b.status in ('closed', 'deferred'), all))
+        new = len(filter(lambda b: b.status == 'new', all))
+        open = len(filter(lambda b: b.status in ('open', 'assigned'), all))
+        stats = {'total': all_num,
+                 'closed': closed,
+                 'closed_perc': '%.2f%%' %(closed*100.0/all_num),
+                 'new': new,
+                 'new_perc': '%.2f%%' %(new*100.0/all_num),
+                 'open': open,
+                 'open_perc': '%.2f%%' %(open*100.0/all_num),
+                 }
+        return stats
+
+    def getDepType(self):
+        return self.request.cookies.get('dep_type', 'dependencies')
+
+    def setDepType(self):
+        value = self.request.get('dep_type', 'dependencies')
+        self.request.response.setCookie('dep_type', value)
+
+    def getShowDepsOptions(self):
+        return int(self.request.cookies.get('show_deps_options', '1'))
+
+    def setShowDepsOptions(self):
+        if 'COLLAPSE' in self.request:
+            value = '0'
+        else:
+            value = '1'
+        self.request.response.setCookie('show_deps_options', value)
+
+    def canChangeDependencies(self):
+        deps = IBugDependencies(self.context)
+        deps = contained(deps, self.context, name='Dependencies')
+        proxy = ProxyFactory(deps)
+        checker = getChecker(proxy)
+        try:
+            checker.check_setattr(deps, 'dependencies')
+        except (Unauthorized, ForbiddenAttribute):
+            return False
+        return True
+    
+    legend = ViewPageTemplateFile('legend.pt')
+
+
+class DependencyEntry(object):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def name(self):
+        return zapi.name(self.context)
+
+    __call__ = ViewPageTemplateFile('branchentry.pt')

Added: Zope3/trunk/src/bugtracker/browser/bug_add.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/bug_add.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/bug_add.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,79 @@
+<html metal:use-macro="context/@@standard_macros/dialog">
+<body>
+<div metal:fill-slot="body">
+
+  <form action="." tal:attributes="action request/URL" method="post"
+        enctype="multipart/form-data">
+
+    <h3 tal:condition="view/label"
+        tal:content="view/label"
+        >Edit something</h3>
+  
+    <p tal:define="status view/update"
+       tal:condition="status"
+       tal:content="status" />
+  
+    <p tal:condition="view/errors">
+      <span i18n:translate="">There are
+      <strong tal:content="python:len(view.errors)" 
+              i18n:name="num_errors">6</strong>
+      input errors.</span>
+    </p>
+  
+    <div>
+
+      <div class="row">
+        <div class="label" i18n:translate="">Title:</div>
+        <div class="field" tal:content="structure view/title_widget"></div>
+      </div>
+
+      <div class="row">
+        <div class="label md_label" i18n:translate="">Type:</div>
+        <div class="field md_field" 
+             tal:content="structure view/type_widget" />
+        <div class="label md_label" i18n:translate="">Status:</div>
+        <div class="field md_field" 
+             tal:content="structure view/status_widget" />
+      </div>
+
+      <div class="row">
+        <div class="label md_label" i18n:translate="">Priority:</div>
+        <div class="field md_field"
+            tal:content="structure view/priority_widget" />
+        <div class="label md_label" i18n:translate="">Release Target:</div>
+        <div class="field md_field"
+            tal:content="structure view/release_widget" />
+      </div>
+
+      <div class="row">
+        <div class="label" i18n:translate="">Owners:</div>
+        <div class="field"
+            tal:content="structure view/owners_widget" />
+      </div>
+
+    </div>
+
+    <h4 i18n:translate="">Description</h4>
+    <div tal:content="structure view/description_widget">
+
+      <div class="row" tal:replace="nothing">
+        <div class="label">Extra bottom</div>
+        <div class="field"><input type="text" style="width:100%" /></div>
+      </div>
+
+    </div>
+
+    <div class="row">
+      <div class="controls">
+        <input type="submit" value="Refresh" 
+            i18n:attributes="value refresh-button" />
+        <input type="submit" name="UPDATE_SUBMIT" value="Submit" 
+            i18n:attributes="value submit-button"/>
+      </div>
+    </div>
+
+  </form>
+
+</div>
+</body>
+</html>

Added: Zope3/trunk/src/bugtracker/browser/bug_edit.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/bug_edit.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/bug_edit.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,85 @@
+<html metal:use-macro="views/standard_macros/view">
+<head>
+  <style metal:fill-slot="style_slot"
+      type="text/css" media="all"
+      tal:content=
+          "string: @import url(${context/++resource++tracker.css});">
+    @import url(tracker.css);>
+  </style>
+</head>
+
+<body>
+<div metal:fill-slot="body">
+
+  <form action="." tal:attributes="action request/URL" method="POST"
+        enctype="multipart/form-data">
+
+    <p tal:define="status view/update"
+       tal:condition="status"
+       tal:content="status" />
+    
+    <p tal:condition="view/errors">
+      <span i18n:translate="">There are
+      <strong tal:content="python:len(view.errors)" 
+              i18n:name="num_errors">6</strong>
+      input errors.</span>
+    </p>
+
+    <h1 tal:content="structure view/title_widget">
+      <input type="text" name="title" value="title" />
+    </h1>
+    <span i18n:translate="" i18n:domain="bugtracker">From 
+      <b tal:content="view/submitter/title" i18n:name="user">user</b> at
+      <b tal:content="view/created" i18n:name="date">2001/01/01 12:00</b>
+    </span>
+
+    <div>
+
+      <div class="row" i18n:domain="bugtracker">
+        <div class="label md_label" i18n:translate="">Type:</div>
+        <div class="field md_field" 
+            tal:content="structure view/type_widget" />
+        <div class="label md_label" i18n:translate="">Status:</div>
+        <div class="field md_field"
+            tal:content="structure view/status_widget" />
+      </div>
+
+      <div class="row" i18n:domain="bugtracker">
+        <div class="label md_label" i18n:translate="">Priority:</div>
+        <div class="field md_field"
+            tal:content="structure view/priority_widget" />
+        <div class="label md_label" i18n:translate="">Release Target:</div>
+        <div class="field md_field"
+            tal:content="structure view/release_widget" />
+      </div>
+
+      <div class="row" i18n:domain="bugtracker">
+        <div class="label" i18n:translate="">Owners:</div>
+        <div class="field"
+            tal:content="structure view/owners_widget" />
+      </div>
+
+      <div class="row">
+        <div class="label" i18n:translate="">Last modified:</div>
+        <div class="field" tal:content="view/modified"></div>
+      </div>
+
+    </div>
+
+    <h4 i18n:translate="">Description</h4>
+    <div tal:content="structure view/description_widget" />
+
+    <div class="row">
+      <div class="controls">
+        <input type="submit" value="Refresh" 
+            i18n:attributes="value refresh-button" />
+        <input type="submit" name="UPDATE_SUBMIT" value="Submit" 
+            i18n:attributes="value submit-button"/>
+      </div>
+    </div>
+    
+    </form>
+    
+  </div>
+</body>
+</html>

Added: Zope3/trunk/src/bugtracker/browser/bug_icon.png
===================================================================
(Binary files differ)


Property changes on: Zope3/trunk/src/bugtracker/browser/bug_icon.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: Zope3/trunk/src/bugtracker/browser/bug_listing_compressed.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/bug_listing_compressed.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/bug_listing_compressed.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,33 @@
+<table class="listing" width="100%" 
+       style="font-size:80%;">
+  <tr>
+    <th width="60%">Title (ID)</th>
+    <th width="10%">Submitter</th>
+    <th width="10%">Date/Time</th>
+    <th width="20%">Owners</th>
+  </tr>
+  <tal:block repeat="bug view/getBugs" i18n:domain="bugtracker">
+    <tr class=""
+        tal:define="oddrow repeat/bug/odd"
+        tal:attributes="class python:oddrow and 'even' or 'odd'">
+
+      <td>
+        <a class="" href="" 
+            tal:attributes="
+              href string:./${bug/name};
+              class string:node ${bug/context/status} ${bug/context/priority}">
+          <span tal:replace="bug/context/title" />
+          (<span tal:replace="bug/name" />)
+        </a>
+      </td>
+      <td tal:content="bug/submitter/login" />
+      <td tal:content="bug/shortCreated" />
+      <td>
+        <tal:block repeat="owner bug/owners">
+          <d tal:content="owner/login" tal:omit-tag="" />
+          <d tal:condition="not:repeat/owner/end" tal:omit-tag="">,</d>
+        </tal:block>
+      </td>  
+    </tr>
+  </tal:block>
+</table>
\ No newline at end of file

Added: Zope3/trunk/src/bugtracker/browser/bug_listing_normal.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/bug_listing_normal.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/bug_listing_normal.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,42 @@
+  <tal:block repeat="bug view/getBugs" i18n:domain="bugtracker">
+    <div class=""
+        tal:define="oddrow repeat/bug/odd"
+        tal:attributes="class python:oddrow and 'even' or 'odd'">
+      <h5 class="summary_title" >
+        <a href="" 
+            tal:attributes="href string:./${bug/name}/@@overview.html"
+            i18n:translate="">
+          Bug #<d tal:replace="bug/name" i18n:name="bug_id">1</d> - 
+          <d tal:replace="bug/context/title" i18n:name="bug_title">
+            Bug Title
+          </d> 
+        </a>
+      </h5>
+
+      <div class="summary_content">
+        <div class="summary_condition">
+          <span i18n:translate="">Status:</span> 
+            <span tal:attributes="class bug/context/status" 
+                  tal:content="bug/status/title">New</span> - 
+          <span i18n:translate="">Priority:</span> 
+            <span tal:attributes="class bug/context/priority"
+                  tal:content="bug/priority/title">Normal</span> - 
+          <span i18n:translate="">Type: </span>
+            <b tal:replace="bug/type/title">Bug</b>
+        </div>
+
+        <div class="summary_body" tal:content="bug/descriptionPreview">
+          Message Description Sneak Preview goes here...
+        </div>
+
+        <div class="summary_metadata" i18n:translate="">
+          Posted by <b tal:content="bug/submitter/title" 
+                       i18n:name="submitter">Submitter</b> 
+          on <b tal:replace="bug/created" 
+                i18n:name="created_date">2003/01/01</b>
+          - <b tal:replace="bug/numberOfComments" 
+               i18n:name="num_comments">3</b> comments
+        </div>
+      </div>
+    </div>
+  </tal:block>

Added: Zope3/trunk/src/bugtracker/browser/bug_overview.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/bug_overview.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/bug_overview.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,109 @@
+<html metal:use-macro="views/standard_macros/view">
+<head>
+  <style metal:fill-slot="style_slot"
+      type="text/css" media="all"
+      tal:content=
+          "string: @import url(${context/++resource++tracker.css});">
+    @import url(tracker.css);>
+  </style>
+</head>
+
+<body>
+<div metal:fill-slot="body">
+
+  <h1 tal:content="context/title">Bug Number 1</h1>
+  <span i18n:translate="" i18n:domain="bugtracker">From 
+    <b tal:content="view/submitter/title" i18n:name="user">user</b> at
+    <b tal:content="view/created" i18n:name="date">2001/01/01 12:00</b>
+  </span>
+
+  <div>
+
+    <div class="row" i18n:domain="bugtracker">
+      <div class="label md_label">Type:</div>
+      <div class="field md_field" tal:content="view/type/title"></div>
+      <div class="label md_label">Status:</div>
+      <div class="field md_field">
+        <span tal:attributes="class context/status" 
+            tal:content="view/status/title" />
+      </div>
+    </div>
+
+    <div class="row" i18n:domain="bugtracker">
+      <div class="label md_label" i18n:translate="">Priority:</div>
+      <div class="field md_field">
+        <span tal:attributes="class context/priority" 
+            tal:content="view/priority/title" />
+      </div>
+      <div class="label md_label" i18n:translate="">Release Target:</div>
+      <div class="field md_field" tal:content="view/release/title"></div>
+    </div>
+
+    <div class="row" i18n:domain="bugtracker">
+      <div class="label" i18n:translate="">Owners:</div>
+      <div class="field">
+        <tal:block repeat="owner view/owners">
+          <d tal:content="owner/title" tal:omit-tag="" />
+          <d tal:condition="not:repeat/owner/end" tal:omit-tag="">,</d>
+        </tal:block>
+      </div>
+    </div>
+
+    <div class="row">
+      <div class="label" i18n:translate="">Last modified:</div>
+      <div class="field" tal:content="view/modified"></div>
+    </div>
+
+  </div>
+
+  <h4 i18n:translate="">Description</h4>
+  <div class="single_p" id="description"
+      tal:content="structure view/description">Bug Description</div>
+  
+  <h4 i18n:translate="" i18n:domain="bugtracker">Direct Dependencies</h4>
+  <div class="single_p">
+    <tal:block repeat="dep view/dependencies" tal:omit-tag="">
+      <a href="" tal:attributes="href string:../$dep"
+                 tal:content="dep">Dep Bug</a>
+      <d tal:condition="not:repeat/dep/end" tal:replace="string:," />
+  
+    </tal:block>
+  </div>
+  <div class="action" i18n:domain="bugtracker">
+    <a href="./@@+/action.html?type_name=AddDependentBug.html" 
+      i18n:translate="">Add Bug</a>
+  </div>
+  
+  <h4 i18n:translate="" i18n:domain="bugtracker">Attachments</h4>
+  <ul id="attachments" tal:condition="view/attachments">
+    <li tal:repeat="attch view/attachments">
+        <a href="" tal:attributes="href string: ./${attch/name}" 
+                   tal:content="attch/name">Attachment 1</a>
+        (<div tal:replace="attch/size">Size here</div>)
+    </li>
+  </ul>
+  <div class="action" i18n:domain="bugtracker">
+    <a href="./@@+/action.html?type_name=zope.app.file.File" 
+      i18n:translate="">Add File</a> |
+    <a href="./@@+/action.html?type_name=zope.app.file.Image" 
+      i18n:translate="">Add Image</a>
+  </div>
+
+  <h4 i18n:translate="" i18n:domain="bugtracker">Comments</h4>
+  <div tal:repeat="comment view/comments" i18n:domain="">
+      <h5 style="margin: 0em 0.6em" i18n:translate="">
+        Entry #<d tal:replace="repeat/comment/number" i18n:name="bug_id"/> by 
+        <d tal:replace="comment/creator/title" i18n:name="creator"/> on 
+        <d tal:replace="comment/modified" i18n:name="modified_datetime"/>
+      </h5>
+    <div class="comment"
+       tal:content="structure comment/body">Comment body</div>
+  </div>
+  <div class="action" i18n:domain="bugtracker">
+    <a href="./@@+/action.html?type_name=AddBugComment.html" 
+      i18n:translate="">Add Comment</a>
+  </div>
+
+</div>
+</body>
+</html>

Added: Zope3/trunk/src/bugtracker/browser/comment.py
===================================================================
--- Zope3/trunk/src/bugtracker/browser/comment.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/comment.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,62 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Structured Text Renderer Classes
+
+$Id: comment.py,v 1.2 2003/08/12 22:50:57 srichter Exp $
+"""
+import re
+
+from zope.proxy import removeAllProxies
+from zope.schema.vocabulary import getVocabularyRegistry
+from zope.structuredtext.document import Document
+from zope.structuredtext.html import HTML
+
+from zope.app import zapi
+from zope.app.dublincore.interfaces import IZopeDublinCore
+
+
+class CommentViewBase(object):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def creator(self):
+        dc = IZopeDublinCore(self.context)
+        registry = getVocabularyRegistry()
+        users = registry.get(self.context, 'Users')
+        id = dc.creators[0]
+        try:
+            return users.getTerm(id).principal
+        except NotFoundError:
+            # There is no principal for this id, so let's just fake one.
+            return {'id': id, 'login': id, 'title': id, 'description': id}
+
+    def modified(self):
+        dc = IZopeDublinCore(self.context)
+        formatter = self.request.locale.dates.getFormatter('dateTime', 'short')
+        if dc.modified is None:
+            return formatter.format(dc.created)
+        return formatter.format(dc.modified)
+
+    def body(self):
+        ttype = getattr(self.context.body, 'ttype', None)
+        if ttype is not None:
+            source = zapi.createObject(None, self.context.body.ttype,
+                                       self.context.body)
+            view = zapi.getView(removeAllProxies(source), '', self.request)
+            html = view.render()
+        else:
+            html = self.context.body
+        return html

Added: Zope3/trunk/src/bugtracker/browser/configure.zcml
===================================================================
--- Zope3/trunk/src/bugtracker/browser/configure.zcml	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/configure.zcml	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,281 @@
+<configure
+   xmlns:zope="http://namespaces.zope.org/zope"
+   xmlns:help="http://namespaces.zope.org/help"
+   xmlns="http://namespaces.zope.org/browser">
+
+  <zope:include package=".skin" />
+
+  <resource 
+       name="tracker.css" file="tracker.css" layer="rotterdam" />
+
+  <icon
+      name="zmi_icon"
+      for="bugtracker.interfaces.IBugTracker"
+      file="tracker_icon.png" />
+
+  <!-- Register a special widgets for choices that have managable
+       vocabularies. -->
+  <zope:view
+      type="zope.publisher.interfaces.browser.IBrowserRequest"
+      for="zope.schema.interfaces.IChoice
+           bugtracker.interfaces.IManagableVocabulary"
+      provides="zope.app.form.interfaces.IInputWidget"
+      factory=".bug.ManagableVocabularyWidget"
+      permission="zope.Public"
+      />
+
+  <!-- Bug tracker configuration -->
+
+  <addform
+      label="Add Bug Tracker"
+      name="AddBugTracker.html"
+      schema="bugtracker.interfaces.IBugTracker"
+      content_factory="bugtracker.tracker.BugTracker"
+      permission="zope.ManageContent"
+      class=".tracker.AddBugTracker"
+      template="tracker_add.pt" 
+      />
+
+  <addMenuItem
+      class="bugtracker.tracker.BugTracker"
+      title="Bug Tracker"
+      description="A Bug Tracker"
+      permission="zope.ManageContent"
+      view="AddBugTracker.html" 
+      />
+
+  <containerViews
+      for="bugtracker.interfaces.IBugTracker"
+      index="bugtracker.ViewBugTracker"
+      contents="zope.ManageContent"
+      add="bugtracker.AddBug"
+      />
+
+  <pages
+      for="bugtracker.interfaces.IBugTracker"
+      class=".tracker.Overview"
+      permission="bugtracker.ViewBugTracker">
+    <page name="overview.html" template="tracker_overview.pt"
+        menu="zmi_views" title="Overview" />
+    <page name="updateOverviewSettings.html" 
+        attribute="updateSettings" />
+  </pages>
+
+  <help:register
+      id="tracker_overview"
+      title="Bug Tracker - Overview"
+      parent="ui"
+      doc_path="./help/tracker_overview.rst"
+      for="bugtracker.interfaces.IBugTracker"
+      view="overview.html"
+      />
+
+  <pages
+      for="bugtracker.interfaces.IBugTracker"
+      class=".tracker.Settings"
+      permission="bugtracker.ManageBugTracker">
+    <page name="settings.html" template="tracker_settings.pt"
+          menu="zmi_views" title="Settings" />
+    <page name="addValue.html" attribute="addValue" />
+    <page name="deleteValues.html" attribute="deleteValues" />
+    <page name="setDefaultValue.html" attribute="setDefaultValue" />
+  </pages>
+
+  <page
+      name="subscriptions.html"
+      for="bugtracker.interfaces.IBugTracker"
+      class=".mail.MailSubscriptions"
+      permission="bugtracker.EditBug"
+      template="subscriptions.pt"
+      menu="zmi_views" title="Subscriptions" />
+
+  <pages
+      for="bugtracker.interfaces.IBugTracker"
+      class=".exportimport.XMLExportImport"
+      permission="bugtracker.ManageBugTracker">
+    <page name="xmlexportimport.html" template="exportimport.pt"
+          menu="zmi_views" title="XML Export/Import" />
+    <page name="tracker.xml" attribute="exportXML" />
+    <page name="import.html" attribute="importXML" />
+  </pages>
+
+  <defaultView
+      name="overview.html"
+      for="bugtracker.interfaces.IBugTracker"/>
+
+
+  <!-- Bug configuration -->
+
+  <addform
+      label="Add Bug"
+      name="AddBug.html"
+      schema="bugtracker.interfaces.IBug"
+      content_factory="bugtracker.bug.Bug"
+      permission="bugtracker.AddBug"
+      fields="title description type owners status priority release"
+      template="bug_add.pt"
+      class=".bug.AddBug">
+
+    <widget
+        field="title"
+        class="zope.app.form.browser.TextWidget" 
+        displayWidth="45"
+        style="width: 100%"/>
+
+    <widget
+        field="description"
+        class=".widgets.RenderableTextAreaWidget" 
+        height="10"
+        style="width: 100%" />
+
+  </addform>
+
+
+  <addMenuItem
+      class="bugtracker.bug.Bug"
+      title="Bug"
+      description="A Bug"
+      permission="zope.ManageContent"
+      view="AddBug.html" 
+      />
+
+  <addform
+      label="Add Dependent Bug"
+      name="AddDependentBug.html"
+      schema="bugtracker.interfaces.IBug"
+      content_factory="bugtracker.bug.Bug"
+      permission="bugtracker.AddBug"
+      fields="title description type owners status priority release"
+      template="bug_add.pt"
+      class=".bug.AddDependentBug">
+
+    <widget
+        field="title"
+        class="zope.app.form.browser.TextWidget" 
+        displayWidth="45"
+        style="width: 100%"/>
+
+    <widget
+        field="description"
+        class=".widgets.RenderableTextAreaWidget" 
+        height="10"
+        style="width: 100%" />
+
+  </addform>
+
+  <containerViews
+      for="bugtracker.interfaces.IBug"
+      index="bugtracker.ViewBug"
+      contents="bugtracker.EditBug"
+      add="zope.View"
+      />
+
+  <editform
+      schema="bugtracker.interfaces.IBug"
+      for="bugtracker.interfaces.IBug"
+      label="Change Bug"
+      name="edit.html"
+      permission="bugtracker.EditBug"
+      fields="title description type owners status priority release"
+      template="bug_edit.pt"
+      class=".bug.EditBug"
+      menu="zmi_views" title="Edit" >
+
+    <widget
+        field="title"
+        class="zope.app.form.browser.TextWidget" 
+        displayWidth="45"
+        style="width: 100%"/>
+
+    <widget
+        field="description"
+        class=".widgets.RenderableTextAreaWidget" 
+        height="10"
+        style="width: 100%" />
+
+  </editform>
+
+
+  <pages
+      for="bugtracker.interfaces.IBug"
+      class=".bug.Overview"
+      permission="bugtracker.ViewBug">
+    <page name="overview.html" template="bug_overview.pt"
+          menu="zmi_views" title="Overview" />
+  </pages>
+
+  <pages
+      for="bugtracker.interfaces.IBug"
+      class=".bug.Dependencies"
+      permission="bugtracker.ViewBug">
+      <page name="dependencies.html" template="dependencies.pt"
+            menu="zmi_views" title="Dependencies" />
+      <page name="setDependencies.html" attribute="setDependencyValues" />
+  </pages>
+
+  <page
+      name="subscriptions.html"
+      for="bugtracker.interfaces.IBug"
+      class=".mail.MailSubscriptions"
+      permission="bugtracker.EditBug"
+      template="subscriptions.pt"
+      menu="zmi_views" title="Subscriptions" />
+
+  <defaultView
+      name="overview.html"
+      for="bugtracker.interfaces.IBug"/>
+
+  <icon
+      name="zmi_icon"
+      for="bugtracker.interfaces.IBug"
+      file="bug_icon.png"/>
+
+
+  <!-- Comment configuration -->
+
+  <addform
+      label="Add Comment"
+      name="AddBugComment.html"
+      schema="bugtracker.interfaces.IComment"
+      content_factory="bugtracker.comment.Comment"
+      permission="bugtracker.AddComment">
+
+    <widget
+        field="body"
+        class=".widgets.RenderableTextAreaWidget" 
+        height="15"
+        style="width: 100%" />
+
+  </addform>
+
+
+  <addMenuItem
+      class="bugtracker.comment.Comment"
+      title="Bug Comment"
+      description="A Comment"
+      permission="bugtracker.AddComment"
+      view="AddBugComment.html" 
+      />
+
+  <editform
+      schema="bugtracker.interfaces.IComment"
+      for="bugtracker.interfaces.IComment"
+      label="Change Comment"
+      name="edit.html"
+      permission="bugtracker.ManageBugTracker"
+      menu="zmi_views" title="Edit">
+
+    <widget
+        field="body"
+        class=".widgets.RenderableTextAreaWidget" 
+        height="15"
+        style="width: 100%" />
+
+  </editform>
+
+  <defaultView
+      name="edit.html"
+      for="bugtracker.interfaces.IComment"/>
+
+
+</configure>

Added: Zope3/trunk/src/bugtracker/browser/dependencies.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/dependencies.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/dependencies.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,142 @@
+<html metal:use-macro="views/standard_macros/view">
+<head>
+  <style metal:fill-slot="style_slot" tal:define="global pagetip view/legend"
+      type="text/css" media="all"
+      tal:content=
+          "string: @import url(${context/++resource++tracker.css});">
+    @import url(tracker.css);>
+  </style>
+</head>
+
+<body i18n:domain="bugtracker">
+<div metal:fill-slot="body"
+     tal:define="dep_type python:view.request.get('dep_type', 'dependencies')">
+
+  <div class="box"
+    tal:condition="view/canChangeDependencies">
+    <h4 style="padding: 0.3em;">Set Dependencies/Dependents</h4>
+    <div class="body"><div class="even">
+      <div id="explanation"
+           tal:condition="view/getShowDepsOptions">
+        Depending on which view you selected, you look at the dependencies
+        differently, once you look at the parents and once at the children of a
+        bug in the dependency tree:
+        <ul>
+          <li i18n:translate="">Dependencies - Bugs that have to be completed 
+            before this bug can be closed.</li>
+          <li i18n:translate="">Dependents - This bug has to be completed in
+            before the Dependents can be closed.</li>
+        </ul>
+      </div>
+    
+      <form action="setDependencies.html" method="post">
+    
+        <div
+          tal:condition="view/getShowDepsOptions">
+
+          <div class="row">
+            <div class="field">
+              <table width="100%"><tr><td width="48%">
+              <b i18n:translate="">Available Bugs</b><br/>
+              <select name="add_deps:list" size="5" multiple=""
+                  style="width: 100%">
+                <div tal:repeat="bug view/availableBugs" tal:omit-tag="">
+                  <option 
+                      value=""
+                      tal:content="string: ${bug/title} (${bug/name})"
+                      tal:attributes="value bug/name"
+                      tal:condition="
+                          python: bug['name'] not in view.dependencyValues()">
+                    Bug1
+                  </option>
+                </div> 
+              </select>
+              </td><td width="4%">
+                <input type="submit" name="ADD" value="-->" /><br/>
+                <input type="submit" name="DELETE" value="<--" />
+              </td><td width="48%">
+              <div tal:condition="python:dep_type == 'dependencies'">
+                <b i18n:translate="">Dependencies</b>
+                [<a href="@@setDependencies.html?dep_type=dependents" 
+                    i18n:translate="">Dependents</a>]
+              </div>
+              <div tal:condition="python:dep_type == 'dependents'">
+                <b i18n:translate="">Dependents</b>
+                [<a href="@@setDependencies.html?dep_type=dependencies" 
+                    i18n:translate="">Dependencies</a>]
+              </div>
+              <input type="hidden" name="dep_type" value=""
+                  tal:attributes="value dep_type"> 
+    
+              <br/>
+              <select name="del_deps:list" size="5" multiple=""
+                  style="width: 100%">
+                <div tal:repeat="bug view/availableBugs" tal:omit-tag="">
+                  <option 
+                     tal:content="string: ${bug/title} (${bug/name})"
+                     tal:attributes="value bug/name"
+                     tal:condition="
+                         python: bug['name'] in view.dependencyValues()">
+                    Bug1
+                  </option>
+                </div>
+              </select>
+              </td></tr></table>
+            </div>
+          </div>
+        </div>
+    
+        <div class="row">
+          <div class="field">
+            <input type="submit" name="COLLAPSE" value="Collapse"
+                tal:condition="view/getShowDepsOptions"
+                i18n:attributes="value collapse-button" />
+    
+            <input type="submit" name="EXPAND" value="Expand" 
+                tal:condition="not: view/getShowDepsOptions"
+                i18n:attributes="value expand-button" />        
+          </div>
+        </div>
+
+      </form>
+      <div style="clear: both;"/>
+    </div></div>
+  </div>   
+
+  <h4 i18n:translate="">Dependency Statistics</h4>
+
+  <div class="stats"
+       tal:define="stats view/getStatistics"
+       tal:condition="view/getStatistics">
+    <div class="row">
+      <div class="label" i18n:translate="">Closed/Deferred Bugs:</div>
+      <div class="field">
+        <b tal:content="stats/closed_perc">75%</b>
+        (<d tal:replace="stats/closed" /> /
+         <d tal:replace="stats/total" />)
+      </div>
+    </div>
+    <div class="row">
+      <div class="label" i18n:translate="">New (unseen) Bugs:</div>
+      <div class="field">
+        <b tal:content="stats/new_perc">75%</b>
+        (<d tal:replace="stats/new" /> /
+         <d tal:replace="stats/total" />)
+      </div>
+    </div>
+    <div class="row">
+      <div class="label" i18n:translate="">Opened/Assigned Bugs:</div>
+      <div class="field">
+        <b tal:content="stats/open_perc">75%</b>
+        (<d tal:replace="stats/open" /> /
+         <d tal:replace="stats/total" />)
+      </div>
+    </div>
+  </div> 
+
+  <h4 i18n:translate="">Dependency Tree</h4>
+  <p tal:replace="structure view/branch" />
+
+</div>
+</body>
+</html>

Added: Zope3/trunk/src/bugtracker/browser/exportimport.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/exportimport.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/exportimport.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,37 @@
+<html metal:use-macro="context/@@standard_macros/view">
+<head>
+  <style metal:fill-slot="style_slot" 
+      type="text/css" media="all"
+      tal:content=
+          "string: @import url(${context/++resource++tracker.css});">
+    @import url(tracker.css);>
+  </style>
+</head>
+
+<body i18n:domain="bugtracker">
+<div metal:fill-slot="body">
+
+  <h3 i18n:translate="">Export XML</h3>
+
+  <p i18n:translate=""> Simply click <a href="./tracker.xml">here</a>.</p>
+
+  <h3 i18n:translate="">Import XML</h3>
+
+  <form action="import.html" method="post" enctype="multipart/form-data">
+
+    <div class="row">
+      <div class="label" i18n:translate="">XML File</div>
+      <div class="field">
+        <input class="fileType" name="xmlfile" size="20" type="file"/>
+      </div>
+    </div>
+    <div class="row">
+      <input type="submit" name="import" value="Import" 
+             i18n:attributes="value import-button" />
+    </div>
+
+  </form>
+
+</div>
+</body>
+</html>

Added: Zope3/trunk/src/bugtracker/browser/exportimport.py
===================================================================
--- Zope3/trunk/src/bugtracker/browser/exportimport.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/exportimport.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,28 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""XML Import/Export views
+
+$Id: exportimport.py,v 1.1 2003/07/26 13:40:47 srichter Exp $
+"""
+from bugtracker.exportimport import XMLExport, XMLImport
+
+class XMLExportImport(object):
+
+    def exportXML(self):
+        self.request.response.setHeader('Content-Type', 'text/xml')
+        return XMLExport(self.context).getXML()
+
+    def importXML(self, xmlfile):
+        XMLImport(self.context).processXML(xmlfile)
+        return self.request.response.redirect('.')

Added: Zope3/trunk/src/bugtracker/browser/help/tracker_overview.rst
===================================================================
--- Zope3/trunk/src/bugtracker/browser/help/tracker_overview.rst	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/help/tracker_overview.rst	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,6 @@
+Tracker Overview
+================
+
+This page is the main user interface for the bug tracker. It allows you search
+by various criteria. If you specify a "Filter Text", only bugs containing this
+text in their title or description will be shown. 
\ No newline at end of file

Added: Zope3/trunk/src/bugtracker/browser/legend.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/legend.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/legend.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,14 @@
+<span i18n:domain="bugtracker">
+  <h6 i18n:translate="">Status Markup</h6>
+    <a href="" class="node new" i18n:translate="">New</a><br/>
+    <a href="" class="node open" i18n:translate="">Open</a><br/>
+    <a href="" class="node assigned" i18n:translate="">Assigned</a><br/>
+    <a href="" class="node deferred" i18n:translate="">Deferred</a><br/>
+    <a href="" class="node closed" i18n:translate="">Closed</a><br/>
+
+  <h6 i18n:translate="">Priority Markup</h6>
+    <a href="" class="node low" i18n:translate="">Low</a><br/>
+    <a href="" class="node normal" i18n:translate="">Normal</a><br/>
+    <a href="" class="node urgent" i18n:translate="">Urgent</a><br/>
+    <a href="" class="node critical" i18n:translate="">Critial</a><br/>
+</span>

Added: Zope3/trunk/src/bugtracker/browser/mail.py
===================================================================
--- Zope3/trunk/src/bugtracker/browser/mail.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/mail.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,41 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Browser Views for IMessage
+
+$Id: mail.py,v 1.2 2003/08/01 11:14:10 srichter Exp $
+"""
+from bugtracker import TrackerMessageID as _
+from bugtracker.interfaces import IBug, IMailSubscriptions
+
+class MailSubscriptions:
+
+    def subscriptions(self):
+        return IMailSubscriptions(self.context).getSubscriptions()
+
+    def update(self):
+        status = None
+        if 'ADD' in self.request:
+            emails = self.request['emails'].strip().split('\n')
+            IMailSubscriptions(self.context).addSubscriptions(emails)
+            status = _('Subscribers successfully added: $emails')
+            status.mapping = {'emails': ', '.join(emails)}
+        elif 'REMOVE' in self.request:
+            emails = self.request['remails']
+            if isinstance(emails, (str, unicode)):
+                emails = [emails]
+            IMailSubscriptions(self.context).removeSubscriptions(emails)
+            status = _('Subscribers successfully deleted: $emails')
+            status.mapping = {'emails': ', '.join(emails)}
+
+        return status

Added: Zope3/trunk/src/bugtracker/browser/skin/__init__.py
===================================================================

Added: Zope3/trunk/src/bugtracker/browser/skin/configure.zcml
===================================================================
--- Zope3/trunk/src/bugtracker/browser/skin/configure.zcml	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/skin/configure.zcml	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,21 @@
+<configure xmlns="http://namespaces.zope.org/browser">
+
+  <layer name="tracker" />
+
+  <skin name="tracker" layers="tracker rotterdam default" />
+
+  <page 
+      for="*"
+      name="skin_macros"
+      permission="zope.View"
+      layer="tracker"
+      template="template.pt" />
+
+  <page
+      for="*"
+      name="dialog_macros"
+      permission="zope.View"
+      layer="tracker"
+      template="dialog_macros.pt" />
+
+</configure>

Added: Zope3/trunk/src/bugtracker/browser/skin/dialog_macros.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/skin/dialog_macros.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/skin/dialog_macros.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,110 @@
+<metal:block define-macro="dialog"><metal:block define-slot="doctype"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"></metal:block>
+
+<html
+  xmlns="http://www.w3.org/1999/xhtml"
+  xml:lang="en"
+  lang="en" 
+  i18n:domain="zope">
+
+  <head>
+    <title metal:define-slot="title">Zope 3 Bug Tracker</title>
+
+
+    <style type="text/css" 
+           media="all"
+           tal:content=
+           "string: @import url(${context/++resource++zope3.css});"
+           >
+      @import url(zope3.css);
+    </style>
+
+    <style type="text/css" media="all">
+      body {
+        font: 85% Verdana, Arial, Helvetica, sans-serif;
+        background: white;
+        color: black;
+        margin: 0;
+        padding: 0;
+      }
+
+      #title {
+        font-size: 30px; 
+        vertical-align: middle;
+      }
+
+      #workspace {
+        width: 97%;
+        float: left;
+        padding: 1em;
+      }
+
+      #context_information {
+        padding-top: 1em;
+        width: 15%;
+        float: left;
+        padding-left: 0.5em;
+      }
+
+      #content { 
+        float: left; 
+        width: 82%;
+      }
+
+      #footer {
+        border: 0px;
+        text-align: center;  
+        clear: both;
+        width: 100%;
+        color: #808080;
+      }
+
+    </style>
+
+    <metal:block define-slot="headers" />
+    <metal:block define-slot="style_slot" />
+    <metal:block define-slot="ecmascript_slot" />
+
+    <link rel="icon" type="image/png"
+          tal:attributes="href context/++resource++favicon.png" />
+  </head>
+  <body>
+
+    <div id="global">
+      <img tal:attributes="src context/++resource++zope3logo.gif" />
+      <span id="title">&nbsp;Bug Tracker</span>
+    </div>
+
+    <div id="workspace">
+      <div id="content">
+        <div class="item">
+          <metal:block define-slot="body">
+            Here goes the content.
+          </metal:block>
+        </div>
+      </div>
+
+      <div id="context_information">
+        <div id="helpers">
+          <div class="box" id="itemHelp" tal:condition="pagetip|nothing">
+            <h4>Tip</h4>
+            <div class="body">
+              <div class="content odd">
+                <metal:block define-slot="pagetip" tal:replace="pagetip">
+                  A short tip goes here              
+                </metal:block>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div id="footer"
+      metal:define-macro="footer">
+      Powered by Zope 3. Written by Stephan Richter in 2003.
+    </div>
+
+  </body>
+</html>
+
+</metal:block>

Added: Zope3/trunk/src/bugtracker/browser/skin/template.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/skin/template.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/skin/template.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,243 @@
+<metal:block define-macro="page"><metal:block define-slot="doctype"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"></metal:block>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+    xml:lang="en"
+    lang="en"
+    i18n:domain="zope">
+
+  <head>
+    <title 
+        metal:define-slot="title"
+        tal:content="context/title|default"
+        i18n:domain="bugtracker" i18n:translate="">Zope 3 Bug Tracker</title>
+
+
+    <style type="text/css"
+           media="all"
+           tal:content=
+               "string: @import url(${context/++resource++zope3.css});"
+           >
+      @import url(zope3.css);
+    </style>
+
+    <style type="text/css" media="all">
+      body {
+        font: 85% Verdana, Arial, Helvetica, sans-serif;
+        background: white;
+        color: black;
+        margin: 0;
+        padding: 0;
+      }
+
+      #title {
+        font-size: 30px; 
+        vertical-align: middle;
+      }
+
+      #actions div.user {
+        color: white;
+        float: right;
+      }
+
+      #workspace {
+        width: 97%;
+        float: left;
+        padding: 1em;
+      }
+
+      #context_information {
+        padding-top: 1em;
+        width: 15%;
+        float: left;
+        padding-left: 0.5em;
+      }
+
+      #content { 
+        float: left; 
+        width: 82%;
+      }
+      #footer {
+        border: 0px;
+        text-align: center;  
+        clear: both;
+        width: 100%;
+        color: #808080;
+      }
+
+      div.clear { 
+        clear: both; 
+      }
+    </style>
+
+    <meta http-equiv="Content-Type"
+          content="text/html;charset=utf-8" />
+
+    <metal:block define-slot="headers" />
+    <metal:block define-slot="style_slot" />
+    <metal:block define-slot="ecmascript_slot" />
+
+    <!-- Popup script for the OnlineHelp -->
+    <script type="text/javascript" src="onlinehelp.js"
+            tal:attributes="src string:${context/++resource++onlinehelp.js}" >
+    </script>
+
+    <link rel="icon" type="image/png"
+          tal:attributes="href context/++resource++favicon.png" />
+  </head>
+
+  <body>
+
+    <div id="global" i18n:domain="bugtracker">
+      <img tal:attributes="src context/++resource++zope3logo.gif" />
+      <span id="title" i18n:translate="">&nbsp;Bug Tracker</span>
+    </div>
+
+    <div id="workspace">
+
+      <div id="breadcrumbs"
+           tal:condition="python: macroname == 'view'"
+           metal:define-macro="breadcrumbs"
+       > Location:&nbsp;<tal:block
+           repeat="breadcrumb context/@@absolute_url/breadcrumbs"
+          ><a href=""
+              tal:condition="repeat/breadcrumb/start"
+              tal:content="string:[top]"
+              tal:attributes="
+              href string:${breadcrumb/url}/@@SelectedManagementView.html"
+              >XXX should not hardcode root folder name string</a
+             ><a href=""
+                 tal:condition="not:repeat/breadcrumb/start"
+                 tal:content="string:${breadcrumb/name}"
+                 tal:attributes="
+                 href string:${breadcrumb/url}/@@SelectedManagementView.html"
+                  >breadcrumb item</a> / </tal:block>
+      </div>
+
+      <div class="itemViews"
+           tal:condition="python: macroname == 'view'"
+           metal:define-slot="tabs">
+        <tal:block repeat="view context/@@view_get_menu/zmi_views">
+          <a href=""
+            tal:attributes="href view/action;
+                            class view/selected;"
+            tal:content="view/title">
+            label
+          </a>
+        </tal:block>
+      </div>
+
+      <div id="actions" 
+           tal:condition="python: macroname == 'view'"
+           tal:define="help_actions context/@@view_get_menu/help_actions"
+           metal:define-slot="actions">
+        <span tal:condition="help_actions"
+              tal:repeat="help_info help_actions"
+              tal:omit-tag="">
+          <a href="#"
+               tal:define="url string:'${view/__name__}/${help_info/action}';
+                           name string:'Online Help';
+                           settings string:'height=400
+                                           ,width=700
+                                           ,resizable=1
+                                           ,scrollbars=yes
+                                           ,location=no
+                                           ,status=no
+                                           ,toolbar=no
+                                           ,menubar=no'"
+               tal:attributes="href python:'javascript:popup('
+                                           + url + ','
+                                           + name + ','
+                                           + settings +')'"
+               tal:content="help_info/title"
+               i18n:translate="">
+              Action name
+          </a>
+        </span>
+        <a href="" 
+          tal:attributes="href string:@@logout.html"
+          tal:condition="python: hasattr(view.request.principal, 'getLogin')"
+          i18n:translate="">
+          Logout</a>
+        <a href="" 
+          tal:attributes="href string:@@login.html"
+          tal:condition="python: not hasattr(view.request.principal, 'getLogin')"
+          i18n:translate="">
+          Login</a>          
+        <div class="user" i18n:domain="bugtracker" i18n:translate="">
+          You are logged in as 
+          <d tal:replace="request/principal/title" i18n:name="user_title"/>.
+        </div>
+        &nbsp;
+      </div>
+
+      <div id="viewspace">
+
+        <div metal:define-slot="message" id="message">
+        </div>
+
+        <div id="content">
+          <metal:block define-slot="body">
+            <table class="listing">
+              <thead>
+                <th>Test</th>
+                <th>Another</th>
+              </thead>
+              <tbody>
+                      <tr>
+                        <td>content</td>
+                        <td>thingy</td>
+                      </tr>
+                      <tr class="even">
+                        <td>more</td>
+                        <td>data</td>
+                      </tr>
+              </tbody>
+            </table>
+          </metal:block>
+        </div>
+
+
+        <div id="context_information">
+
+          <div id="helpers">
+
+            <div class="box" id="itemHelp" tal:condition="pagetip|nothing">
+              <h4 i18n:translate="">Tip</h4>
+              <div class="body">
+                <div class="content odd">
+                  <metal:block define-slot="pagetip"
+                               tal:replace="structure pagetip">
+                    A short tip goes here
+                  </metal:block>
+                </div>
+              </div>
+            </div>
+
+            <div class="spacer">
+              &nbsp;
+            </div>
+
+          </div>
+
+        </div>
+
+        <div class="spacer">
+          &nbsp;
+        </div>
+
+      </div>
+    </div>
+
+    <div id="footer" metal:define-macro="footer" 
+         i18n:domain="bugtracker" i18n:translate="">
+      Powered by Zope 3. Written by Stephan Richter in 2003.
+    </div>
+
+    <div id="logger" />
+
+  </body>
+
+</html>
+
+</metal:block>
+

Added: Zope3/trunk/src/bugtracker/browser/subscriptions.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/subscriptions.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/subscriptions.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,48 @@
+<html metal:use-macro="context/@@standard_macros/view">
+<body>
+<div metal:fill-slot="body" i18n:domain="bugtracker">
+
+  <p tal:define="status view/update"
+    tal:condition="status"
+    tal:content="status" />
+
+  <form method="post">
+
+    <div class="row">
+        <div class="label" i18n:translate="">Current Subscriptions</div>
+        <div class="field">
+          <div class="row" tal:repeat="email view/subscriptions">
+            <input type="checkbox" name="remails:list" 
+                   value="" tal:attributes="value email">
+            <div tal:replace="email">zope3 at zope3.org</div>
+          </div>
+          <div class="row">
+            <input type="submit" name="REMOVE" value="Remove" 
+                 i18n:attributes="value remove-button">
+          </div>
+        </div>
+    </div>
+
+    <div class="row">
+        <div class="label" i18n:translate="">
+          Enter new Users (separate by 'Return')
+        </div>
+        <div class="field">
+          <textarea name="emails" cols="40" rows="10"></textarea>
+        </div>
+    </div>
+
+  	<div class="row">
+  	  <div class="controls">
+  	    <input type="submit" value="Refresh" 
+            i18n:attributes="value refresh-button" />
+  	    <input type="submit" name="ADD" value="Add" 
+  		i18n:attributes="value add-button" />
+  	  </div>
+  	</div>
+
+  </form>
+
+</div>
+</body>
+</html>

Added: Zope3/trunk/src/bugtracker/browser/tests/__init__.py
===================================================================

Added: Zope3/trunk/src/bugtracker/browser/tests/test_bug.py
===================================================================
--- Zope3/trunk/src/bugtracker/browser/tests/test_bug.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/tests/test_bug.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,94 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bug Tracker Mail Subscription and Mailer Tests
+
+$Id$
+"""
+import unittest
+
+from zope.publisher.browser import TestRequest
+from bugtracker.browser.bug import BugBaseView, Overview
+from bugtracker.tests.placelesssetup import PlacelessSetup
+
+class BugBaseViewTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        super(BugBaseViewTest, self).setUp()
+        self.view = BugBaseView()
+        self.view.context = self.generateBug()
+        self.view.request = TestRequest(environ={'HTTP_ACCEPT_LANGUAGE': 'en'})
+
+    def test_created(self):
+        self.assertEqual(self.view.created(), '3/3/03 3:00 AM')
+
+    def test_modified(self):
+        self.assertEqual(self.view.modified(), '3/3/03 4:00 AM')
+
+    def test_description(self):
+        self.assertEqual(self.view.description(), 'This is Bug 1.')
+
+    def test_status(self):
+        self.assertEqual(self.view.status().value, 'new')
+        self.assertEqual(self.view.status().title, 'New')
+
+    def test_type(self):
+        self.assertEqual(self.view.type().value, 'bug')
+        self.assertEqual(self.view.type().title, 'Bug')
+
+    def test_priority(self):
+        self.assertEqual(self.view.priority().value, 'normal')
+        self.assertEqual(self.view.priority().title, 'Normal')
+
+    def test_release(self):
+        self.assertEqual(self.view.release().value, 'None')
+        self.assertEqual(self.view.release().title, '(not specified)')
+
+    def test_owners(self):
+        self.assertEqual(self.view.owners()[0]['login'], 'jim')
+        self.assertEqual(self.view.owners()[0]['title'], 'Jim Fulton')
+        self.assertEqual(self.view.owners()[1]['login'], 'stevea')
+        self.assertEqual(self.view.owners()[1]['title'], 'Steve Alexander')
+
+
+class OverviewTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        self.view = Overview()
+        self.view.context = self.generateBug()
+        self.view.request = TestRequest(environ={'HTTP_ACCEPT_LANGUAGE': 'en'})
+
+    def test_comments(self):
+        comments = self.view.comments()
+        self.assertEqual(comments[0].creator()['id'], u'zope.srichter')
+        self.assertEqual(comments[0].modified(), '3/3/03 6:00 AM')
+        self.assertEqual(comments[0].body(), 'This is comment 1.')
+
+    def test_attachments(self):
+        attachments = self.view.attachments()
+        self.assertEqual(attachments[0]['name'], 'attach.txt')
+        self.assertEqual(attachments[0]['size'], '1 KB')
+
+    def test_dependencies(self):
+        self.assertEqual(self.view.dependencies(), ())
+
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(BugBaseViewTest),
+        unittest.makeSuite(OverviewTest),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()

Added: Zope3/trunk/src/bugtracker/browser/tracker.css
===================================================================
--- Zope3/trunk/src/bugtracker/browser/tracker.css	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/tracker.css	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,170 @@
+/* Tracker Overview */
+
+.summary_content {
+  padding-left: 0.5em;
+}
+
+.summary_title {
+  font-weight: bold;
+}
+
+.summary_body {
+  font-size: 100%;
+}
+
+.summary_condition {
+}
+
+.summary_metadata {
+  font-size: 80%;
+  color: #808080;
+}
+
+div.even {
+  padding: 0.3em;
+  background-color: #F8F8F8;
+}
+
+div.odd {
+  padding: 0.3em;
+  background-color: White;
+}
+
+div.batch {
+  background-color: #F0F0F0;
+  border: 1px solid #808080;
+  padding: 2px;
+  clear: both;
+}
+
+div.batch div.prev_batch {
+  text-align: left;
+  float: left;        
+  width: 20%;
+}
+
+div.batch div.curr_batch {
+  text-align: center;
+  float: left;
+  width: 59%;
+}
+
+div.batch div.next_batch {
+  text-align: right;
+  float: right;
+  width: 20%;
+}
+
+div.clear { 
+  clear: both; 
+}
+
+
+/* Bug Overview */
+
+.single_p {
+  padding-top: 0.5em;
+}
+
+.comment {
+  margin: 0.6em;
+  padding: 0.6em;
+  background-color: #F0F0F0;
+  border: 1pt solid #AAAAAA;
+}
+
+#description {
+  padding: 0.6em;
+  margin: 0.6em;
+  background-color: #F8F8F8;
+  border: 1pt solid #EEEEEE;        
+}
+
+p {
+  margin: 0px;
+}
+
+div.action {
+  padding-top: 1em;
+}
+
+#attachments {
+  margin-top: 4px;
+  margin-bottom: 0px;
+}
+
+div.md_label {
+  width: 25%;
+}
+
+div.md_field {
+  width: 25%;
+}
+
+/* Bug Dependencies */
+
+#explanation {
+  font-weight: bold;
+}
+
+a.node {
+  text-decoration: none;
+}
+
+/* Status */
+
+.new {
+  font-style: italic;
+}
+
+.open {
+  font-variant: small-caps;
+}
+
+.assigned {
+}
+
+.closed {
+  text-decoration: line-through;
+}
+
+.deferred {
+  font-style: italic;
+  text-decoration: line-through;
+}
+
+/* Priorities */ 
+
+.low {
+  color: black;
+}
+
+.normal {
+}
+
+.urgent {
+  font-weight: bold;
+  color: #FFCE7B;
+}
+
+.critical {
+  font-weight: bold;
+  color: #FFA500;
+}
+
+/* Statistics-specific options */
+
+div.stats {
+  padding-top: 0.5em;
+  padding-bottom: 1.5em;
+}
+
+div.stats div.row {
+  margin: 2pt;
+  padding: 0pt;
+  width: 100%;
+}
+
+div.stats div.label {
+  width: 30%;
+}

Added: Zope3/trunk/src/bugtracker/browser/tracker.py
===================================================================
--- Zope3/trunk/src/bugtracker/browser/tracker.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/tracker.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,266 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Browser View Components for Bug Trackers
+
+$Id: tracker.py,v 1.13 2004/03/18 18:04:54 philikon Exp $
+"""
+from zope.app import zapi
+from zope.app.container.browser.adding import Adding
+from zope.app.dublincore.interfaces import IZopeDublinCore
+from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
+from zope.schema.vocabulary import getVocabularyRegistry
+
+from bugtracker import TrackerMessageID as _
+from bugtracker.batching import Batch
+from bugtracker.browser.bug import BugBaseView
+from bugtracker.interfaces import IStatusVocabulary, IReleaseVocabulary
+from bugtracker.interfaces import IPriorityVocabulary, IBugTypeVocabulary
+from bugtracker.interfaces import IComment
+from bugtracker.interfaces import ISearchableText
+
+
+class BugTrackerAdding(Adding):
+    """Custom adding view for NewsSite objects."""
+
+    def add(self, content):
+        self.context['dummy'] = content
+        self.contentName = zapi.name(content)
+        return self.context[name]
+
+
+class AddBugTracker(object):
+    """Add a bug tracker."""
+
+    def createAndAdd(self, data):
+        content = super(AddBugTracker, self).createAndAdd(data)
+
+        if self.request.get('setup_vocabs'):
+            vocab = IStatusVocabulary(content)
+            vocab.add('new', _('New'), True)
+            vocab.add('open', _('Open'))
+            vocab.add('assigned', _('Assigned'))
+            vocab.add('deferred', _('Deferred'))
+            vocab.add('closed', _('Closed'))
+            vocab = IBugTypeVocabulary(content)
+            vocab.add('bug', _('Bug'), True)
+            vocab.add('feature', _('Feature'))
+            vocab.add('release', _('Release'))
+            vocab = IReleaseVocabulary(content)
+            vocab.add('None', _('(not specified)'), True)
+            vocab = IPriorityVocabulary(content)
+            vocab.add('low', _('Low'))
+            vocab.add('normal', _('Normal'), True)
+            vocab.add('urgent', _('Urgent'))
+            vocab.add('critical', _('Critical'))
+
+        return content
+
+
+class Settings(object):
+    """Change the settings of the Bug Tracker."""
+
+    # list of managable vocabulary interfaces that we want to have
+    ifaces = [IStatusVocabulary, IReleaseVocabulary,
+              IPriorityVocabulary, IBugTypeVocabulary]
+
+    def getManagableVocabularyViews(self):
+        return map(lambda iface:
+                   ManagableVocabularyView(self.context, self.request, iface),
+                   self.ifaces)
+
+    def addValue(self, iface, value, title):
+        iface = filter(lambda i: i.getName() == iface, self.ifaces)[0]
+        vocab = ManagableVocabularyView(self.context, self.request, iface)
+        vocab.addValue(value, title)
+
+    def deleteValues(self, iface, values):
+        iface = filter(lambda i: i.getName() == iface, self.ifaces)[0]
+        vocab = ManagableVocabularyView(self.context, self.request, iface)
+        vocab.deleteValues(values)
+
+    def setDefaultValue(self, iface, values):
+        iface = filter(lambda i: i.getName() == iface, self.ifaces)[0]
+        vocab = ManagableVocabularyView(self.context, self.request, iface)
+        vocab.setDefault(values[0])
+
+
+class ManagableVocabularyView(object):
+
+    def __init__(self, context, request, vocab_iface):
+        self.context = context
+        self.request = request
+        self.vocab_iface = vocab_iface
+
+    def getExistingValues(self):
+        vocab = self.vocab_iface(self.context)
+        return iter(vocab)
+
+    def addValue(self, value, title):
+        vocab = self.vocab_iface(self.context)
+        vocab.add(value, title)
+        return self.request.response.redirect('./@@settings.html')
+
+    def deleteValues(self, values):
+        vocab = self.vocab_iface(self.context)
+        for value in values:
+            vocab.delete(value)
+        return self.request.response.redirect('./@@settings.html')
+
+    def title(self):
+        vocab = self.vocab_iface(self.context)
+        return vocab.title
+
+    def default(self):
+        vocab = self.vocab_iface(self.context)
+        return vocab.default
+
+    def setDefault(self, value):  
+        vocab = self.vocab_iface(self.context)
+        vocab.default = value
+        return self.request.response.redirect('./@@settings.html')
+
+
+def checkBug(bug, criteria, search_text):
+    for name, values in criteria:
+        if name is 'owners':
+            if values and not filter(lambda u: u in bug.owners, values):
+                return False
+        else:
+            if values and not getattr(bug, name) in values:
+                return False
+
+    # XXX: Extremely crude text search; should use indexes
+    text = ' '.join(ISearchableText(bug).getSearchableText())
+    if search_text != '':
+        terms = search_text.split(' ')
+        for term in terms:
+            if not term in text:
+                return False
+        
+    return True
+
+
+class BugView(BugBaseView):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def numberOfComments(self):
+        return len(filter(IComment.isImplementedBy, self.context.values()))
+
+    def name(self):
+        return zapi.name(self.context)
+
+    def descriptionPreview(self):
+        if len(self.context.description) < 200:
+            return self.context.description
+        else:
+            return self.context.description[:200] + u'...'
+
+    def shortCreated(self):
+        return self.created().split()[0]
+
+
+class Overview(object):
+    """Overview of all the bugs."""
+
+    bug_listing_normal = ViewPageTemplateFile('bug_listing_normal.pt')
+    bug_listing_compressed = ViewPageTemplateFile('bug_listing_compressed.pt')
+
+    # Tuple values:
+    #   - collection name
+    #   - Vocabulary Registry name
+    #   - Display Title
+    #   - bug attribute name
+    filter_vars = (('stati', 'Stati', 'Status', 'status'),
+                   ('types', 'BugTypes', 'Type', 'type'),
+                   ('releases', 'Releases', 'Release', 'release'),
+                   ('priorities', 'Priorities', 'Priority', 'priority'),
+                   ('owners', 'Users', 'Owners', 'owners'),
+                   )
+
+    def getBugs(self):
+        """Return a list of all bugs having a status listed in the parameter.
+
+        If the parameter is an empty list/tuple, then show all bugs.
+        """
+        if hasattr(self, '_bugs'):
+            return self._bugs
+
+        criteria = []
+        for collName, dummy1, dummy2, name in self.filter_vars:
+            raw = self.request.cookies.get('filter_'+collName, "")
+            criteria.append((name, raw != "" and raw.split(", ") or []))
+        
+        formatter = self.request.locale.dates.getFormatter('dateTime', 'short')
+        result = []
+        for name, bug in self.context.items():
+            if checkBug(bug, criteria, self.getSearchText()):
+                result.append(BugView(bug, self.request))
+
+
+        start = int(self.request.get('start', 0))
+        size = int(self.request.get('size', 20))
+                
+        self._bugs = Batch(result, start, size)
+        return self._bugs
+
+    def updateSettings(self):
+        for collName, dummy1, dummy2, dummy3 in self.filter_vars:
+            values = self.request.get(collName, [])
+            self.request.response.setCookie('filter_'+collName,
+                                            ', '.join(values))
+        self.setSearchText()
+        self.setViewType()
+        self.setShowFilterOptions()
+        return self.request.response.redirect('./overview.html')
+
+    def getSettingsInfo(self):
+        registry = getVocabularyRegistry()
+        info = []
+        for varname, vocname, title, dummy in self.filter_vars:
+            raw = self.request.cookies.get('filter_'+varname, "")
+            info.append({'setting': raw != "" and raw.split(", ") or [],
+                         'all': iter(registry.get(self.context, vocname)),
+                         'title': title,
+                         'name': varname})
+        return info
+            
+    def getSearchText(self):
+        return self.request.cookies.get('search_text', '')
+
+    def setSearchText(self):
+        value = self.request.get('search_text', '')
+        self.request.response.setCookie('search_text', value)
+
+    def getViewType(self):
+        return self.request.cookies.get('view_type', 'normal')
+
+    def setViewType(self):
+        value = self.request.get('view_type', 'normal')
+        self.request.response.setCookie('view_type', value)
+
+    def getShowFilterOptions(self):
+        return int(self.request.cookies.get('show_filter_options', '1'))
+
+    def setShowFilterOptions(self):
+        if 'COLLAPSE' in self.request:
+            value = '0'
+        else:
+            value = '1'
+        self.request.response.setCookie('show_filter_options', value)
+
+    def numberOfBugs(self):
+        return len(self.context)

Added: Zope3/trunk/src/bugtracker/browser/tracker_add.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/tracker_add.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/tracker_add.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,26 @@
+<html metal:use-macro="context/@@standard_macros/page">
+<body>
+
+  <div metal:fill-slot="body">
+
+    <div metal:use-macro="context/@@form_macros/addform">
+
+      <div class="row" metal:fill-slot="extra_bottom">
+        <div class="field">
+          <h3><input type="checkbox" name="setup_vocabs:int" value="1"
+                checked=""/>
+            <span i18n:translate="">Create Initial Vocabulary Entries</span>
+          </h3>
+          <span i18n:translate="">To make your life easier, when this
+                option is selected, it creates vocabulary entries for the
+                status, release, priority and type fields of a bug. This
+                will save you some time with the setup.</span>
+        </div>
+      </div>
+
+    </div>
+
+  </div>
+
+</body>
+</html>

Added: Zope3/trunk/src/bugtracker/browser/tracker_icon.png
===================================================================
(Binary files differ)


Property changes on: Zope3/trunk/src/bugtracker/browser/tracker_icon.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: Zope3/trunk/src/bugtracker/browser/tracker_overview.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/tracker_overview.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/tracker_overview.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,179 @@
+<html metal:use-macro="context/@@standard_macros/view">
+<head>
+  <style metal:fill-slot="style_slot"
+      type="text/css" media="all"
+      tal:content=
+          "string: @import url(${context/++resource++tracker.css});">
+    @import url(tracker.css);>
+  </style>
+</head>
+
+<body>
+<div metal:fill-slot="body" tal:define="bugs view/getBugs">
+
+  <div class="box">
+    <h4 style="padding: 0.3em">Filter Options</h4>
+    <div class="body">
+      <form action="updateOverviewSettings.html" method="post" 
+          class="even" style="padding: 0.5em;"
+          tal:define="settings view/getSettingsInfo">
+            
+        <div class="row"
+            tal:condition="view/getShowFilterOptions">
+          <div class="field">
+            <b>Filter Text</b>
+            <input type="text" name="search_text" value=""
+                tal:attributes="value view/getSearchText">
+          </div>
+          <div class="field">&nbsp;&nbsp;&nbsp;&nbsp;</div>
+          <div class="field">
+            <b>View Type</b>
+            <select name="view_type">
+              <tal:block repeat="vt python: ('normal', 'compressed')">
+                <option selected=""
+                    tal:content="vt"
+                    tal:attributes="value vt"
+                    tal:condition="python: view.getViewType() == vt" />
+                <option
+                    tal:content="vt"
+                    tal:attributes="value vt"
+                    tal:condition="python: view.getViewType() != vt" />
+              </tal:block>
+            </select>
+          </div>
+        </div>
+    
+        <div class="row"
+            tal:condition="view/getShowFilterOptions">
+          <div class="field" tal:repeat="var settings">
+    
+            <b tal:content="var/title">Status</b><br/>
+            <select size="5" name="stati:list" multiple="yes"
+                tal:attributes="name string:${var/name}:list">
+              <tal:block repeat="entry var/all">
+                <option value=""
+                  tal:condition="python: entry.value in var['setting']"
+                  tal:attributes="value entry/value"
+                  tal:content="entry/title" selected="">New</option>
+                <option value=""
+                  tal:condition="python: entry.value not in var['setting']"
+                  tal:attributes="value entry/value"
+                  tal:content="entry/title">New</option>
+              </tal:block>
+            </select>
+    
+          </div>
+        </div>
+        <div class="row">
+          <div class="field">
+            <input type="submit" value="Apply Filter/Changes" 
+                tal:condition="view/getShowFilterOptions"
+                i18n:attributes="value save-filter-changes-button" />
+          </div>
+          <div class="field">
+            <input type="submit" name="COLLAPSE" value="Collapse"
+                tal:condition="view/getShowFilterOptions"
+                i18n:attributes="value collapse-button" />
+    
+            <input type="submit" name="EXPAND" value="Expand" 
+                tal:condition="not: view/getShowFilterOptions"
+                i18n:attributes="value expand-button" />        
+          </div>
+        </div>
+        <div class="clear"/>
+    
+      </form>
+    </div>
+  </div>
+
+  <div class="row" i18n:domain="bugtracker">
+    <div class="control">
+      <form action="./@@+/AddBug=" method="post">
+        <input type="submit" name="add" value="Add Bug" 
+               i18n:attributes="value add-bug-button"/>
+      </form>
+    </div>
+  </div>
+  <div class="batch" tal:condition="bugs/startNumber" i18n:domain="bugtracker">
+    <div class="prev_batch" tal:define="prev bugs/prevBatch">
+      <a href=""
+          tal:condition="prev"
+          tal:attributes="href 
+              string:./@@overview.html?start=${prev/start}&size=${prev/size}"
+          i18n:translate="">
+        Previous
+        (<d tal:replace="prev/startNumber" i18n:name="start_number" /> to 
+         <d tal:replace="prev/endNumber" i18n:name="end_number" />) 
+      </a>&nbsp;
+    </div>
+    <div class="curr_batch" i18n:translate="">
+        <d tal:replace="bugs/startNumber" i18n:name="start_number"/> to 
+        <d tal:replace="bugs/endNumber" i18n:name="end_number"/>
+        of <d tal:replace="bugs/total" i18n:name="batch_total_number"/> found
+        (<d tal:replace="view/numberOfBugs" i18n:name="bug_number"/> total)
+    </div>
+    <div class="next_batch" tal:define="next bugs/nextBatch">
+      <a href=""
+          tal:condition="next"
+          tal:attributes="href 
+              string:./@@overview.html?start=${next/start}&size=${next/size}"
+          i18n:translate="">
+        Next
+        (<d tal:replace="next/startNumber" i18n:name="start_number" /> to 
+         <d tal:replace="next/endNumber" i18n:name="end_number" />) 
+      </a>&nbsp;
+    </div>
+    <div class="clear"></div> 
+  </div>
+
+  <div 
+      tal:condition="python: view.getViewType() == 'normal'"
+      tal:replace="structure view/bug_listing_normal" />
+
+  <div 
+      tal:condition="python: view.getViewType() == 'compressed'"
+      tal:replace="structure view/bug_listing_compressed" />
+
+  <div class="batch" tal:condition="bugs/startNumber" i18n:domain="bugtracker">
+    <div class="prev_batch" tal:define="prev bugs/prevBatch">
+      <a href=""
+          tal:condition="prev"
+          tal:attributes="href 
+              string:./@@overview.html?start=${prev/start}&size=${prev/size}"
+          i18n:translate="">
+        Previous
+        (<d tal:replace="prev/startNumber" i18n:name="start_number" /> to 
+         <d tal:replace="prev/endNumber" i18n:name="end_number" />) 
+      </a>&nbsp;
+    </div>
+    <div class="curr_batch" i18n:translate="">
+        <d tal:replace="bugs/startNumber" i18n:name="start_number"/> to 
+        <d tal:replace="bugs/endNumber" i18n:name="end_number"/>
+        of <d tal:replace="bugs/total" i18n:name="batch_total_number"/> found
+        (<d tal:replace="view/numberOfBugs" i18n:name="bug_number"/> total)
+    </div>
+    <div class="next_batch" tal:define="next bugs/nextBatch">
+      <a href=""
+          tal:condition="next"
+          tal:attributes="href 
+              string:./@@overview.html?start=${next/start}&size=${next/size}"
+          i18n:translate="">
+        Next
+        (<d tal:replace="next/startNumber" i18n:name="start_number" /> to 
+         <d tal:replace="next/endNumber" i18n:name="end_number" />) 
+      </a>&nbsp;
+    </div>
+    <div class="clear"></div> 
+  </div>
+  <div class="row" i18n:domain="bugtracker">
+    <div class="control">
+      <form action="./@@+/AddBug=" method="post">
+        <input type="submit" name="add" value="Add Bug" 
+               i18n:attributes="value add-bug-button"/>
+      </form>
+    </div>
+  </div>
+
+</div>
+</body>
+</html>

Added: Zope3/trunk/src/bugtracker/browser/tracker_settings.pt
===================================================================
--- Zope3/trunk/src/bugtracker/browser/tracker_settings.pt	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/tracker_settings.pt	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,46 @@
+<html metal:use-macro="context/@@standard_macros/view">
+<body>
+<div metal:fill-slot="body">
+
+  <div tal:repeat="vocab view/getManagableVocabularyViews">     
+
+  <h2 tal:content="vocab/title">Some Variable Definitions</h2>
+  <br />
+  <form action="." method="post">
+
+    <input type="hidden" name="iface" value=""
+        tal:attributes="value vocab/vocab_iface/getName" />
+
+    <b i18n:translate="" i18n:domain="bugtracker">Existing Values:</b> 
+    <select size="5" name="values:list" multiple="yes">
+      <tal:block repeat="entry vocab/getExistingValues">
+        <option value=""
+            tal:attributes="value entry/value"
+            tal:content="string: ${entry/title} (${entry/value})">
+          New
+        </option>
+      </tal:block>
+    </select>
+    <input type="submit" name="deleteValues.html:method" value="Delete" 
+           i18n:attributes="value delete-button"/>
+    <input type="submit" name="setDefaultValue.html:method" 
+           value="Set Default" i18n:attributes="value set-default-button"/>
+    <br>
+    <b i18n:translate="" i18n:domain="bugtracker">Default Value:</b>
+    <span tal:define="default vocab/default" 
+          tal:content="string: ${default/title} (${default/value})" />
+
+    <br>
+    <span i18n:translate="">Value:</span> 
+      <input type="text" size="10" name="value" />
+    <span i18n:translate="">Title:</span>
+      <input type="text" size="10" name="title" />
+    <input type="submit" name="addValue.html:method" value="Add" 
+           i18n:attributes="value add-button"/>
+  </form>
+
+  </div>
+
+</div>
+</body>
+</html>

Added: Zope3/trunk/src/bugtracker/browser/widgets.py
===================================================================
--- Zope3/trunk/src/bugtracker/browser/widgets.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/browser/widgets.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,65 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Browser widgets for bug tracker
+
+$Id: textwidgets.py 25318 2004-06-09 21:00:59Z garrett $
+"""
+from zope.proxy import removeAllProxies
+from zope.schema import Choice
+
+from zope.app.form.browser import TextAreaWidget
+from zope.app.form.browser.itemswidgets import ChoiceInputWidget
+
+from bugtracker.renderable import RenderableText
+
+choice = Choice(
+    title=u'Type',
+    description=u'Type of the text above',
+    default='zope.source.rest',
+    required=True,
+    vocabulary='SourceTypes')
+
+
+class RenderableTextAreaWidget(TextAreaWidget):
+
+    def _getTypeWidget(self):
+        bound_choice = choice.bind(self.context.context)
+        bound_choice.__name__ = self.context.__name__ + '_ttype'
+        widget = ChoiceInputWidget(bound_choice, self.request)
+        attr = getattr(self.context.context, self.context.__name__, None)
+        widget.setRenderedValue(getattr(attr, 'ttype', choice.default))
+        return widget
+
+    def __call__(self):
+        html = super(RenderableTextAreaWidget, self).__call__()
+        html += '\n<br />\n'
+        html += self._getTypeWidget()()
+        return html
+
+    def _toFieldValue(self, value):
+        value = super(RenderableTextAreaWidget, self)._toFieldValue(value)
+        ttype = self._getTypeWidget().getInputValue()
+        return RenderableText(value, ttype)
+        
+    def applyChanges(self, content):
+        field = self.context
+        value = self.getInputValue()
+        current = field.query(content, self)
+
+        if unicode(value) == unicode(current) and \
+           value.ttype == getattr(current, 'ttype', object()):
+            return False
+        else:
+            field.set(content, value)
+            return True

Added: Zope3/trunk/src/bugtracker/bug.py
===================================================================
--- Zope3/trunk/src/bugtracker/bug.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/bug.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,210 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+# 
+##############################################################################
+"""A Container based Bug
+
+$Id: bug.py,v 1.4 2003/08/28 05:22:30 srichter Exp $
+"""
+from zope.interface import implements
+from zope.proxy import removeAllProxies
+
+from zope.app import zapi
+from zope.app.annotation.interfaces import IAnnotations
+from zope.app.container.btree import BTreeContainer
+from zope.app.container.contained import contained
+from zope.app.dublincore.interfaces import IZopeDublinCore
+
+from bugtracker.interfaces import IBug, IComment
+from bugtracker.interfaces import IAttachmentContainer
+from bugtracker.interfaces import IBugDependencies
+from bugtracker.interfaces import IBugTrackerContained
+from bugtracker.interfaces import ISearchableText
+from bugtracker.vocabulary import VocabularyPropertyGetter
+from bugtracker.vocabulary import VocabularyPropertySetter
+from bugtracker import TrackerMessageID as _
+
+DependencyKey = 'bugtracker.dependencies'
+
+
+class Bug(BTreeContainer):
+
+    implements(IBug, IAttachmentContainer, IBugTrackerContained)
+
+    # See zopeproducts.bugtracker.interfaces.IBug
+    status = property(
+        VocabularyPropertyGetter('_status', _('Stati')),
+        VocabularyPropertySetter('_status', _('Stati')))
+
+    # See zopeproducts.bugtracker.interfaces.IBug
+    priority = property(
+        VocabularyPropertyGetter('_priority', _('Priorities')),
+        VocabularyPropertySetter('_priority', _('Priorities')))
+
+    # See zopeproducts.bugtracker.interfaces.IBug
+    type = property(
+        VocabularyPropertyGetter('_type', _('BugTypes')),
+        VocabularyPropertySetter('_type', _('BugTypes')))
+
+    # See zopeproducts.bugtracker.interfaces.IBug
+    release = property(
+        VocabularyPropertyGetter('_release', _('Releases')),
+        VocabularyPropertySetter('_release', _('Releases')))
+
+    def getOwners(self):
+        return getattr(self, '_owners', [])
+    
+    def setOwners(self, owners):
+        self._owners = removeAllProxies(owners)
+
+    # See zopeproducts.bugtracker.interfaces.IBug
+    owners = property(getOwners, setOwners)
+
+    def setTitle(self, title):
+        """Set bug title."""
+        dc = IZopeDublinCore(self)
+        dc.title = title
+
+    def getTitle(self):
+        """Get bug title."""
+        dc = IZopeDublinCore(self)
+        return dc.title
+
+    # See zopeproducts.bugtracker.interfaces.IBug
+    title = property(getTitle, setTitle)
+
+    def setDescription(self, description):
+        """Set bug description."""
+        dc = IZopeDublinCore(self)
+        dc.description = description
+
+    def getDescription(self):
+        """Get bug description."""
+        dc = IZopeDublinCore(self)
+        return dc.description
+
+    # See zopeproducts.bugtracker.interfaces.IBug
+    description = property(getDescription, setDescription)    
+
+    def getSubmitter(self):
+        """Get bug submitter."""
+        dc = IZopeDublinCore(self)
+        if not dc.creators:
+            return None
+        return dc.creators[0]
+
+    # See zopeproducts.bugtracker.interfaces.IBug
+    submitter = property(getSubmitter)    
+
+
+class BugDependencyAdapter(object):
+
+    implements(IBugDependencies)
+    __used_for__ = IBug
+
+    def __init__(self, context):
+        self.context = context
+        self._annotations = IAnnotations(context)
+        if not self._annotations.get(DependencyKey):
+            self._annotations[DependencyKey] = ()
+
+    def addDependencies(self, dependencies):
+        self._annotations[DependencyKey] += tuple(dependencies)
+
+    def deleteDependencies(self, dependencies):
+        self.dependencies = filter(lambda d: d not in dependencies,
+                                   self.dependencies)
+
+    def setDependencies(self, dependencies):
+        self._annotations[DependencyKey] = tuple(dependencies)
+
+    def getDependencies(self):
+        return self._annotations[DependencyKey]
+
+    dependencies = property(getDependencies, setDependencies)
+
+    def addDependents(self, dependents):
+        tracker = zapi.getParent(self.context)
+        bug_id = zapi.name(self.context)
+        for id in dependents:
+            deps = IBugDependencies(tracker[id])
+            deps.dependencies += (bug_id,)
+
+    def deleteDependents(self, dependents):
+        tracker = zapi.getParent(self.context)
+        bug_id = zapi.name(self.context)
+        for id in dependents:
+            deps = IBugDependencies(tracker[id])
+            d = filter(lambda x: str(x) != str(bug_id), deps.dependencies)
+            deps.dependencies = tuple(d)
+
+    def setDependents(self, dependents):
+        tracker = zapi.getParent(self.context)
+        bug_id = zapi.name(self.context)
+        for id, bug in tracker.items():
+            deps = IBugDependencies(bug)
+            if bug_id in deps.dependencies and id not in dependents:
+                d = list(deps.dependencies)
+                d.remove(bug_id)
+                deps.dependencies += tuple(d)
+            if bug_id not in deps.dependencies and id in dependents:
+                deps.dependencies += (bug_id,)
+
+    def getDependents(self):
+        tracker = zapi.getParent(self.context)
+        bug_id = zapi.name(self.context)
+        dependents = []
+        for id, bug in tracker.items():
+            deps = IBugDependencies(bug)
+            if bug_id in deps.dependencies:
+                dependents.append(id)
+        return dependents
+
+    dependents = property(getDependents, setDependents)
+
+    def findChildren(self, recursive=True, all=None):
+        "See zopeproducts.bugtracker.interfaces.IBugDependencies"
+        if all is None:
+            all = []
+        tracker = zapi.getParent(self.context)
+        contextName = zapi.name(self.context)
+        deps = IBugDependencies(self.context)
+        children = []
+        for bugName in deps.dependencies:
+            # Circle detection; if the bugName was processed before, skip it
+            if bugName in all:
+                continue
+            else:
+                all.append(bugName)
+
+            bug = tracker[bugName]
+            if recursive is True:
+                deps = IBugDependencies(bug)
+                subs = deps.findChildren(all=all)
+            else:
+                subs = ()
+
+            children.append((bug, subs))
+
+        return tuple(children)
+
+class SearchableText:
+    """This adapter allows us to get all searchable text at once.""" 
+
+    implements(ISearchableText)
+    __used_for__ = IBug
+
+    def __init__(self, context):
+        self.context = context
+
+    def getSearchableText(self):
+        return [self.context.title, self.context.description]

Added: Zope3/trunk/src/bugtracker/comment.py
===================================================================
--- Zope3/trunk/src/bugtracker/comment.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/comment.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,34 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+# 
+##############################################################################
+"""A simple Comment
+
+$Id: comment.py,v 1.1 2003/07/24 18:08:03 srichter Exp $
+"""
+from persistent import Persistent
+
+from zope.interface import implements
+
+from zope.app.container.contained import Contained
+
+from bugtracker.interfaces import IComment
+from bugtracker.interfaces import IBugContained
+from bugtracker.renderable import RenderableText
+
+
+class Comment(Persistent, Contained):
+
+    implements(IComment, IBugContained)
+
+    # See zopeproducts.bugtracker.interfaces.IComment
+    body = RenderableText('')

Added: Zope3/trunk/src/bugtracker/configure.zcml
===================================================================
--- Zope3/trunk/src/bugtracker/configure.zcml	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/configure.zcml	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,388 @@
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
+   xmlns:mail="http://namespaces.zope.org/mail"
+   xmlns:i18n="http://namespaces.zope.org/i18n"
+   i18n_domain="bugtracker"
+   >
+
+  <!-- Security setup -->
+
+  <role
+      id="bugtracker.User"
+      title="Bug Tracker User"
+      description="The user can browse the bug tracker" />
+
+  <role
+      id="bugtracker.Editor"
+      title="Bug Tracker Editor"
+      description="The Bug Tracker editor can add and edit bugs." />
+
+  <role
+      id="bugtracker.Admin"
+      title="Bug Tracker Administrator"
+      description="The Admin can fully manage a bug tracker." />
+
+  <permission
+      id="bugtracker.ManageBugTracker"
+      title="Manage Bug Tracker"
+      description="Allows to change the settings of the Bug Tracker." />
+
+  <grant
+      permission="bugtracker.ManageBugTracker"
+      role="bugtracker.Admin" />
+
+  <permission
+      id="bugtracker.ViewBugTracker"
+      title="View Bug Tracker"
+      description="Allows to view the overview of the Bug Tracker." />
+
+  <grant
+      permission="bugtracker.ViewBugTracker"
+      role="bugtracker.User" />
+
+  <permission
+      id="bugtracker.ViewBug"
+      title="View Bug"
+      description="Allows to view the overview and dependencies of the Bug." />
+
+  <grant
+      permission="bugtracker.ViewBug"
+      role="bugtracker.User" />
+
+  <permission
+      id="bugtracker.AddBug"
+      title="Add Bug"
+      description="Allows to add a Bug to the Tracker." />
+
+  <grant
+      permission="bugtracker.AddBug"
+      role="bugtracker.User" />
+
+  <permission
+      id="bugtracker.EditBug"
+      title="Edit Bug"
+      description="Allows to edit the data of a Bug." />
+
+  <grant
+      permission="bugtracker.EditBug"
+      role="bugtracker.Editor" />
+
+  <permission
+      id="bugtracker.AddComment"
+      title="Add Bug Comment"
+      description="Allows to add comments to a bug." />
+
+  <grant
+      permission="bugtracker.AddComment"
+      role="bugtracker.User" />
+
+  <permission
+      id="bugtracker.AddAttachment"
+      title="Add Bug Attachment"
+      description="Allows to add attachments (files and images)  to a bug." />
+
+  <grant
+      permission="bugtracker.AddAttachment"
+      role="bugtracker.Editor" />
+
+  <!-- Setting up the vocabularies for the bug tracker -->  
+
+  <vocabulary
+     name="Stati"
+     factory=".vocabulary.StatusVocabulary" />
+
+  <vocabulary
+     name="Releases"
+     factory=".vocabulary.ReleaseVocabulary" />
+
+  <vocabulary
+     name="Priorities"
+     factory=".vocabulary.PriorityVocabulary" />
+
+  <vocabulary
+     name="BugTypes"
+     factory=".vocabulary.BugTypeVocabulary" />
+
+  <vocabulary
+     name="Users"
+     factory=".vocabulary.UserVocabulary" />
+
+  <content class=".vocabulary.ManagableVocabulary">
+    <allow interface="zope.schema.interfaces.IVocabulary"/>
+    <allow interface="zope.schema.interfaces.IVocabularyTokenized"/>
+    <allow attributes="default"/>
+    <require
+        permission="bugtracker.ManageBugTracker"
+        attributes="add delete"
+        set_attributes="default"/>
+  </content>
+
+  <content class=".vocabulary.StatusVocabulary">
+    <require like_class=".vocabulary.ManagableVocabulary"/>    
+  </content>
+
+  <content class=".vocabulary.PriorityVocabulary">
+    <require like_class=".vocabulary.ManagableVocabulary"/>    
+  </content>
+
+  <content class=".vocabulary.ReleaseVocabulary">
+    <require like_class=".vocabulary.ManagableVocabulary"/>    
+  </content>
+
+  <content class=".vocabulary.BugTypeVocabulary">
+    <require like_class=".vocabulary.ManagableVocabulary"/>    
+  </content>
+
+  <content class=".vocabulary.SimpleTerm">
+    <allow interface="zope.schema.interfaces.ITokenizedTerm"/>
+    <allow attributes="title"/>
+  </content>
+
+  <content class=".vocabulary.UserVocabulary">
+    <allow interface="zope.schema.interfaces.IVocabulary"/>
+    <allow interface="zope.schema.interfaces.IVocabularyTokenized"/>
+  </content>
+
+  <content class=".vocabulary.UserTerm">
+    <allow
+        interface="zope.schema.interfaces.ITokenizedTerm"/>
+    <allow attributes="principal title"/>
+  </content>
+
+  <!-- Bug Tracker related configuration -->
+
+  <interface 
+      interface=".interfaces.IBugTracker" 
+      type="zope.app.content.interfaces.IContentType"
+      /> 
+
+  <content class=".tracker.BugTracker">
+
+    <implements
+       interface="zope.app.annotation.interfaces.IAttributeAnnotatable" />
+
+    <factory
+        id="BugTracker"
+	title="Bug Tracker"
+        description="A Bug Tracker" />
+
+    <require
+        permission="bugtracker.ViewBugTracker"
+        interface="zope.app.container.interfaces.IReadContainer"/>
+
+    <require
+        permission="bugtracker.AddBug"
+        interface="zope.app.container.interfaces.IWriteContainer"/>
+
+    <require
+        permission="bugtracker.ViewBugTracker"
+        attributes="title" />
+
+    <require
+        permission="zope.ManageContent"
+        set_schema=".interfaces.IBugTracker" />
+
+  </content>
+
+  <adapter 
+      for=".interfaces.IBugTracker"
+      factory=".tracker.BugTrackerNameChooser"
+      provides="zope.app.container.interfaces.INameChooser" />
+
+  <adapter
+      factory=".vocabulary.StatusVocabulary"
+      provides=".interfaces.IStatusVocabulary"
+      for=".interfaces.IBugTracker" />
+
+  <adapter
+      factory=".vocabulary.ReleaseVocabulary"
+      provides=".interfaces.IReleaseVocabulary"
+      for=".interfaces.IBugTracker" />
+
+  <adapter
+      factory=".vocabulary.PriorityVocabulary"
+      provides=".interfaces.IPriorityVocabulary"
+      for=".interfaces.IBugTracker" />
+
+  <adapter
+      factory=".vocabulary.BugTypeVocabulary"
+      provides=".interfaces.IBugTypeVocabulary"
+      for=".interfaces.IBugTracker" />
+
+  <adapter
+      factory=".mail.MailSubscriptions"
+      provides=".interfaces.IMailSubscriptions"
+      for=".interfaces.IBugTracker" />
+
+  <!-- Bug related configuration -->
+
+  <interface 
+      interface=".interfaces.IBug" 
+      type="zope.app.content.interfaces.IContentType"
+      /> 
+
+  <content class=".bug.Bug">
+
+    <implements
+       interface="zope.app.annotation.interfaces.IAttributeAnnotatable"/>
+
+    <implements
+       interface="zope.app.container.interfaces.IContentContainer" />
+
+    <factory
+        id="Bug"
+        title="Bug"
+        description="A Bug" />
+
+    <require
+        permission="bugtracker.ViewBug"
+        interface="zope.app.container.interfaces.IReadContainer"/>
+
+    <require
+        permission="bugtracker.EditBug"
+        interface="zope.app.container.interfaces.IWriteContainer"/>
+
+    <require
+        permission="bugtracker.ViewBug"
+        interface=".interfaces.IBug" />
+
+    <require
+        permission="bugtracker.AddBug"
+        set_schema=".interfaces.IBug" />
+
+  </content>
+
+  <adapter 
+      factory=".bug.SearchableText"
+      provides=".interfaces.ISearchableText"
+      for=".interfaces.IBug" />
+
+  <content class=".bug.BugDependencyAdapter">
+
+    <require
+        permission="bugtracker.ViewBug"
+        attributes="dependencies dependents"/>
+
+    <require
+        permission="bugtracker.EditBug"
+        attributes="addDependencies deleteDependencies 
+                    addDependents deleteDependents" 
+        set_schema=".interfaces.IBugDependencies"/>
+
+  </content>
+
+  <adapter
+      factory=".bug.BugDependencyAdapter"
+      provides=".interfaces.IBugDependencies"
+      for=".interfaces.IBug" />
+
+  <adapter
+      factory=".mail.MailSubscriptions"
+      provides=".interfaces.IMailSubscriptions"
+      for=".interfaces.IBug" />
+
+
+  <!-- File and Image Attachment related configuration -->
+
+  <content class="zope.app.file.file.File">
+    <implements 
+        interface="bugtracker.interfaces.IAttachment" />
+  </content>
+
+  <content class="zope.app.file.image.Image">
+    <implements 
+        interface="bugtracker.interfaces.IAttachment" />
+  </content>
+
+
+  <!-- Comment related configuration -->
+
+  <interface 
+      interface=".interfaces.IComment" 
+      type="zope.app.content.interfaces.IContentType"
+      /> 
+
+  <content class=".comment.Comment">
+
+    <implements
+       interface="zope.app.annotation.interfaces.IAttributeAnnotatable"/>
+
+    <factory
+        id="BugComment"
+        title="Comment"
+        description="A comment about the bug." />
+
+    <require
+        permission="bugtracker.ViewBug"
+        interface=".interfaces.IComment" />
+
+    <require
+        permission="bugtracker.AddComment"
+        set_schema=".interfaces.IComment" />
+
+  </content>
+
+
+  <!-- XML-RPC presentation -->
+  <xmlrpc:view
+      name="methods"
+      for=".interfaces.IBugTracker"
+      permission="bugtracker.ViewBug" 
+      allowed_attributes="getBugNames addBug deleteBug"
+      class=".xmlrpc.BugTrackerMethods" />
+
+  <xmlrpc:defaultView
+      name="methods"
+      for=".interfaces.IBugTracker" />
+
+  <xmlrpc:view
+      name="methods"
+      for=".interfaces.IBug"
+      permission="bugtracker.ViewBug" 
+      allowed_attributes="getProperties setProperties 
+                       getCommentNames addComment deleteComment
+                       getAttachmentNames addAttachment deleteAttachment
+                       "
+      class=".xmlrpc.BugMethods" />
+
+  <xmlrpc:defaultView
+      name="methods"
+      for=".interfaces.IBug" />
+
+  <xmlrpc:view
+      name="methods"
+      for=".interfaces.IComment"
+      permission="bugtracker.ViewBug" 
+      allowed_attributes="getBody setBody"
+      class=".xmlrpc.CommentMethods" />
+
+  <xmlrpc:defaultView
+      name="methods"
+      for=".interfaces.IComment" />
+
+
+  <!-- Register Mailer and Mail Service -->
+
+  <mail:smtpMailer name="bugs-smtp" hostname="localhost" port="25" />
+
+  <mail:queuedDelivery 
+      permission="zope.SendMail"
+      queuePath="./mail-queue"
+      mailer="bugs-smtp"
+      name="bug-mailer"/>
+
+  <!-- Register event listener for change mails -->
+  <subscriber
+      factory=".mail.Mailer"
+      for="zope.app.event.interfaces.IObjectCreatedEvent
+           zope.app.event.interfaces.IObjectModifiedEvent" />
+
+
+  <!-- Register various browser related components, including all views -->
+  <include package=".browser" />
+
+  <!-- Translations -->
+  <i18n:registerTranslations directory="locales" />
+
+</configure>

Added: Zope3/trunk/src/bugtracker/exportimport.py
===================================================================
--- Zope3/trunk/src/bugtracker/exportimport.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/exportimport.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,362 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+# 
+##############################################################################
+"""XML Import/Export facility
+
+$Id: exportimport.py,v 1.6 2003/08/29 22:59:52 srichter Exp $
+"""
+VERSION = '1.0'
+
+import base64
+from xml.sax import parse
+from xml.sax.handler import ContentHandler
+
+from zope.exceptions import NotFoundError
+from zope.i18n.locales import locales
+from zope.publisher.browser import TestRequest
+from zope.schema.vocabulary import getVocabularyRegistry
+from zope.security.proxy import trustedRemoveSecurityProxy 
+
+from zope.app import zapi
+from zope.app.dublincore.interfaces import IZopeDublinCore
+from zope.app.file import File, Image
+from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
+
+from bugtracker.bug import Bug
+from bugtracker.comment import Comment
+from bugtracker.interfaces import IBugDependencies, IComment
+from bugtracker.renderable import RenderableText
+
+
+def _getPrincipalIdsByLogin(logins, vocab=None):
+    """Convert logins to principal id.
+
+    If no principal is found for a login, simply return the login name for
+    this entry. This way not all users must exist.
+    """
+    if vocab is None:
+        registry = getVocabularyRegistry()
+        vocab = registry.get(None, 'Users')
+
+    ids = []
+    for login in logins:
+        for term in vocab:
+            if term.principal['login'] == login:
+                ids.append(unicode(term.value))
+                break
+        else:
+            ids.append(unicode(login))
+
+    return ids
+    
+
+class XMLExport:
+
+    template = ViewPageTemplateFile('template.xml')
+
+    def __init__(self, tracker):
+        self.context = tracker
+        self.request = TestRequest()
+
+    def getXML(self):
+        return self.template()
+
+    def title(self):
+        return self.context.title        
+
+    def version(self):
+        return VERSION
+
+    def vocabularies(self):
+        registry = getVocabularyRegistry()
+        vocabs = []
+        for name in ('Stati', 'Priorities', 'BugTypes', 'Releases'):
+            vocab = registry.get(self.context, name)
+            vocabs.append({'name': name,
+                           'terms' : iter(vocab),
+                           'default' : vocab.default})
+        return vocabs
+
+    def bugs(self):
+        return map(lambda item: BugInfo(item[0], item[1]), self.context.items())
+            
+
+class BugInfo:
+
+    def __init__(self, name, bug):
+        self.context = bug
+        self.name = name
+        self.locale = locales.getLocale('en')
+
+    def id(self):
+        return self.name
+
+    def title(self):
+        return self.context.title
+        
+    def description(self):
+        return '\n'+self.context.description+'\n      '
+
+    def description_ttype(self):
+        return getattr(self.context.description, 'ttype',
+                       'zope.source.plaintext')
+
+    def submitter(self):
+        registry = getVocabularyRegistry()
+        vocab = registry.get(self.context, 'Users')
+        try:
+            return vocab.getTerm(self.context.submitter).principal['login']
+        except NotFoundError:
+            return self.context.submitter
+        
+    def created(self):
+        dc = IZopeDublinCore(self.context)
+        return self.locale.dates.getFormatter('dateTime', 'medium').format(
+            dc.created)
+
+    def modified(self):
+        dc = IZopeDublinCore(self.context)
+        if dc.modified is None:
+            return self.created()
+        return self.locale.dates.getFormatter('dateTime', 'medium').format(
+            dc.modified)
+
+    def status(self):
+        return self.context.status
+
+    def type(self):        
+        return self.context.type
+
+    def release(self):
+        return self.context.release
+
+    def priority(self):
+        return self.context.priority
+
+    def owners(self):
+        # Principal ids are totally useless for exporting; use logins instead
+        registry = getVocabularyRegistry()
+        vocab = registry.get(self.context, 'Users')
+        owners = []
+        for owner in self.context.owners:
+            try:
+                owners.append(vocab.getTerm(owner).principal['login'])
+            except NotFoundError:
+                owners.append(owner)
+        return ', '.join(owners)
+
+    def dependencies(self):
+        deps = IBugDependencies(self.context)
+        return ", ".join(deps.dependencies)
+
+    def comments(self):
+        comments = []
+        for name, obj in self.context.items():
+            if IComment.providedBy(obj):
+                comments.append(CommentInfo(name, obj))
+        return comments
+        
+    def attachments(self):
+        attachs = []
+        for name, obj in self.context.items():
+            if not IComment.providedBy(obj):
+                attachs.append(AttachmentInfo(name, obj))
+        return attachs
+    
+
+class CommentInfo:
+
+    def __init__(self, name, comment):
+        self.context = comment
+        self.name = name
+        self.locale = locales.getLocale('en')
+
+    def id(self):
+        return self.name
+
+    def body(self):
+        return '\n'+self.context.body+'\n        '
+
+    def ttype(self):
+        return getattr(self.context.body, 'ttype', 'zope.source.plaintext')
+
+    def creator(self):
+        dc = IZopeDublinCore(self.context)
+        registry = getVocabularyRegistry()
+        vocab = registry.get(self.context, 'Users')
+        return vocab.getTerm(dc.creators[0]).principal['login']
+        
+    def created(self):
+        dc = IZopeDublinCore(self.context)
+        return self.locale.dates.getFormatter('dateTime', 'medium').format(
+            dc.created)
+
+
+class AttachmentInfo:
+
+    def __init__(self, name, attach):
+        self.context = attach
+        self.name = name
+        self.locale = locales.getLocale('en')
+
+    def id(self):
+        return self.name
+
+    def type(self):
+        if isinstance(self.context, Image):
+            return 'Image'
+        return 'File'
+    
+    def data(self):
+        return base64.encodestring(self.context.data)
+
+    def creator(self):
+        dc = IZopeDublinCore(self.context)
+        registry = getVocabularyRegistry()
+        vocab = registry.get(self.context, 'Users')
+        return vocab.getTerm(dc.creators[0]).principal['login']
+        
+    def created(self):
+        dc = IZopeDublinCore(self.context)
+        return self.locale.dates.getFormatter('dateTime', 'medium').format(
+            dc.created)
+
+
+class XMLImporter(ContentHandler):
+
+    def __init__(self, context, encoding='latin-1'):
+        self.context = context
+        self.encoding = encoding
+        self.locale = locales.getLocale('en')
+        self.parser = self.locale.dates.getFormatter('dateTime', 'medium')
+        self.chars = u''
+
+    def startElement(self, name, attrs):
+        handler = getattr(self, 'start' + name.title().replace('-', ''), None)
+        if not handler:
+            raise ValueError, 'Unknown element %s' % name
+
+        handler(attrs)
+
+    def endElement(self, name):
+        handler = getattr(self, 'end' + name.title().replace('-', ''), None)
+        if handler:
+            handler()
+
+
+    def characters(self, content):
+        self.chars += content
+
+    def noop(*args):
+        pass
+
+    startVocabularies = noop
+    startBugs = noop
+    startComments = noop
+    startAttachments = noop
+    
+    def startBugtracker(self, attrs):
+        assert attrs.get('version') == VERSION
+        self.context.title = attrs.get('title')
+
+    def startVocabulary(self, attrs):
+        self.vocab_name = attrs.get('name', None)
+
+    def endVocabulary(self):
+        self.vocab_name = None
+
+    def startTerm(self, attrs):
+        registry = getVocabularyRegistry()
+        vocab = registry.get(self.context, self.vocab_name)
+        # XXX: I do not understand why my security does not work here.
+        vocab = trustedRemoveSecurityProxy(vocab)
+        vocab.add(attrs.get('value'), attrs.get('title'))
+        if attrs.get('default', None) is not None:
+            vocab.default = attrs.get('value')
+
+    def startBug(self, attrs):
+        registry = getVocabularyRegistry()
+        vocab = registry.get(self.context, 'Users')
+
+        bug = Bug()
+        bug.title = attrs.get('title')
+        bug.status = attrs.get('status')
+        bug.priority = attrs.get('priority')
+        bug.type = attrs.get('type')
+        bug.release = attrs.get('release')
+
+        logins = attrs.get('owners').split(', ')
+        logins = [login for login in logins if login.strip() != u'']
+        bug.owners = _getPrincipalIdsByLogin(logins)
+
+        deps_adapter = IBugDependencies(bug)
+        deps = attrs.get('dependencies').split(', ')
+        deps_adapter.setDependencies(filter(lambda o: o.strip() != u'', deps))
+        dc = IZopeDublinCore(bug)
+        dc.created = self.parser.parse(attrs.get('created'))
+        dc.modified = self.parser.parse(attrs.get('modified'))
+        dc.creators = _getPrincipalIdsByLogin([attrs.get('submitter')])
+        self.bug = bug
+        self.bug_name = attrs.get('id')
+
+    def endBug(self):
+        self.context[self.bug_name] = self.bug
+        
+    def startDescription(self, attrs):
+        self.chars = u''
+        self.description_ttype = attrs.get('ttype', 'zope.source.plaintext')
+
+    def endDescription(self):
+        self.bug.description = RenderableText(self.chars.strip(),
+                                              self.description_ttype)
+
+    def startComment(self, attrs):
+        self.chars = u''
+        comment = Comment()
+        dc = IZopeDublinCore(comment)
+        dc.created = self.parser.parse(attrs.get('created'))
+        dc.creators = _getPrincipalIdsByLogin([attrs.get('creator')])
+        self.comment = comment
+        self.comment_name = attrs.get('id')
+        self.comment_ttype = attrs.get('ttype', 'zope.source.plaintext')
+
+    def endComment(self):
+        self.comment.body = RenderableText(self.chars.strip(),
+                                           self.comment_ttype)
+        self.bug[self.comment_name] = self.comment
+
+    def startAttachment(self, attrs):
+        self.chars = u''
+        type = attrs.get('type')
+        if type == 'Image':
+            attach = Image()
+        else:
+            attach = File()
+        dc = IZopeDublinCore(attach)
+        dc.created = self.parser.parse(attrs.get('created'))
+        dc.creators = _getPrincipalIdsByLogin([attrs.get('creator')])
+        self.attach = attach
+        self.attach_name = attrs.get('id')
+
+    def endAttachment(self):
+        self.attach.data = base64.decodestring(self.chars.strip(' '))
+        self.bug[self.attach_name] = self.attach
+
+
+class XMLImport:
+
+    def __init__(self, tracker):
+        self.context = tracker
+
+    def processXML(self, xml):
+        parse(xml, XMLImporter(self.context))

Added: Zope3/trunk/src/bugtracker/interfaces.py
===================================================================
--- Zope3/trunk/src/bugtracker/interfaces.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/interfaces.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,253 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bug Tracker Interfaces
+
+Bag Tracker related interfaces.
+
+$Id$
+"""
+from zope.interface import Interface, implements
+from zope.schema import Text, TextLine, List, Dict, Choice, Field
+from zope.schema.interfaces import IText
+from zope.schema.interfaces import IVocabulary, IVocabularyTokenized
+
+from zope.app.container.constraints import ContainerTypesConstraint
+from zope.app.container.constraints import ItemTypePrecondition
+from zope.app.container.interfaces import IContainer, IContained
+from zope.app.container.interfaces import IContentContainer
+from zope.app.file.interfaces import IFile
+from bugtracker import TrackerMessageID as _
+
+
+class IBugTracker(IContainer, IContentContainer):
+    """A Bug Tracker object represents a collection of bugs for a particular
+    software or subject.
+
+    Note that there is no specific interface for managing bugs, since the
+    generic IContainer interface is sufficient.
+    """
+
+    title = TextLine(
+        title = _(u"Title"),
+        description = _(u"Title of the bug tracker."),
+        required=True)
+
+
+class IBugTrackerContained(IContained):
+    """Objects that can be contained by bug trackers should implement this
+    interface."""
+    __parent__ = Field(
+        constraint = ContainerTypesConstraint(IBugTracker))
+    
+
+class IBug(Interface):
+    """A bug is the content object containing all necessary information that
+    are relevant to a bug report.
+
+    Note: I also included title in the interface, since I think it is part of
+    the data and not the meta data of the bug content object.
+    """
+
+    title = TextLine(
+        title = _(u"Title"),
+        description = _(u"Title/Summary of the bug."),
+        required=True)
+
+    description = Text(
+        title = _(u"Description"),
+        description = _(u"Detailed Description of the bug."),
+        required=True)
+
+    submitter = TextLine(
+        title = _(u"Submitter"),
+        description = _(u"Name of the person that submitted the bug."),
+        required=False)
+
+    status = Choice(
+        title = _(u"Status"),
+        description = _(u"The current status of the bug."),
+        default= 'new',
+        required = True,
+        vocabulary = "Stati")
+
+    priority = Choice(
+        title = _(u"Priority"),
+        description = _(u"Specifies how urgent this bug is."),
+        default= 'normal',
+        required = True,
+        vocabulary = "Priorities")
+
+    type = Choice(
+        title = _(u"Type"),
+        description = _(u"Specifies of what nature the bug is."),
+        default= 'bug',
+        required = True,
+        vocabulary = "BugTypes")
+
+    release = Choice(
+        title = _(u"Release"),
+        description = _(u"Defines the release for which the bug is scheduled."),
+        default = 'None',
+        required = True,
+        vocabulary = "Releases")
+
+    owners = List(
+        title = _(u"Owners"),
+        description = _(u"List of people assigned as owners of the bug."),
+        required=False,
+        unique=True,
+        value_type=Choice(vocabulary = "Users"))
+
+
+class IBugContained(IContained):
+    """Objects that can be contained by Bugs should implement this
+    interface."""
+    __parent__ = Field(
+        constraint = ContainerTypesConstraint(IBug))
+
+
+class IBugContainer(IContainer):
+    """An object that contains bugs."""
+
+    def __setitem__(name, object):
+        """Add attachment."""
+
+    __setitem__.precondition = ItemTypePrecondition(IBug)
+
+
+class IBugDependencies(Interface):
+    """This object handles the dependencies of a bug."""
+    
+    def addDependencies(self, dependencies):
+        """Add the dependencies given to the existing list."""
+
+    def deleteDependencies(self, dependencies):
+        """Delete the dependencies given from the existing list."""
+
+    dependencies = List(
+        title = _(u"Dependencies"),
+        description = _(u"Other bugs this bug depends on."),
+        value_type = TextLine(title=_(u"Bug Id"),
+                              description=_(u"Bug Id.")),
+        required=False)
+
+    def addDependents(self, dependents):
+        """Add the dependents given to the existing list."""
+
+    def deleteDependents(self, dependents):
+        """Delete the dependents given from the existing list."""
+
+    dependents = List(
+        title = _(u"Dependents"),
+        description = _(u"Other bugs that depend on this one"),
+        value_type = TextLine(title=_(u"Bug Id"),
+                              description=_(u"Bug Id.")),
+        required=False)
+
+    def findChildren(recursive=True):
+        """Returns a list of children for this bug.
+
+        If the recursive is True, the method recurses into all children
+        returning the entire sub-graph of this Bug. Is the recursive
+        argument set to False, only the first level of children will be
+        returned.
+
+        While circular references are okay (since dependencies are general
+        graphs, not trees), this method must be aware of this fact and cannot
+        just implement a plain old recursive algorithm. When a circle is
+        detected, then simply cut off the search at this point.
+        """
+
+
+class IAttachment(Interface):
+    """A marker interface for objects that can serve as Bug attachments."""
+
+
+class IAttachmentContainer(IContainer, IContentContainer):
+    """An object that contains attachments, i.e. comments and files."""
+
+    def __setitem__(name, object):
+        """Add attachment."""
+
+    __setitem__.precondition = ItemTypePrecondition(IAttachment)
+
+
+class IComment(IAttachment):
+    """Simple comment for Bug.
+
+    For now we assume the body to be structured text.
+    """
+
+    body = Text(
+        title=_(u"Body"),
+        description=_(u"Renderable body of the Comment."),
+        default=u"",
+        required=True)
+
+
+class IManagableVocabulary(IVocabulary, IVocabularyTokenized):
+    """Vocabulary that can be modified by addign and deleting terms.
+
+    Note that this is a simple interface, where vocabularies are simple
+    value-title mappings. The values should be preferibly in ASCII, so that
+    there is no problem with encoding them in HTML.
+    """
+
+    def add(value, title):
+        """Add a new vocabulary entry."""
+
+    def delete(value):
+        """Delete an entry from the vocabulary."""
+
+    default = TextLine(
+        title = _(u"Default"),
+        description = _(u"Default value of the vocabulary."),
+        required=True)
+
+    
+class IStatusVocabulary(IManagableVocabulary):
+    """Manageable vocabulary that stores stati."""
+
+
+class IReleaseVocabulary(IManagableVocabulary):
+    """Manageable vocabulary that stores all releases."""
+
+
+class IPriorityVocabulary(IManagableVocabulary):
+    """Manageable vocabulary that stores all priority values."""
+
+
+class IBugTypeVocabulary(IManagableVocabulary):
+    """Manageable vocabulary that stores all type values."""
+
+
+class IMailSubscriptions(Interface):
+    """This interface allows you to retrieve a list of E-mails for
+    mailings."""
+
+    def getSubscriptions():
+        """Return a list of E-mails."""
+
+    def addSubscriptions(emails):
+        """Add a bunch of subscriptions; one would be okay too."""
+
+    def removeSubscriptions(emails):
+        """Remove a set of subscriptions."""
+
+
+# XXX: Remove once index is back
+class ISearchableText(Interface):
+
+    def getSearchableText():
+        """Return searchable text."""

Added: Zope3/trunk/src/bugtracker/locales/bugtracker.pot
===================================================================
--- Zope3/trunk/src/bugtracker/locales/bugtracker.pot	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/locales/bugtracker.pot	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,611 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+msgid ""
+msgstr ""
+"Project-Id-Version: Development/Revision: 26086\n"
+"POT-Creation-Date: Tue Jul  6 13:12:02 2004\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
+"Language-Team: Zope 3 Developers <zope3-dev at zope.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/locales/extract.py\n"
+
+#: src/bugtracker/browser/bug_edit.pt:33
+#: src/bugtracker/browser/bug_overview.pt:17
+msgid "From ${user} at ${date}"
+msgstr ""
+
+#: src/bugtracker/browser/bug_edit.pt:39
+#: src/bugtracker/browser/bug_listing_normal.pt:24
+msgid "Type:"
+msgstr ""
+
+#: src/bugtracker/browser/bug_edit.pt:42
+#: src/bugtracker/browser/bug_listing_normal.pt:18
+msgid "Status:"
+msgstr ""
+
+#: src/bugtracker/browser/bug_edit.pt:48
+#: src/bugtracker/browser/bug_listing_normal.pt:21
+#: src/bugtracker/browser/bug_overview.pt:33
+msgid "Priority:"
+msgstr ""
+
+#: src/bugtracker/browser/bug_edit.pt:51
+#: src/bugtracker/browser/bug_overview.pt:38
+msgid "Release Target:"
+msgstr ""
+
+#: src/bugtracker/browser/bug_edit.pt:57
+#: src/bugtracker/browser/bug_overview.pt:43
+msgid "Owners:"
+msgstr ""
+
+#: src/bugtracker/browser/bug_listing_normal.pt:10
+msgid "Bug #${bug_id} - ${bug_title}"
+msgstr ""
+
+#: src/bugtracker/browser/bug_listing_normal.pt:37
+msgid "Posted by ${submitter} on ${created_date} - ${num_comments} comments"
+msgstr ""
+
+#: src/bugtracker/browser/bug_overview.pt:63
+msgid "Direct Dependencies"
+msgstr ""
+
+#: src/bugtracker/browser/bug_overview.pt:77
+msgid "Attachments"
+msgstr ""
+
+#: src/bugtracker/browser/bug_overview.pt:86
+msgid "Add File"
+msgstr ""
+
+#: src/bugtracker/browser/bug_overview.pt:88
+msgid "Add Image"
+msgstr ""
+
+#: src/bugtracker/browser/bug_overview.pt:92
+msgid "Comments"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:142
+msgid "Add Dependent Bug"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:173
+msgid "Change Bug"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:173
+#: src/bugtracker/browser/configure.zcml:260
+msgid "Edit"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:236
+#: src/bugtracker/browser/bug_overview.pt:103
+msgid "Add Comment"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:252
+msgid "Bug Comment"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:252
+msgid "A Comment"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:260
+msgid "Change Comment"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:29
+msgid "Add Bug Tracker"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:58
+#: src/bugtracker/browser/configure.zcml:203
+msgid "Overview"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:64
+msgid "Bug Tracker - Overview"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:77
+msgid "Settings"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:84
+#: src/bugtracker/browser/configure.zcml:216
+msgid "Subscriptions"
+msgstr ""
+
+#: src/bugtracker/browser/configure.zcml:96
+msgid "XML Export/Import"
+msgstr ""
+
+#: src/bugtracker/browser/dependencies.pt:106
+msgid "Dependency Statistics"
+msgstr ""
+
+#: src/bugtracker/browser/dependencies.pt:112
+msgid "Closed/Deferred Bugs:"
+msgstr ""
+
+#: src/bugtracker/browser/dependencies.pt:120
+msgid "New (unseen) Bugs:"
+msgstr ""
+
+#: src/bugtracker/browser/dependencies.pt:128
+msgid "Opened/Assigned Bugs:"
+msgstr ""
+
+#: src/bugtracker/browser/dependencies.pt:137
+msgid "Dependency Tree"
+msgstr ""
+
+#: src/bugtracker/browser/dependencies.pt:25
+msgid "Dependencies - Bugs that have to be completed before this bug can be closed."
+msgstr ""
+
+#: src/bugtracker/browser/dependencies.pt:27
+msgid "Dependents - This bug has to be completed in before the Dependents can be closed."
+msgstr ""
+
+#: src/bugtracker/browser/dependencies.pt:40
+msgid "Available Bugs"
+msgstr ""
+
+#: src/bugtracker/browser/dependencies.pt:91
+# Default: "Collapse"
+msgid "collapse-button"
+msgstr ""
+
+#: src/bugtracker/browser/dependencies.pt:95
+# Default: "Expand"
+msgid "expand-button"
+msgstr ""
+
+#: src/bugtracker/browser/exportimport.pt:14
+msgid "Export XML"
+msgstr ""
+
+#: src/bugtracker/browser/exportimport.pt:16
+msgid "Simply click <a href=\"./tracker.xml\">here</a>."
+msgstr ""
+
+#: src/bugtracker/browser/exportimport.pt:18
+msgid "Import XML"
+msgstr ""
+
+#: src/bugtracker/browser/exportimport.pt:23
+msgid "XML File"
+msgstr ""
+
+#: src/bugtracker/browser/exportimport.pt:29
+# Default: "Import"
+msgid "import-button"
+msgstr ""
+
+#: src/bugtracker/browser/legend.pt:13
+msgid "Critial"
+msgstr ""
+
+#: src/bugtracker/browser/legend.pt:2
+msgid "Status Markup"
+msgstr ""
+
+#: src/bugtracker/browser/legend.pt:9
+msgid "Priority Markup"
+msgstr ""
+
+#: src/bugtracker/browser/mail.py:31
+msgid "Subscribers successfully added: $emails"
+msgstr ""
+
+#: src/bugtracker/browser/mail.py:38
+msgid "Subscribers successfully deleted: $emails"
+msgstr ""
+
+#: src/bugtracker/browser/skin/template.pt:168
+msgid "You are logged in as ${user_title}."
+msgstr ""
+
+#: src/bugtracker/browser/skin/template.pt:231
+msgid "Powered by Zope 3. Written by Stephan Richter in 2003."
+msgstr ""
+
+#: src/bugtracker/browser/skin/template.pt:92
+msgid "&nbsp;Bug Tracker"
+msgstr ""
+
+#: src/bugtracker/browser/subscriptions.pt:12
+msgid "Current Subscriptions"
+msgstr ""
+
+#: src/bugtracker/browser/subscriptions.pt:20
+# Default: "Remove"
+msgid "remove-button"
+msgstr ""
+
+#: src/bugtracker/browser/subscriptions.pt:27
+msgid "Enter new Users (separate by 'Return')"
+msgstr ""
+
+#: src/bugtracker/browser/subscriptions.pt:37
+# Default: "Refresh"
+msgid "refresh-button"
+msgstr ""
+
+#: src/bugtracker/browser/subscriptions.pt:39
+# Default: "Add"
+msgid "add-button"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:50
+#: src/bugtracker/browser/legend.pt:3
+msgid "New"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:51
+#: src/bugtracker/browser/legend.pt:4
+msgid "Open"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:52
+#: src/bugtracker/browser/legend.pt:5
+msgid "Assigned"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:53
+#: src/bugtracker/browser/legend.pt:6
+msgid "Deferred"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:54
+#: src/bugtracker/browser/legend.pt:7
+msgid "Closed"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:56
+#: src/bugtracker/configure.zcml:233
+#: src/bugtracker/browser/configure.zcml:134
+msgid "Bug"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:57
+msgid "Feature"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:58
+#: src/bugtracker/interfaces.py:99
+msgid "Release"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:60
+msgid "(not specified)"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:62
+#: src/bugtracker/browser/legend.pt:10
+msgid "Low"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:63
+#: src/bugtracker/browser/legend.pt:11
+msgid "Normal"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:64
+#: src/bugtracker/browser/legend.pt:12
+msgid "Urgent"
+msgstr ""
+
+#: src/bugtracker/browser/tracker.py:65
+msgid "Critical"
+msgstr ""
+
+#: src/bugtracker/browser/tracker_overview.pt:106
+#: src/bugtracker/browser/tracker_overview.pt:146
+msgid "Previous (${start_number} to ${end_number})"
+msgstr ""
+
+#: src/bugtracker/browser/tracker_overview.pt:113
+#: src/bugtracker/browser/tracker_overview.pt:153
+msgid "${start_number} to ${end_number} of ${batch_total_number} found (${bug_number} total)"
+msgstr ""
+
+#: src/bugtracker/browser/tracker_overview.pt:123
+#: src/bugtracker/browser/tracker_overview.pt:163
+msgid "Next (${start_number} to ${end_number})"
+msgstr ""
+
+#: src/bugtracker/browser/tracker_overview.pt:92
+#: src/bugtracker/browser/tracker_overview.pt:171
+# Default: "Add Bug"
+msgid "add-bug-button"
+msgstr ""
+
+#: src/bugtracker/browser/tracker_settings.pt:14
+msgid "Existing Values:"
+msgstr ""
+
+#: src/bugtracker/browser/tracker_settings.pt:29
+msgid "Default Value:"
+msgstr ""
+
+#: src/bugtracker/bug.py:45
+#: src/bugtracker/bug.py:46
+msgid "Stati"
+msgstr ""
+
+#: src/bugtracker/bug.py:50
+#: src/bugtracker/bug.py:51
+msgid "Priorities"
+msgstr ""
+
+#: src/bugtracker/bug.py:55
+#: src/bugtracker/bug.py:56
+msgid "BugTypes"
+msgstr ""
+
+#: src/bugtracker/bug.py:60
+#: src/bugtracker/bug.py:61
+msgid "Releases"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:11
+msgid "Bug Tracker User"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:11
+msgid "The user can browse the bug tracker"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:16
+msgid "Bug Tracker Editor"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:16
+msgid "The Bug Tracker editor can add and edit bugs."
+msgstr ""
+
+#: src/bugtracker/configure.zcml:165
+#: src/bugtracker/browser/configure.zcml:39
+msgid "A Bug Tracker"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:165
+#: src/bugtracker/browser/configure.zcml:39
+msgid "Bug Tracker"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:21
+msgid "The Admin can fully manage a bug tracker."
+msgstr ""
+
+#: src/bugtracker/configure.zcml:21
+msgid "Bug Tracker Administrator"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:233
+#: src/bugtracker/browser/configure.zcml:134
+msgid "A Bug"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:26
+msgid "Manage Bug Tracker"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:26
+msgid "Allows to change the settings of the Bug Tracker."
+msgstr ""
+
+#: src/bugtracker/configure.zcml:311
+msgid "Comment"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:311
+msgid "A comment about the bug."
+msgstr ""
+
+#: src/bugtracker/configure.zcml:35
+msgid "Allows to view the overview of the Bug Tracker."
+msgstr ""
+
+#: src/bugtracker/configure.zcml:35
+msgid "View Bug Tracker"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:44
+msgid "View Bug"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:44
+msgid "Allows to view the overview and dependencies of the Bug."
+msgstr ""
+
+#: src/bugtracker/configure.zcml:53
+msgid "Allows to add a Bug to the Tracker."
+msgstr ""
+
+#: src/bugtracker/configure.zcml:53
+#: src/bugtracker/browser/configure.zcml:109
+#: src/bugtracker/browser/bug_overview.pt:73
+msgid "Add Bug"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:62
+msgid "Allows to edit the data of a Bug."
+msgstr ""
+
+#: src/bugtracker/configure.zcml:62
+msgid "Edit Bug"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:71
+msgid "Allows to add comments to a bug."
+msgstr ""
+
+#: src/bugtracker/configure.zcml:71
+msgid "Add Bug Comment"
+msgstr ""
+
+#: src/bugtracker/configure.zcml:80
+msgid "Allows to add attachments (files and images)  to a bug."
+msgstr ""
+
+#: src/bugtracker/configure.zcml:80
+msgid "Add Bug Attachment"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:100
+msgid "Defines the release for which the bug is scheduled."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:106
+msgid "Owners"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:107
+msgid "List of people assigned as owners of the bug."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:139
+#: src/bugtracker/browser/configure.zcml:211
+#: src/bugtracker/browser/dependencies.pt:59
+#: src/bugtracker/browser/dependencies.pt:65
+msgid "Dependencies"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:140
+msgid "Other bugs this bug depends on."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:141
+#: src/bugtracker/interfaces.py:154
+msgid "Bug Id"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:142
+#: src/bugtracker/interfaces.py:155
+msgid "Bug Id."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:152
+#: src/bugtracker/browser/dependencies.pt:60
+#: src/bugtracker/browser/dependencies.pt:64
+msgid "Dependents"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:153
+msgid "Other bugs that depend on this one"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:193
+msgid "Body"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:194
+msgid "Renderable body of the Comment."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:214
+msgid "Default"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:215
+msgid "Default value of the vocabulary."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:42
+#: src/bugtracker/interfaces.py:63
+msgid "Title"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:43
+msgid "Title of the bug tracker."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:64
+msgid "Title/Summary of the bug."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:68
+msgid "Description"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:69
+msgid "Detailed Description of the bug."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:73
+msgid "Submitter"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:74
+msgid "Name of the person that submitted the bug."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:78
+msgid "Status"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:79
+msgid "The current status of the bug."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:85
+msgid "Priority"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:86
+msgid "Specifies how urgent this bug is."
+msgstr ""
+
+#: src/bugtracker/interfaces.py:92
+msgid "Type"
+msgstr ""
+
+#: src/bugtracker/interfaces.py:93
+msgid "Specifies of what nature the bug is."
+msgstr ""
+
+#: src/bugtracker/vocabulary.py:120
+msgid "The value '${value}' was not found in the vocabulary"
+msgstr ""
+
+#: src/bugtracker/vocabulary.py:136
+msgid "Status Definitions"
+msgstr ""
+
+#: src/bugtracker/vocabulary.py:146
+msgid "Release Definitions"
+msgstr ""
+
+#: src/bugtracker/vocabulary.py:156
+msgid "Priority Definitions"
+msgstr ""
+
+#: src/bugtracker/vocabulary.py:166
+msgid "Bug Type Definitions"
+msgstr ""
+
+#: src/bugtracker/vocabulary.py:97
+msgid "Cannot delete default value '${value}'."
+msgstr ""
+

Added: Zope3/trunk/src/bugtracker/locales/de/LC_MESSAGES/bugtracker.mo
===================================================================
(Binary files differ)


Property changes on: Zope3/trunk/src/bugtracker/locales/de/LC_MESSAGES/bugtracker.mo
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: Zope3/trunk/src/bugtracker/locales/de/LC_MESSAGES/bugtracker.po
===================================================================
--- Zope3/trunk/src/bugtracker/locales/de/LC_MESSAGES/bugtracker.po	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/locales/de/LC_MESSAGES/bugtracker.po	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,592 @@
+# translation of bugtracker.po to German
+# #############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+# #############################################################################
+# Stephan Richter <stephan.richter at tufts.edu>, 2003, 2004.
+msgid ""
+msgstr ""
+"Project-Id-Version: bugtracker\n"
+"POT-Creation-Date: Tue Jul  6 13:12:02 2004\n"
+"PO-Revision-Date: 2004-07-06 13:27-0400\n"
+"Last-Translator: Stephan Richter <stephan.richter at tufts.edu>\n"
+"Language-Team: German <zope3-dev at zope.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: zope/app/translation_files/extract.py\n"
+"X-Generator: KBabel 1.9\n"
+
+#: src/bugtracker/browser/bug_edit.pt:33
+#: src/bugtracker/browser/bug_overview.pt:17
+msgid "From ${user} at ${date}"
+msgstr "Von ${user} um ${date}"
+
+#: src/bugtracker/browser/bug_edit.pt:39
+#: src/bugtracker/browser/bug_listing_normal.pt:24
+msgid "Type:"
+msgstr "Typ:"
+
+#: src/bugtracker/browser/bug_edit.pt:42
+#: src/bugtracker/browser/bug_listing_normal.pt:18
+msgid "Status:"
+msgstr "Status:"
+
+#: src/bugtracker/browser/bug_edit.pt:48
+#: src/bugtracker/browser/bug_listing_normal.pt:21
+#: src/bugtracker/browser/bug_overview.pt:33
+msgid "Priority:"
+msgstr "Priorität:"
+
+#: src/bugtracker/browser/bug_edit.pt:51
+#: src/bugtracker/browser/bug_overview.pt:38
+msgid "Release Target:"
+msgstr "Zielversion:"
+
+#: src/bugtracker/browser/bug_edit.pt:57
+#: src/bugtracker/browser/bug_overview.pt:43
+msgid "Owners:"
+msgstr "Besitzer:"
+
+#: src/bugtracker/browser/bug_listing_normal.pt:10
+msgid "Bug #${bug_id} - ${bug_title}"
+msgstr "Fehler #${bug_id} - ${bug_title}"
+
+#: src/bugtracker/browser/bug_listing_normal.pt:37
+msgid "Posted by ${submitter} on ${created_date} - ${num_comments} comments"
+msgstr "Erstellt von ${submitter} am ${created_date} - ${num_comments} Kommentare"
+
+#: src/bugtracker/browser/bug_overview.pt:63
+msgid "Direct Dependencies"
+msgstr "Direkte Abhängigkeiten"
+
+#: src/bugtracker/browser/bug_overview.pt:77
+msgid "Attachments"
+msgstr "Anhänge"
+
+#: src/bugtracker/browser/bug_overview.pt:86
+msgid "Add File"
+msgstr "Datei hinzufügen"
+
+#: src/bugtracker/browser/bug_overview.pt:88
+msgid "Add Image"
+msgstr "Bild hinzufügen"
+
+#: src/bugtracker/browser/bug_overview.pt:92
+msgid "Comments"
+msgstr "Kommentare"
+
+#: src/bugtracker/browser/configure.zcml:142
+msgid "Add Dependent Bug"
+msgstr "Abhängigen Fehler hinzufügen"
+
+#: src/bugtracker/browser/configure.zcml:173
+msgid "Change Bug"
+msgstr "Bearbeite Fehler"
+
+#: src/bugtracker/browser/configure.zcml:173
+#: src/bugtracker/browser/configure.zcml:260
+msgid "Edit"
+msgstr "Bearbeiten"
+
+#: src/bugtracker/browser/configure.zcml:236
+#: src/bugtracker/browser/bug_overview.pt:103
+msgid "Add Comment"
+msgstr "Kommentar hinzufügen"
+
+#: src/bugtracker/browser/configure.zcml:252
+msgid "Bug Comment"
+msgstr "Fehlerkommentar"
+
+#: src/bugtracker/browser/configure.zcml:252
+msgid "A Comment"
+msgstr "Ein Kommentar"
+
+#: src/bugtracker/browser/configure.zcml:260
+msgid "Change Comment"
+msgstr "Kommentar bearbeiten"
+
+#: src/bugtracker/browser/configure.zcml:29
+msgid "Add Bug Tracker"
+msgstr "Fehlerverwalter hinzufügen"
+
+#: src/bugtracker/browser/configure.zcml:58
+#: src/bugtracker/browser/configure.zcml:203
+msgid "Overview"
+msgstr "Ãœbersicht"
+
+#: src/bugtracker/browser/configure.zcml:64
+msgid "Bug Tracker - Overview"
+msgstr "Fehlerverwalter - Ãœberblick"
+
+#: src/bugtracker/browser/configure.zcml:77
+msgid "Settings"
+msgstr "Einstellungen"
+
+#: src/bugtracker/browser/configure.zcml:84
+#: src/bugtracker/browser/configure.zcml:216
+msgid "Subscriptions"
+msgstr "Abonnements"
+
+#: src/bugtracker/browser/configure.zcml:96
+msgid "XML Export/Import"
+msgstr "XML Exportieren/Importieren"
+
+#: src/bugtracker/browser/dependencies.pt:106
+msgid "Dependency Statistics"
+msgstr "Abhängigkeitsstatistiken"
+
+#: src/bugtracker/browser/dependencies.pt:112
+msgid "Closed/Deferred Bugs:"
+msgstr "Geschlossene/Verschobene Fehler:"
+
+#: src/bugtracker/browser/dependencies.pt:120
+msgid "New (unseen) Bugs:"
+msgstr "Neue (unangesehene) Fehler:"
+
+#: src/bugtracker/browser/dependencies.pt:128
+msgid "Opened/Assigned Bugs:"
+msgstr "Offene/zugewiesende Fehler:"
+
+#: src/bugtracker/browser/dependencies.pt:137
+msgid "Dependency Tree"
+msgstr "Abhängikeitsbaum"
+
+#: src/bugtracker/browser/dependencies.pt:25
+msgid "Dependencies - Bugs that have to be completed before this bug can be closed."
+msgstr ""
+"Abhängigkeiten - In der folgenden Auswahlbox können sie die Fehler aussuchen die behoben "
+"werden müssen, <em>bevor</em> dieser Fehler als behoben markiert werden kann."
+
+#: src/bugtracker/browser/dependencies.pt:27
+msgid ""
+"Dependents - This bug has to be completed in before the Dependents can be "
+"closed."
+msgstr "Abhängige - Dieser Fehler muss behoben werden bevor einer der aufgeführten Fehler geschlossen werden kann."
+
+#: src/bugtracker/browser/dependencies.pt:40
+msgid "Available Bugs"
+msgstr "Verfügbare Fehler"
+
+#: src/bugtracker/browser/dependencies.pt:91
+msgid "collapse-button"
+msgstr "Zuklappen"
+
+#: src/bugtracker/browser/dependencies.pt:95
+msgid "expand-button"
+msgstr "Aufklappen"
+
+#: src/bugtracker/browser/exportimport.pt:14
+msgid "Export XML"
+msgstr "XML Exportieren"
+
+#: src/bugtracker/browser/exportimport.pt:16
+msgid "Simply click <a href=\"./tracker.xml\">here</a>."
+msgstr "Einfach <a href=\"./tracker.xml\">hier</a> klicken."
+
+#: src/bugtracker/browser/exportimport.pt:18
+msgid "Import XML"
+msgstr "XML Importieren"
+
+#: src/bugtracker/browser/exportimport.pt:23
+msgid "XML File"
+msgstr "XML-Datei"
+
+#: src/bugtracker/browser/exportimport.pt:29
+msgid "import-button"
+msgstr "Importieren"
+
+#: src/bugtracker/browser/legend.pt:13
+msgid "Critial"
+msgstr "Kritisch"
+
+#: src/bugtracker/browser/legend.pt:2
+msgid "Status Markup"
+msgstr "Status-Markup"
+
+#: src/bugtracker/browser/legend.pt:9
+msgid "Priority Markup"
+msgstr "Prioritäts-Markup"
+
+#: src/bugtracker/browser/mail.py:31
+msgid "Subscribers successfully added: $emails"
+msgstr "Abonnenten erfolgreich hinzugefügt: $emails"
+
+#: src/bugtracker/browser/mail.py:38
+msgid "Subscribers successfully deleted: $emails"
+msgstr "Abonnenten erfolgreich gelöscht: $emails"
+
+#: src/bugtracker/browser/skin/template.pt:168
+msgid "You are logged in as ${user_title}."
+msgstr "Sie sind als ${user_title} angemeldet."
+
+#: src/bugtracker/browser/skin/template.pt:231
+msgid "Powered by Zope 3. Written by Stephan Richter in 2003."
+msgstr "Von Zope 3 angetrieben. Geschrieben von Stephan Richter in 2003."
+
+#: src/bugtracker/browser/skin/template.pt:92
+msgid "&nbsp;Bug Tracker"
+msgstr "&nbsp;Fehlerverwalter"
+
+#: src/bugtracker/browser/subscriptions.pt:12
+msgid "Current Subscriptions"
+msgstr "Derzeitige Abonnements"
+
+#: src/bugtracker/browser/subscriptions.pt:20
+msgid "remove-button"
+msgstr "Entfernen"
+
+#: src/bugtracker/browser/subscriptions.pt:27
+msgid "Enter new Users (separate by 'Return')"
+msgstr "Neue Benutzer eingeben (mit 'Return' getrennt)"
+
+#: src/bugtracker/browser/subscriptions.pt:37
+msgid "refresh-button"
+msgstr "Auffrischen"
+
+#: src/bugtracker/browser/subscriptions.pt:39
+msgid "add-button"
+msgstr "Hinzufügen"
+
+#: src/bugtracker/browser/tracker.py:50 src/bugtracker/browser/legend.pt:3
+msgid "New"
+msgstr "Neu"
+
+#: src/bugtracker/browser/tracker.py:51 src/bugtracker/browser/legend.pt:4
+msgid "Open"
+msgstr "Offen"
+
+#: src/bugtracker/browser/tracker.py:52 src/bugtracker/browser/legend.pt:5
+msgid "Assigned"
+msgstr "Zugewiesen"
+
+#: src/bugtracker/browser/tracker.py:53 src/bugtracker/browser/legend.pt:6
+msgid "Deferred"
+msgstr "Verschoben"
+
+#: src/bugtracker/browser/tracker.py:54 src/bugtracker/browser/legend.pt:7
+msgid "Closed"
+msgstr "Geschlossen"
+
+#: src/bugtracker/browser/tracker.py:56 src/bugtracker/configure.zcml:233
+#: src/bugtracker/browser/configure.zcml:134
+msgid "Bug"
+msgstr "Fehler"
+
+#: src/bugtracker/browser/tracker.py:57
+msgid "Feature"
+msgstr "Funktion"
+
+#: src/bugtracker/browser/tracker.py:58 src/bugtracker/interfaces.py:99
+msgid "Release"
+msgstr "Version"
+
+#: src/bugtracker/browser/tracker.py:60
+msgid "(not specified)"
+msgstr "(nicht spezifiziert)"
+
+#: src/bugtracker/browser/tracker.py:62 src/bugtracker/browser/legend.pt:10
+msgid "Low"
+msgstr "Niedrig"
+
+#: src/bugtracker/browser/tracker.py:63 src/bugtracker/browser/legend.pt:11
+msgid "Normal"
+msgstr "Normal"
+
+#: src/bugtracker/browser/tracker.py:64 src/bugtracker/browser/legend.pt:12
+msgid "Urgent"
+msgstr "Dringend"
+
+#: src/bugtracker/browser/tracker.py:65
+msgid "Critical"
+msgstr "Kritisch"
+
+#: src/bugtracker/browser/tracker_overview.pt:106
+#: src/bugtracker/browser/tracker_overview.pt:146
+msgid "Previous (${start_number} to ${end_number})"
+msgstr "Vorherige (${start_number} bis ${end_number})"
+
+#: src/bugtracker/browser/tracker_overview.pt:113
+#: src/bugtracker/browser/tracker_overview.pt:153
+msgid ""
+"${start_number} to ${end_number} of ${batch_total_number} found "
+"(${bug_number} total)"
+msgstr ""
+"${start_number} bis ${end_number} von ${batch_total_number} gefunden "
+"(${bug_number} total)"
+
+#: src/bugtracker/browser/tracker_overview.pt:123
+#: src/bugtracker/browser/tracker_overview.pt:163
+msgid "Next (${start_number} to ${end_number})"
+msgstr "Nächste (${start_number} bis ${end_number})"
+
+#: src/bugtracker/browser/tracker_overview.pt:92
+#: src/bugtracker/browser/tracker_overview.pt:171
+msgid "add-bug-button"
+msgstr "Fehler hinzufügen"
+
+#: src/bugtracker/browser/tracker_settings.pt:14
+msgid "Existing Values:"
+msgstr "Existierende Werte:"
+
+#: src/bugtracker/browser/tracker_settings.pt:29
+msgid "Default Value:"
+msgstr "Standardwert:"
+
+#: src/bugtracker/bug.py:45 src/bugtracker/bug.py:46
+msgid "Stati"
+msgstr "Stati"
+
+#: src/bugtracker/bug.py:50 src/bugtracker/bug.py:51
+msgid "Priorities"
+msgstr "Prioritäten"
+
+#: src/bugtracker/bug.py:55 src/bugtracker/bug.py:56
+msgid "BugTypes"
+msgstr "Fehlertypen"
+
+#: src/bugtracker/bug.py:60 src/bugtracker/bug.py:61
+msgid "Releases"
+msgstr "Versionen"
+
+#: src/bugtracker/configure.zcml:11
+msgid "Bug Tracker User"
+msgstr "Fehlerverwalter-Nutzer"
+
+#: src/bugtracker/configure.zcml:11
+msgid "The user can browse the bug tracker"
+msgstr "Dieser Nutzer kann den Bug Tracker ansehen."
+
+#: src/bugtracker/configure.zcml:16
+msgid "Bug Tracker Editor"
+msgstr "Fehlerverwalter Editor"
+
+#: src/bugtracker/configure.zcml:16
+msgid "The Bug Tracker editor can add and edit bugs."
+msgstr "Der Fehlerverwalter Editor can Fehler hinzufügen und bearbeiten."
+
+#: src/bugtracker/configure.zcml:165 src/bugtracker/browser/configure.zcml:39
+msgid "A Bug Tracker"
+msgstr "Ein Fehlerverwalter"
+
+#: src/bugtracker/configure.zcml:165 src/bugtracker/browser/configure.zcml:39
+msgid "Bug Tracker"
+msgstr "Fehlerverwalter"
+
+#: src/bugtracker/configure.zcml:21
+msgid "The Admin can fully manage a bug tracker."
+msgstr "Der Administrator kann vollständig den Fehlerverwalter verwalten."
+
+#: src/bugtracker/configure.zcml:21
+msgid "Bug Tracker Administrator"
+msgstr "Fehlerverwalter-Administrator"
+
+#: src/bugtracker/configure.zcml:233 src/bugtracker/browser/configure.zcml:134
+msgid "A Bug"
+msgstr "Ein Fehler"
+
+#: src/bugtracker/configure.zcml:26
+msgid "Manage Bug Tracker"
+msgstr "Fehlerverwalter verwalten"
+
+#: src/bugtracker/configure.zcml:26
+msgid "Allows to change the settings of the Bug Tracker."
+msgstr "Erlaubt es Einstellungen der Fehlerverwalters zu ändern."
+
+#: src/bugtracker/configure.zcml:311
+msgid "Comment"
+msgstr "Kommentar"
+
+#: src/bugtracker/configure.zcml:311
+msgid "A comment about the bug."
+msgstr "Ein Kommentar über diesen Fehler."
+
+#: src/bugtracker/configure.zcml:35
+msgid "Allows to view the overview of the Bug Tracker."
+msgstr "Erlaubt es den Ãœberblick des Fehlerverwalters anzusehen."
+
+#: src/bugtracker/configure.zcml:35
+msgid "View Bug Tracker"
+msgstr "Fehlerverwalter ansehen"
+
+#: src/bugtracker/configure.zcml:44
+msgid "View Bug"
+msgstr "Fehler ansehen"
+
+#: src/bugtracker/configure.zcml:44
+msgid "Allows to view the overview and dependencies of the Bug."
+msgstr "Erlaubt es die Übersicht und Abhängigkeiten des Fehlers anzusehen."
+
+#: src/bugtracker/configure.zcml:53
+msgid "Allows to add a Bug to the Tracker."
+msgstr "Erlaubt es einen Fehler zum Verwalter hinzuzufügen."
+
+#: src/bugtracker/configure.zcml:53 src/bugtracker/browser/configure.zcml:109
+#: src/bugtracker/browser/bug_overview.pt:73
+msgid "Add Bug"
+msgstr "Fehler hinzufügen"
+
+#: src/bugtracker/configure.zcml:62
+msgid "Allows to edit the data of a Bug."
+msgstr "Erlaubt es die Daten eines Fehlers zu bearbeiten."
+
+#: src/bugtracker/configure.zcml:62
+msgid "Edit Bug"
+msgstr "Fehler bearbeiten"
+
+#: src/bugtracker/configure.zcml:71
+msgid "Allows to add comments to a bug."
+msgstr "Erlaubt es einen Kommentar zu dem Fehler hinzuzufügen."
+
+#: src/bugtracker/configure.zcml:71
+msgid "Add Bug Comment"
+msgstr "Fehlerkommentar hinzufügen"
+
+#: src/bugtracker/configure.zcml:80
+msgid "Allows to add attachments (files and images)  to a bug."
+msgstr "Erlaubt es Anhänge (Dateien und Bilder) zu einem Fehler hinzuzufügen."
+
+#: src/bugtracker/configure.zcml:80
+msgid "Add Bug Attachment"
+msgstr "Fehleranhang hinzufügen"
+
+#: src/bugtracker/interfaces.py:100
+msgid "Defines the release for which the bug is scheduled."
+msgstr "Definiert die Version für welche die Behebung des Fehlers vorgesehen ist."
+
+#: src/bugtracker/interfaces.py:106
+msgid "Owners"
+msgstr "Besitzer"
+
+#: src/bugtracker/interfaces.py:107
+msgid "List of people assigned as owners of the bug."
+msgstr "Eine List von Leuten die als Eigentümer des Fehlers erklärt wurden."
+
+#: src/bugtracker/interfaces.py:139 src/bugtracker/browser/configure.zcml:211
+#: src/bugtracker/browser/dependencies.pt:59
+#: src/bugtracker/browser/dependencies.pt:65
+msgid "Dependencies"
+msgstr "Abhängigkeiten"
+
+#: src/bugtracker/interfaces.py:140
+msgid "Other bugs this bug depends on."
+msgstr "Andere Fehler von denen dieser Fehler abhängt."
+
+#: src/bugtracker/interfaces.py:141 src/bugtracker/interfaces.py:154
+msgid "Bug Id"
+msgstr "Fehler Id"
+
+#: src/bugtracker/interfaces.py:142 src/bugtracker/interfaces.py:155
+msgid "Bug Id."
+msgstr "Fehler Id."
+
+#: src/bugtracker/interfaces.py:152 src/bugtracker/browser/dependencies.pt:60
+#: src/bugtracker/browser/dependencies.pt:64
+msgid "Dependents"
+msgstr "Abhängige"
+
+#: src/bugtracker/interfaces.py:153
+msgid "Other bugs that depend on this one"
+msgstr "Andere Fehler, die von diesem Fehler abhängen"
+
+#: src/bugtracker/interfaces.py:193
+msgid "Body"
+msgstr "Beschreibung"
+
+#: src/bugtracker/interfaces.py:194
+msgid "Renderable body of the Comment."
+msgstr "Interpretierbare Beschreibung des Kommentars."
+
+#: src/bugtracker/interfaces.py:214
+msgid "Default"
+msgstr "Standard"
+
+#: src/bugtracker/interfaces.py:215
+msgid "Default value of the vocabulary."
+msgstr "Standaerwert des Vokabulars."
+
+#: src/bugtracker/interfaces.py:42 src/bugtracker/interfaces.py:63
+msgid "Title"
+msgstr "Titel"
+
+#: src/bugtracker/interfaces.py:43
+msgid "Title of the bug tracker."
+msgstr "Titel des Fehlerverwalters."
+
+#: src/bugtracker/interfaces.py:64
+msgid "Title/Summary of the bug."
+msgstr "Titel/Zusammenfassung des Fehlers."
+
+#: src/bugtracker/interfaces.py:68
+msgid "Description"
+msgstr "Beschreibung"
+
+#: src/bugtracker/interfaces.py:69
+msgid "Detailed Description of the bug."
+msgstr "Detailierte Beschreibung des Fehlers."
+
+#: src/bugtracker/interfaces.py:73
+msgid "Submitter"
+msgstr "Einreicher"
+
+#: src/bugtracker/interfaces.py:74
+msgid "Name of the person that submitted the bug."
+msgstr "Name der Person, die den Fehler eingereicht hat."
+
+#: src/bugtracker/interfaces.py:78
+msgid "Status"
+msgstr "Status"
+
+#: src/bugtracker/interfaces.py:79
+msgid "The current status of the bug."
+msgstr "Der derzeitige Status des Fehlers."
+
+#: src/bugtracker/interfaces.py:85
+msgid "Priority"
+msgstr "Priorität"
+
+#: src/bugtracker/interfaces.py:86
+msgid "Specifies how urgent this bug is."
+msgstr "Gibt an wie dringend dieser Fehler ist."
+
+#: src/bugtracker/interfaces.py:92
+msgid "Type"
+msgstr "Typ"
+
+#: src/bugtracker/interfaces.py:93
+msgid "Specifies of what nature the bug is."
+msgstr "Gibt die Natur des Fehlers an."
+
+#: src/bugtracker/vocabulary.py:120
+msgid "The value '${value}' was not found in the vocabulary"
+msgstr "Der Wert '${value}' wurde nicht im Vokabular gefunden."
+
+#: src/bugtracker/vocabulary.py:136
+msgid "Status Definitions"
+msgstr "Status-Definitionen"
+
+#: src/bugtracker/vocabulary.py:146
+msgid "Release Definitions"
+msgstr "Versions-Definitionen"
+
+#: src/bugtracker/vocabulary.py:156
+msgid "Priority Definitions"
+msgstr "Prioritäts-Definitionen"
+
+#: src/bugtracker/vocabulary.py:166
+msgid "Bug Type Definitions"
+msgstr "Fehlertyp-Definitionen"
+
+#: src/bugtracker/vocabulary.py:97
+msgid "Cannot delete default value '${value}'."
+msgstr "Kann nicht den Standardwert '${value}' löschen."
+

Added: Zope3/trunk/src/bugtracker/mail.py
===================================================================
--- Zope3/trunk/src/bugtracker/mail.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/mail.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,105 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+# 
+##############################################################################
+"""Mail Support
+
+$Id: mail.py,v 1.3 2003/08/28 05:22:30 srichter Exp $
+"""
+from zope.interface import implements
+
+from zope.app import zapi
+from zope.app.annotation.interfaces import IAnnotations
+from zope.app.event.interfaces import IObjectCreatedEvent, IObjectModifiedEvent
+from zope.app.mail.interfaces import IMailDelivery
+
+from interfaces import IBug, IBugTracker, IMailSubscriptions
+
+SubscriberKey = 'bugtracker.MailSubsriptions.emails'
+
+class MailSubscriptions:
+    """An adapter for IBugTracker and IBug to provide an
+    interface for collecting E-mails for sending out change notices."""
+
+    implements(IMailSubscriptions)
+
+    def __init__(self, context):
+        self.context = context
+        self._annotations = IAnnotations(context)
+        if not self._annotations.get(SubscriberKey):
+            self._annotations[SubscriberKey] = ()
+
+    def getSubscriptions(self):
+        "See zopeproducts.messageboard.interfaces.IMailSubscriptions"
+        return self._annotations[SubscriberKey]
+        
+    def addSubscriptions(self, emails):
+        "See zopeproducts.messageboard.interfaces.IMailSubscriptions"
+        subscribers = list(self._annotations[SubscriberKey])
+        for email in emails:
+            if email not in subscribers:
+                subscribers.append(email.strip())
+        self._annotations[SubscriberKey] = tuple(subscribers)
+                
+    def removeSubscriptions(self, emails):
+        "See zopeproducts.messageboard.interfaces.IMailSubscriptions"
+        subscribers = list(self._annotations[SubscriberKey])
+        for email in emails:
+            if email in subscribers:
+                subscribers.remove(email)
+        self._annotations[SubscriberKey] = tuple(subscribers)
+
+
+class Mailer:
+    """Class to handle all outgoing mail."""
+
+    def __call__(self, event):
+        if IBug.providedBy(event.object):
+            if IObjectCreatedEvent.providedBy(event):
+                self.handleAdded(event.object)
+            elif IObjectModifiedEvent.providedBy(event):
+                self.handleModified(event.object)
+
+    def handleAdded(self, object):
+        subject = 'Added: %s (%s)' %(object.title, zapi.name(object))
+        emails = self.getAllSubscribers(object)
+        body = object.description
+        self.mail(emails, subject, body)        
+
+    def handleModified(self, object):
+        subject = 'Modified: %s (%s)' %(object.title, zapi.name(object))
+        emails = self.getAllSubscribers(object)
+        body = object.description
+        self.mail(emails, subject, body)
+
+    def handleRemoved(self, object):
+        subject = 'Removed: %s (%s)' %(object.title, zapi.name(object))
+        emails = self.getAllSubscribers(object)
+        body = object.description
+        self.mail(emails, subject, body)
+
+    def getAllSubscribers(self, object):
+        """Retrieves all email subscribers for this message and all above."""
+        emails = ()
+        obj = object
+        while IBug.providedBy(obj) or IBugTracker.providedBy(obj):
+            emails += tuple(IMailSubscriptions(obj).getSubscriptions())
+            obj = zapi.getParent(obj)
+        return emails
+
+    def mail(self, toaddrs, subject, body):
+        """Mail out the change message."""
+        if not toaddrs:
+            return
+        msg = 'Subject: %s\n\n\n%s' %(subject, body)
+        mail_utility = zapi.getUtility(IMailDelivery, 'bug-mailer')
+        mail_utility.send('bugtracker at zope3.org' , toaddrs, msg)

Added: Zope3/trunk/src/bugtracker/renderable.py
===================================================================
--- Zope3/trunk/src/bugtracker/renderable.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/renderable.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,65 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Renderable Text Object.
+
+$Id$
+"""
+from zope.security.checker import defineChecker, NoProxy
+
+class RenderableText(unicode):
+    """An extension to unicode to keep track of text's type.
+
+    In general you can store anything in the 'ttype' slot. However, to make this
+    attribute useful in the context of the bugtracker, the 'ttype' must
+    *always* contain a valid source type as specified in the zope.app.renderer
+    package.
+
+    Here a simple example on using the slot:
+
+      >>> rt = RenderableText('foo bar')
+      >>> rt
+      u'foo bar'
+      >>> rt.ttype
+      u'zope.source.rest'
+      >>> rt.ttype = u'zope.source.plaintext'
+      >>> rt.ttype
+      u'zope.source.plaintext'
+
+      >>> rt = RenderableText(u'foo bar', u'zope.source.stx')
+      >>> rt
+      u'foo bar'
+      >>> rt.ttype
+      u'zope.source.stx'
+    """
+
+    __slots__ = ('ttype',)
+
+    def __new__(cls, ustr, ttype=None):
+        """Create a new RenderableText object."""
+        self = unicode.__new__(cls, ustr)
+        self.ttype = ttype or u'zope.source.rest'
+        return self
+
+    def __getstate__(self):
+        """Return the state of the object."""
+        return unicode(self), self.ttype
+
+    def __setstate__(self, (ustr, ttype)):
+        """Set the state of the object."""
+        super(RenderableText, self).__init__(ustr)
+        self.ttype = ttype
+
+
+# Make sure the renderable texts get never proxied
+defineChecker(RenderableText, NoProxy)

Added: Zope3/trunk/src/bugtracker/template.xml
===================================================================
--- Zope3/trunk/src/bugtracker/template.xml	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/template.xml	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+<bugtracker version="1.0"
+    xmlns:tal="http://xml.zope.org/namespaces/tal"
+    tal:attributes="version view/version;
+                    title view/title">
+
+  <vocabularies>
+    <vocabulary 
+        tal:repeat="vocab view/vocabularies"
+        tal:attributes="name vocab/name"
+        ><tal:block repeat="term vocab/terms">
+      <term value="" title="" 
+         tal:condition="python: vocab['default'].value != term.value"
+         tal:attributes="value term/value;
+                         title term/title"
+      /><term value="" title="" default=""
+         tal:condition="python: vocab['default'].value == term.value"
+         tal:attributes="value term/value;
+                         title term/title" 
+      /></tal:block>
+    </vocabulary>
+  </vocabularies>
+
+  <bugs>
+    <bug 
+        id="1" 
+        title="Title of Bug" 
+        submitter="srichter"
+        status="new"
+        priority="normal"
+        type="bug"
+        release="zope_x3"
+        owners="srichter, jim"
+        dependencies="2, 3"
+        created="Jan 01, 2003 12:00:00 AM"
+        modified="Jan 01, 2003 12:00:00 AM"
+
+        tal:repeat="bug view/bugs"
+        tal:attributes="id bug/id;
+                        title bug/title;
+                        submitter bug/submitter;
+                        status bug/status;
+                        priority bug/priority;
+                        type bug/type;
+                        release bug/release;
+                        owners bug/owners;
+                        dependencies bug/dependencies;
+                        created bug/created;
+                        modified bug/modified;
+                        ">
+      <description ttype=""
+          tal:attributes="ttype bug/description_ttype"
+          tal:content="bug/description">
+      </description>
+
+      <comments>
+        <comment
+            tal:repeat="comment bug/comments"
+            tal:attributes="creator comment/creator;
+                            created comment/created;
+                            id comment/id;
+                            ttype comment/ttype" 
+            tal:content="structure comment/body" />
+      </comments>
+
+      <attachments>
+        <attachment
+            tal:repeat="attachment bug/attachments"
+            tal:attributes="type attachment/type;
+                            creator attachment/creator;
+                            created attachment/created;
+                            id attachment/id" 
+            tal:content="attachment/data" />   
+      </attachments>
+    </bug>
+  </bugs>
+
+</bugtracker>

Added: Zope3/trunk/src/bugtracker/tests/__init__.py
===================================================================

Added: Zope3/trunk/src/bugtracker/tests/placelesssetup.py
===================================================================
--- Zope3/trunk/src/bugtracker/tests/placelesssetup.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/placelesssetup.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,175 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bug Tracker System PlacelessSetup
+
+Since it requires **a lot** of basic setup for the simplest integration unit
+tests between Bug Tracker and Bu, a seperate setup mixin seems very
+appropriate.
+
+$Id: placelesssetup.py,v 1.3 2003/07/28 20:38:38 srichter Exp $
+"""
+from datetime import datetime
+
+from zope.component.interfaces import IFactory
+from zope.component.service import defineService, serviceManager
+from zope.component.tests.placelesssetup import PlacelessSetup as ComponentSetup
+from zope.interface import classImplements, implements
+from zope.schema.vocabulary import getVocabularyRegistry
+
+from zope.app import zapi
+from zope.app.tests import ztapi
+from zope.app.annotation.attribute import AttributeAnnotations
+from zope.app.file import File
+from zope.app.container.interfaces import INameChooser
+from zope.app.dublincore.annotatableadapter import ZDCAnnotatableAdapter
+from zope.app.event.tests.placelesssetup import PlacelessSetup as EventSetup
+from zope.app.annotation.interfaces import IAnnotations, IAttributeAnnotatable
+from zope.app.dublincore.interfaces import IWriteZopeDublinCore
+from zope.app.dublincore.interfaces import IZopeDublinCore
+from zope.app.location.interfaces import ILocation
+from zope.app.location.traversing import LocationPhysicallyLocatable
+from zope.app.renderer.plaintext import IPlainTextSource
+from zope.app.renderer.plaintext import PlainTextToHTMLRenderer
+from zope.app.renderer.plaintext import PlainTextSourceFactory
+from zope.app.security.interfaces import IAuthenticationService
+from zope.app.size.interfaces import ISized
+from zope.app.traversing.interfaces import IContainmentRoot, ITraverser
+from zope.app.traversing.interfaces import ITraversable, IPhysicallyLocatable
+from zope.app.security.principalregistry import principalRegistry
+from zope.app.size import DefaultSized
+from zope.app.traversing.adapters import DefaultTraversable, Traverser
+from zope.app.traversing.interfaces import IPhysicallyLocatable
+
+from bugtracker.bug import Bug, BugDependencyAdapter
+from bugtracker.comment import Comment
+from bugtracker.interfaces import IBug, IBugTracker
+from bugtracker.interfaces import IBugDependencies
+from bugtracker.interfaces import IStatusVocabulary, IPriorityVocabulary
+from bugtracker.interfaces import IBugTypeVocabulary, IReleaseVocabulary
+from bugtracker.renderable import RenderableText
+from bugtracker.tracker import BugTracker, BugTrackerNameChooser
+from bugtracker.vocabulary import StatusVocabulary, PriorityVocabulary
+from bugtracker.vocabulary import BugTypeVocabulary, ReleaseVocabulary
+from bugtracker.vocabulary import UserVocabulary
+
+class Root(object):
+    implements(IContainmentRoot)
+    __parent__ = None
+    __name__ = ''
+
+class PlacelessSetup(ComponentSetup, EventSetup):
+
+    def setUp(self):
+        ComponentSetup.setUp(self)
+        EventSetup.setUp(self)
+        classImplements(Bug, IAttributeAnnotatable)
+        classImplements(BugTracker, IAttributeAnnotatable)
+        classImplements(Comment, IAttributeAnnotatable)
+        classImplements(File, IAttributeAnnotatable)
+        ztapi.provideAdapter(IAttributeAnnotatable, IAnnotations,
+                             AttributeAnnotations)
+        ztapi.provideAdapter(IAttributeAnnotatable, IWriteZopeDublinCore,
+                             ZDCAnnotatableAdapter)
+        ztapi.provideAdapter(ILocation, IPhysicallyLocatable,
+                             LocationPhysicallyLocatable)
+        ztapi.provideAdapter(IBug, IBugDependencies, BugDependencyAdapter)
+        ztapi.provideAdapter(None, ITraverser, Traverser)
+        ztapi.provideAdapter(None, ITraversable, DefaultTraversable)
+        ztapi.provideAdapter(None, ISized, DefaultSized)
+
+        ztapi.provideUtility(IFactory, PlainTextSourceFactory,
+                             u'zope.source.plaintext')
+        ztapi.browserView(IPlainTextSource, '', PlainTextToHTMLRenderer)
+        
+        ztapi.provideAdapter(IBugTracker, IStatusVocabulary, StatusVocabulary)
+        ztapi.provideAdapter(IBugTracker, IPriorityVocabulary,
+                             PriorityVocabulary)
+        ztapi.provideAdapter(IBugTracker, IReleaseVocabulary, ReleaseVocabulary)
+        ztapi.provideAdapter(IBugTracker, IBugTypeVocabulary, BugTypeVocabulary)
+
+        ztapi.provideAdapter(IBugTracker, INameChooser, BugTrackerNameChooser)
+
+        registry = getVocabularyRegistry()
+        registry.register('Stati', StatusVocabulary)
+        registry.register('Priorities', PriorityVocabulary)
+        registry.register('BugTypes', BugTypeVocabulary)
+        registry.register('Releases', ReleaseVocabulary)
+        registry.register('Users', UserVocabulary)
+
+        defineService(zapi.servicenames.Authentication,
+                      IAuthenticationService)
+        serviceManager.provideService(zapi.servicenames.Authentication,
+                                      principalRegistry)
+
+        principalRegistry.definePrincipal(u'zope.srichter',
+                                          u'Stephan Richter', u'',
+                                          'srichter', 'foo')
+        principalRegistry.definePrincipal(u'zope.jim',
+                                          u'Jim Fulton', u'',
+                                          'jim', 'bar')
+        principalRegistry.definePrincipal(u'zope.stevea',
+                                          u'Steve Alexander', u'',
+                                          'stevea', 'blah')
+
+
+    def generateTracker(self):
+        tracker = BugTracker()
+        tracker.__parent__ = Root()
+        tracker.__name__ = "tracker"
+        vocab = IStatusVocabulary(tracker)
+        vocab.add('new', u'New', True)
+        vocab.add('open', u'Open')
+        vocab.add('assigned', u'Assigned')
+        vocab.add('deferred', u'Deferred')
+        vocab.add('closed', u'Closed')
+        vocab = IBugTypeVocabulary(tracker)
+        vocab.add('bug', u'Bug', True)
+        vocab.add('feature', u'Feature')
+        vocab.add('release', u'Release')
+        vocab = IReleaseVocabulary(tracker)
+        vocab.add('None', u'(not specified)', True)
+        vocab.add('zope_x3', u'Zope X3')
+        vocab = IPriorityVocabulary(tracker)
+        vocab.add('low', u'Low')
+        vocab.add('normal', u'Normal', True)
+        vocab.add('urgent', u'Urgent')
+        vocab.add('critical', u'Critical')
+        return tracker
+        
+    def generateBug(self, id='1'):
+        bug = Bug()
+        bug.__parent__ = self.generateTracker()
+        bug.__name__ = id
+        bug.title = u'Bug %s' %id
+        bug.description = RenderableText(u'This is Bug %s.' %id,
+                                         'zope.source.plaintext')
+        dc = IZopeDublinCore(bug)
+        dc.created = datetime(2003, 03, 02+int(id), 03, 00, 00)
+        dc.modified = datetime(2003, 03, 02+int(id), 04, 00, 00)
+        dc.creators = [u'zope.srichter']
+        bug.owners = [u'zope.jim', u'zope.stevea']
+        comment = Comment()
+        dc = IZopeDublinCore(comment)
+        dc.creators = [u'zope.srichter']
+        dc.created = datetime(2003, 03, 02+int(id), 05, 00, 00)
+        comment.body = RenderableText('This is comment 1.',
+                                      'zope.source.plaintext')
+        bug['comment1'] = comment
+        attach = File()
+        dc = IZopeDublinCore(comment)
+        dc.creators = [u'zope.srichter']
+        dc.created = datetime(2003, 03, 02+int(id), 06, 00, 00)
+        attach.data = 'This is an attachment.'
+        bug['attach.txt'] = attach
+        return bug

Added: Zope3/trunk/src/bugtracker/tests/test_bug.py
===================================================================
--- Zope3/trunk/src/bugtracker/tests/test_bug.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/test_bug.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,105 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bug Tests
+
+$Id: test_bug.py,v 1.2 2003/07/28 17:13:48 srichter Exp $
+"""
+import unittest
+
+from zope.app.container.tests.test_btree import TestBTreeContainer
+
+from bugtracker.tests.placelesssetup import PlacelessSetup
+from bugtracker.interfaces import IBug
+from bugtracker.bug import Bug
+
+DCkey = "zope.app.dublincore.ZopeDublinCore"
+
+class BugTest(PlacelessSetup, TestBTreeContainer, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+
+    def getBug(self):
+        bug = Bug()
+        bug.__parent__ = self.generateTracker()
+        bug.__name__ = id
+        return bug
+
+    def makeTestObject(self):
+        return Bug()
+
+    def test_Interface(self):
+        self.failUnless(IBug.providedBy(self.getBug()))
+
+    def test_title(self):
+        bug = self.getBug()
+        self.assertEqual(bug.title, u'')
+        bug.title = u'Title'
+        self.assertEqual(bug.title, u'Title')
+        self.assertEqual(bug.__annotations__[DCkey]['Title'], (u'Title',))
+
+    def test_description(self):
+        bug = self.getBug()
+        self.assertEqual(bug.description, u'')
+        bug.description = u'Description'
+        self.assertEqual(bug.description, u'Description')
+        self.assertEqual(bug.__annotations__[DCkey]['Description'],
+                         (u'Description',))
+
+    def test_owners(self):
+        bug = self.getBug()
+        self.assertEqual(bug.owners, [])
+        bug.owners = [u'srichter']
+        self.assertEqual(bug.owners, [u'srichter'])
+
+    def test_status(self):
+        bug = self.getBug()
+        self.assertEqual(bug.status, u'new')
+        bug.status = u'open'
+        self.assertEqual(bug.status, u'open')
+
+    def test_type(self):
+        bug = self.getBug()
+        self.assertEqual(bug.type, u'bug')
+        bug.type = u'feature'
+        self.assertEqual(bug.type, u'feature')
+
+    def test_priority(self):
+        bug = self.getBug()
+        self.assertEqual(bug.priority, u'normal')
+        bug.priority = u'urgent'
+        self.assertEqual(bug.priority, u'urgent')
+
+    def test_release(self):
+        bug = self.getBug()
+        self.assertEqual(bug.release, u'None')
+        bug.release = u'zope_x3'
+        self.assertEqual(bug.release, u'zope_x3')
+
+    def test_submitter(self):
+        bug = self.getBug()
+        # Just here to create the annotations
+        bug.title = u''
+        self.assertEqual(bug.submitter, None)
+        bug.__annotations__[DCkey]['Creator'] = ['srichter']
+        self.assertEqual(bug.submitter, u'srichter')
+
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(BugTest),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()

Added: Zope3/trunk/src/bugtracker/tests/test_comment.py
===================================================================
--- Zope3/trunk/src/bugtracker/tests/test_comment.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/test_comment.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,43 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bug Tracker Mail Subscription and Mailer Tests
+
+$Id: test_comment.py,v 1.1 2003/07/24 18:08:38 srichter Exp $
+"""
+import unittest
+
+from bugtracker.interfaces import IComment
+from bugtracker.comment import Comment
+
+class CommentTest(unittest.TestCase):
+
+    def setUp(self):
+        self.comment = Comment()
+
+    def test_Interface(self):
+        self.failUnless(IComment.providedBy(self.comment))
+
+    def test_body(self):
+        self.assertEqual(self.comment.body, u'')
+        self.comment.body = u'test'
+        self.assertEqual(self.comment.body, u'test')
+        
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(CommentTest),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()

Added: Zope3/trunk/src/bugtracker/tests/test_dependencies.py
===================================================================
--- Zope3/trunk/src/bugtracker/tests/test_dependencies.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/test_dependencies.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,98 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bug Dependencies Tests
+
+$Id: test_dependencies.py,v 1.3 2003/08/28 05:22:32 srichter Exp $
+"""
+import unittest
+
+from zope.interface import classImplements
+from zope.component.tests.placelesssetup import PlacelessSetup
+
+from zope.app.tests import ztapi
+from zope.app.annotation.interfaces import IAnnotations, IAttributeAnnotatable
+from zope.app.annotation.attribute import AttributeAnnotations
+from zope.app.location.interfaces import ILocation
+from zope.app.location.traversing import LocationPhysicallyLocatable
+from zope.app.traversing.interfaces import IPhysicallyLocatable
+
+from bugtracker.interfaces import IBug, IBugDependencies
+from bugtracker.bug import Bug, BugDependencyAdapter
+from bugtracker.tracker import BugTracker
+
+
+class DependencyTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        super(DependencyTest, self).setUp()
+        classImplements(Bug, IAttributeAnnotatable);
+        ztapi.provideAdapter(IAttributeAnnotatable, IAnnotations,
+                             AttributeAnnotations)
+        ztapi.provideAdapter(IBug, IBugDependencies,
+                             BugDependencyAdapter)
+        ztapi.provideAdapter(ILocation, IPhysicallyLocatable,
+                             LocationPhysicallyLocatable)
+
+        self.bug = Bug()
+
+    def makeTestObject(self):
+        return BugDependencyAdapter(self.bug)
+
+    def test_Interface(self):
+        deps = self.makeTestObject()
+        self.failUnless(IBugDependencies.providedBy(deps))
+
+    def test_dependencies(self):
+        deps = self.makeTestObject()
+        self.assertEqual((), deps.dependencies)
+        deps.dependencies = ('foo',)
+        self.assertEqual(('foo',), deps.dependencies)
+        deps.addDependencies(('foobar',))
+        self.assertEqual(('foo', 'foobar'), deps.dependencies)
+        deps.deleteDependencies(('foobar',))
+        self.assertEqual(('foo',), deps.dependencies)
+        # Test whether the annotations stay.
+        deps = self.makeTestObject()
+        self.assertEqual(('foo',), deps.dependencies)
+
+    def test_findChildren(self):
+        tracker = BugTracker()
+
+        bug1 = Bug()
+        tracker['1'] = bug1
+        deps1 = BugDependencyAdapter(bug1)
+
+        bug2 = Bug()
+        tracker['2'] = bug2
+        deps2 = BugDependencyAdapter(bug2)
+        deps1.dependencies = ('2',)
+
+        bug3 = Bug()
+        tracker['3'] = bug3
+        deps2.dependencies = ('3',)
+
+        self.assertEqual(( (bug2, ()), ),
+                         deps1.findChildren(False));
+
+        self.assertEqual(((bug2, ((bug3, ()),) ),),
+                         deps1.findChildren());
+        
+    
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(DependencyTest),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()

Added: Zope3/trunk/src/bugtracker/tests/test_mail.py
===================================================================
--- Zope3/trunk/src/bugtracker/tests/test_mail.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/test_mail.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,150 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bug Tracker Mail Subscription and Mailer Tests
+
+$Id: test_mail.py,v 1.1 2003/07/24 18:08:38 srichter Exp $
+"""
+import unittest
+
+from zope.component.service import defineService, serviceManager
+from zope.component.tests.placelesssetup import PlacelessSetup
+from zope.interface import classImplements, implements
+
+from zope.app.tests import ztapi
+from zope.app.annotation.interfaces import IAnnotations, IAttributeAnnotatable
+from zope.app.dublincore.interfaces import IWriteZopeDublinCore
+from zope.app.traversing.interfaces import IPhysicallyLocatable
+
+from zope.app.annotation.attribute import AttributeAnnotations
+from zope.app.dublincore.annotatableadapter import ZDCAnnotatableAdapter
+from zope.app.event.objectevent import ObjectModifiedEvent
+from zope.app.location.interfaces import ILocation
+from zope.app.location.traversing import LocationPhysicallyLocatable
+from zope.app.mail.interfaces import IMailDelivery
+
+from bugtracker.bug import Bug
+from bugtracker.interfaces import IBug, IBugTracker, IMailSubscriptions
+from bugtracker.mail import MailSubscriptions, SubscriberKey, Mailer
+from bugtracker.tracker import BugTracker
+
+mail_result = [] 
+
+class MailDeliveryStub(object):
+    implements(IMailDelivery)
+
+    def send(self, fromaddr, toaddrs, message):
+        mail_result.append((fromaddr, toaddrs, message))
+
+
+class MailSubscriptionTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        # This needs to be done, since the IAttributeAnnotable interface
+        # is usually set in the ZCML
+        classImplements(Bug, IAttributeAnnotatable)
+        ztapi.provideAdapter(IAttributeAnnotatable, IAnnotations,
+                             AttributeAnnotations)
+        self._sub = MailSubscriptions(Bug())
+
+    def test_Interface(self):
+        self.failUnless(IMailSubscriptions.providedBy(self._sub))
+
+    def test_getSubscriptions(self):
+        self.assertEqual((), self._sub.getSubscriptions())
+        self._sub.context.__annotations__[SubscriberKey] = ('foo at bar.com',)
+        self.assertEqual(('foo at bar.com',), self._sub.getSubscriptions())
+
+    def test_addSubscriptions(self):
+        self._sub.addSubscriptions(('foo at bar.com',))
+        self.assertEqual(('foo at bar.com',),
+                         self._sub.context.__annotations__[SubscriberKey])
+        self._sub.addSubscriptions(('blah at bar.com',))
+        self.assertEqual(('foo at bar.com', 'blah at bar.com'),
+                         self._sub.context.__annotations__[SubscriberKey])
+        self._sub.addSubscriptions(('blah at bar.com',))
+        self.assertEqual(('foo at bar.com', 'blah at bar.com'),
+                         self._sub.context.__annotations__[SubscriberKey])
+        self._sub.addSubscriptions(('blah at bar.com', 'doh at bar.com'))
+        self.assertEqual(('foo at bar.com', 'blah at bar.com', 'doh at bar.com'),
+                         self._sub.context.__annotations__[SubscriberKey])
+
+    def test_removeSubscriptions(self):
+        self._sub.context.__annotations__[SubscriberKey] = (
+            'foo at bar.com', 'blah at bar.com', 'doh at bar.com')
+        self._sub.removeSubscriptions(('foo at bar.com',))
+        self.assertEqual(('blah at bar.com', 'doh at bar.com'),
+                         self._sub.context.__annotations__[SubscriberKey])
+        self._sub.removeSubscriptions(('foo at bar.com',))
+        self.assertEqual(('blah at bar.com', 'doh at bar.com'),
+                         self._sub.context.__annotations__[SubscriberKey])
+        self._sub.removeSubscriptions(('blah at bar.com', 'doh at bar.com'))
+        self.assertEqual((),
+                         self._sub.context.__annotations__[SubscriberKey])
+
+
+class MailerTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        # This needs to be done, since the IAttributeAnnotable interface
+        # is usually set in the ZCML
+        ztapi.provideAdapter(ILocation, IPhysicallyLocatable,
+                             LocationPhysicallyLocatable)
+        classImplements(BugTracker, IAttributeAnnotatable)
+        classImplements(Bug, IAttributeAnnotatable)
+        ztapi.provideAdapter(IBugTracker, IMailSubscriptions, MailSubscriptions)
+        ztapi.provideAdapter(IBug, IMailSubscriptions, MailSubscriptions)
+        ztapi.provideAdapter(IAttributeAnnotatable, IAnnotations,
+                             AttributeAnnotations)
+        ztapi.provideAdapter(IAttributeAnnotatable, IWriteZopeDublinCore,
+                             ZDCAnnotatableAdapter)
+        ztapi.provideUtility(IMailDelivery, MailDeliveryStub(), 'bug-mailer')
+
+    def test_getAllSubscribers(self):
+        tracker = BugTracker()
+        tracker.__parent__ = object()
+        tracker.__name__ = 'tracker'
+        tracker_sub = MailSubscriptions(tracker)
+        tracker_sub.context.__annotations__[SubscriberKey] = ('foo at bar.com',)
+        bug = Bug()
+        bug_sub = MailSubscriptions(bug)
+        bug_sub.context.__annotations__[SubscriberKey] = ('blah at bar.com',)
+        tracker['1'] = bug
+        self.assertEqual(('blah at bar.com', 'foo at bar.com'),
+                         Mailer().getAllSubscribers(bug))
+
+    def test_call(self):
+        bug = Bug()
+        bug.__parent__ = object()
+        bug.__name__ = 'bug'
+        bug.title = u'Hello'
+        bug.description = u'Hello World!'
+        bug_sub = MailSubscriptions(bug)
+        bug_sub.context.__annotations__[SubscriberKey] = ('foo at bar.com',)
+        event = ObjectModifiedEvent(bug)
+        Mailer()(event)
+        self.assertEqual('bugtracker at zope3.org', mail_result[0][0])
+        self.assertEqual(('foo at bar.com', ), mail_result[0][1])
+        self.assertEqual('Subject: Modified: Hello (bug)\n\n\nHello World!',
+                         mail_result[0][2])
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(MailSubscriptionTest),
+        unittest.makeSuite(MailerTest),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()

Added: Zope3/trunk/src/bugtracker/tests/test_renderable.py
===================================================================
--- Zope3/trunk/src/bugtracker/tests/test_renderable.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/test_renderable.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,27 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Tests for the renderable text class.
+
+$Id$
+"""
+import unittest
+from zope.testing.doctestunit import DocTestSuite
+
+def test_suite():
+    return unittest.TestSuite((
+        DocTestSuite('bugtracker.renderable'),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Added: Zope3/trunk/src/bugtracker/tests/test_tracker.py
===================================================================
--- Zope3/trunk/src/bugtracker/tests/test_tracker.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/test_tracker.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,54 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bug Tracker Mail Subscription and Mailer Tests
+
+$Id: test_tracker.py,v 1.5 2003/07/28 17:13:48 srichter Exp $
+"""
+import unittest
+
+from zope.app.dublincore.annotatableadapter import ZDCAnnotatableAdapter
+from zope.app.dublincore.interfaces import IWriteZopeDublinCore
+from zope.app.container.tests.test_btree import TestBTreeContainer
+
+from bugtracker.tests.placelesssetup import PlacelessSetup
+from bugtracker.interfaces import IBugTracker
+from bugtracker.tracker import BugTracker
+from bugtracker.bug import Bug
+
+
+class TrackerTest(PlacelessSetup, TestBTreeContainer, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+
+    def makeTestObject(self):
+        return BugTracker()
+
+    def test_Interface(self):
+        self.failUnless(IBugTracker.providedBy(self.makeTestObject()))
+
+    def test_title(self):
+        tracker = self.makeTestObject()
+        self.assertEqual(tracker.title, u'')
+        tracker.title = u'test'
+        self.assertEqual(tracker.title, u'test')
+        
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(TrackerTest),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()

Added: Zope3/trunk/src/bugtracker/tests/test_vocabularies.py
===================================================================
--- Zope3/trunk/src/bugtracker/tests/test_vocabularies.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/test_vocabularies.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,308 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bug Tracker Vocabulary Tests
+
+$Id: test_vocabularies.py,v 1.3 2003/07/28 17:13:48 srichter Exp $
+"""
+import unittest
+
+from zope.component.service import defineService, serviceManager
+from zope.component.tests.placelesssetup import PlacelessSetup
+from zope.interface import classImplements, implements
+from zope.schema.interfaces import ITokenizedTerm
+from zope.schema.vocabulary import getVocabularyRegistry
+
+from zope.app import zapi
+from zope.app.tests import ztapi
+from zope.app.annotation.attribute import AttributeAnnotations
+from zope.app.annotation.interfaces import IAnnotations, IAttributeAnnotatable
+from zope.app.container.contained import contained, Contained
+from zope.app.security.interfaces import IAuthenticationService
+from zope.app.security.principalregistry import principalRegistry, Principal
+
+from bugtracker.interfaces import IManagableVocabulary
+from bugtracker.interfaces import IBugTracker
+from bugtracker.tracker import BugTracker
+from bugtracker.vocabulary import SimpleTerm
+from bugtracker.vocabulary import StatusVocabulary, PriorityVocabulary
+from bugtracker.vocabulary import BugTypeVocabulary, ReleaseVocabulary
+from bugtracker.vocabulary import UserTerm, UserVocabulary
+from bugtracker.vocabulary import ManagableVocabulary
+from bugtracker.vocabulary import VocabularyPropertyGetter
+from bugtracker.vocabulary import VocabularyPropertySetter
+from bugtracker.tests.placelesssetup import Root
+
+
+class ManagableVocabularyBaseTest(PlacelessSetup):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        classImplements(BugTracker, IAttributeAnnotatable)
+        ztapi.provideAdapter(IAttributeAnnotatable, IAnnotations,
+                             AttributeAnnotations)
+
+    def getVocabularyClass(self):
+        return NotImplemented
+
+    def makeVocabulary(self):
+        tracker = BugTracker()
+        contained(tracker, Root(), name="tracker")
+        vocab = self.getVocabularyClass()(tracker)
+        vocab.context.__annotations__ = {}
+        data = {'1': SimpleTerm('1', u'one'),
+                '2': SimpleTerm('2', u'two'),
+                '3': SimpleTerm('3', u'three'),
+                '4': SimpleTerm('4', u'four')}
+        vocab.context.__annotations__[vocab.key] = data
+        vocab.context.__annotations__[vocab.key+'/default'] = '1'
+        return vocab
+
+    def test_contains(self):
+        vocab = self.makeVocabulary()
+        self.assertEqual(vocab.__contains__('2'), True)
+        self.assertEqual(vocab.__contains__('6'), False)
+
+    def test_iter(self):
+        vocab = self.makeVocabulary()
+        self.assertEqual('2' in map(lambda x: x.value, vocab.__iter__()), True)
+        self.assertEqual('6' in map(lambda x: x.value, vocab.__iter__()), False)
+        self.assertEqual('2' in map(lambda x: x.value, iter(vocab)), True)
+        self.assertEqual('6' in map(lambda x: x.value, iter(vocab)), False)
+
+    def test_len(self):
+        vocab = self.makeVocabulary()
+        self.assertEqual(vocab.__len__(), 4)
+        self.assertEqual(len(vocab), 4)
+
+    def test_getQuery(self):
+        vocab = self.makeVocabulary()
+        self.assertEqual(vocab.getQuery(), None)
+
+    def test_getTerm(self):
+        vocab = self.makeVocabulary()
+        self.assertEqual(vocab.getTerm('1').value, '1')
+        self.assertEqual(vocab.getTerm('1').title, 'one')
+        self.assertRaises(KeyError, vocab.getTerm, ('6',))
+
+    def test_getTermByToken(self):
+        vocab = self.makeVocabulary()
+        self.assertEqual(vocab.getTermByToken('1').value, '1')
+        self.assertEqual(vocab.getTermByToken('1').title, 'one')
+        self.assertRaises(KeyError, vocab.getTermByToken, ('6',))
+
+    def test_add(self):
+        vocab = self.makeVocabulary()
+        vocab.add('5', 'five')
+        self.assertEqual(vocab.getTerm('5').value, '5')
+        self.assertEqual(vocab.getTerm('5').title, 'five')
+        vocab.add('6', 'six', True)
+        self.assertEqual(vocab.getTerm('6').value, '6')
+        self.assertEqual(vocab.getTerm('6').title, 'six')
+        self.assertEqual(vocab.default.value, '6')
+        self.assertEqual(vocab.default.title, 'six')
+
+    def test_delete(self):
+        vocab = self.makeVocabulary()
+        vocab.delete('4')
+        self.assertRaises(KeyError, vocab.getTerm, ('4',))
+        vocab.default = '2'
+        self.assertRaises(ValueError, vocab.delete, '2')
+
+    def test_default(self):
+        vocab = self.makeVocabulary()
+        vocab.default = '4'
+        self.assertEqual(vocab.default.value, '4')
+        self.assertEqual(vocab.default.title, 'four')
+        vocab.default = vocab.getTerm('3')
+        self.assertEqual(vocab.default.value, '3')
+        self.assertEqual(vocab.default.title, 'three')
+    
+
+class StatusVocabularyTest(ManagableVocabularyBaseTest, unittest.TestCase):
+
+    def getVocabularyClass(self):
+        return StatusVocabulary
+
+
+class PriorityVocabularyTest(ManagableVocabularyBaseTest, unittest.TestCase):
+
+    def getVocabularyClass(self):
+        return PriorityVocabulary
+
+
+class ReleaseVocabularyTest(ManagableVocabularyBaseTest, unittest.TestCase):
+
+    def getVocabularyClass(self):
+        return ReleaseVocabulary
+
+
+class BugTypeVocabularyTest(ManagableVocabularyBaseTest, unittest.TestCase):
+
+    def getVocabularyClass(self):
+        return BugTypeVocabulary
+
+
+class SimpleTermTest(unittest.TestCase):
+
+    def setUp(self):
+        self.term = SimpleTerm('foo', 'bar')
+
+    def test_Interface(self):
+        self.failUnless(ITokenizedTerm.providedBy(self.term))
+
+    def test_token(self):
+        self.assertEqual(self.term.token, 'foo')
+        self.assertEqual(self.term.getToken(), 'foo')
+
+    def test_value_title(self):
+        self.assertEqual(self.term.value, 'foo')
+        self.assertEqual(self.term.title, 'bar')
+
+
+class UserTermTest(unittest.TestCase):
+
+    def setUp(self):
+        principal = Principal('0', 'Stephan', 'blah', 'srichter', 'Nothing') 
+        self.term = UserTerm(principal)
+
+    def test_Interface(self):
+        self.failUnless(ITokenizedTerm.providedBy(self.term))
+
+    def test_token(self):
+        self.assertEqual(self.term.token, '0')
+
+    def test_value(self):
+        self.assertEqual(self.term.value, '0')
+
+    def test_principal(self):
+        self.assertEqual(self.term.principal['id'], '0')
+        self.assertEqual(self.term.principal['login'], 'srichter')
+        self.assertEqual(self.term.principal['title'], 'Stephan')
+
+
+class UserVocabularyTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        defineService(zapi.servicenames.Authentication, IAuthenticationService)
+        serviceManager.provideService(zapi.servicenames.Authentication,
+                                      principalRegistry)
+        principalRegistry.definePrincipal(
+            '0', 'title0', 'desc0', 'zero', 'pass0')
+        principalRegistry.definePrincipal(
+            '1', 'title1', 'desc1', 'one', 'pass1')
+        principalRegistry.definePrincipal(
+            '2', 'title2', 'desc2', 'two', 'pass2')
+
+        self.vocab = UserVocabulary(None)
+
+    def test_contains(self):
+        self.assertEqual(self.vocab.__contains__('0'), True)
+        self.assertEqual(self.vocab.__contains__('3'), False)
+
+    def test_iter(self):
+        vocab = self.vocab
+        self.assertEqual('0' in map(lambda x: x.value, vocab.__iter__()), True)
+        self.assertEqual('3' in map(lambda x: x.value, vocab.__iter__()), False)
+        self.assertEqual('0' in map(lambda x: x.value, iter(vocab)), True)
+        self.assertEqual('3' in map(lambda x: x.value, iter(vocab)), False)
+
+    def test_len(self):
+        self.assertEqual(self.vocab.__len__(), 3)
+        self.assertEqual(len(self.vocab), 3)
+
+    def test_getQuery(self):
+        self.assertEqual(self.vocab.getQuery(), None)
+
+    def test_getTerm(self):
+        self.assertEqual(self.vocab.getTerm('1').value, '1')
+        self.assertEqual(self.vocab.getTerm('1').principal['login'], 'one')
+        self.assertRaises(KeyError, self.vocab.getTerm, ('3',))
+
+    def test_getTermByToken(self):
+        vocab = self.vocab
+        self.assertEqual(vocab.getTermByToken('1').value, '1')
+        self.assertEqual(vocab.getTermByToken('1').principal['login'], 'one')
+        self.assertRaises(KeyError, vocab.getTermByToken, ('3',))
+
+
+class SampleVocabulary(ManagableVocabulary):
+    key = 'vocab/values'
+    interface = IBugTracker
+    title = 'Vocabulary'
+
+class SampleObject(Contained):
+
+    sample = property(VocabularyPropertyGetter('_sample', 'Vocabs'),
+                      VocabularyPropertySetter('_sample', 'Vocabs'))
+    
+
+class ManagableVocabularyBaseTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        classImplements(BugTracker, IAttributeAnnotatable)
+        ztapi.provideAdapter(IAttributeAnnotatable, IAnnotations,
+                             AttributeAnnotations)
+        registry = getVocabularyRegistry()
+        registry.register('Vocabs', SampleVocabulary)
+
+        tracker = BugTracker()
+        contained(tracker, Root(), name="tracker")
+        vocab = SampleVocabulary(tracker)
+        vocab.context.__annotations__ = {}
+        data = {'1': SimpleTerm('1', u'one'),
+                '2': SimpleTerm('2', u'two'),
+                '3': SimpleTerm('3', u'three'),
+                '4': SimpleTerm('4', u'four')}
+        vocab.context.__annotations__[vocab.key] = data
+        vocab.context.__annotations__[vocab.key+'/default'] = '1'
+        self.tracker = tracker
+
+    def getObject(self):
+        obj = SampleObject()
+        contained(obj, self.tracker, name="1")
+        return obj
+
+    def test_getter(self):
+        obj = self.getObject()
+        self.assertEqual(obj.sample, '1')
+        obj._sample = '2'
+        self.assertEqual(obj.sample, '2')
+
+    def test_setter(self):
+        obj = self.getObject()
+        obj.sample = '2'
+        self.assertEqual(obj.sample, '2')
+        try:
+            obj.sample = '7'
+            self.failIf(True)
+        except ValueError:
+            pass
+
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(SimpleTermTest),
+        unittest.makeSuite(StatusVocabularyTest),
+        unittest.makeSuite(PriorityVocabularyTest),
+        unittest.makeSuite(ReleaseVocabularyTest),
+        unittest.makeSuite(BugTypeVocabularyTest),
+        unittest.makeSuite(UserTermTest),
+        unittest.makeSuite(UserVocabularyTest),
+        unittest.makeSuite(ManagableVocabularyBaseTest),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()

Added: Zope3/trunk/src/bugtracker/tests/test_xmlexportimport.py
===================================================================
--- Zope3/trunk/src/bugtracker/tests/test_xmlexportimport.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/test_xmlexportimport.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,147 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""XML Export/Import Tests
+
+$Id: test_xmlexportimport.py,v 1.4 2003/07/28 17:13:48 srichter Exp $
+"""
+import unittest, os
+from datetime import datetime
+
+from zope.app.file import File
+from zope.app.datetimeutils import parseDatetimetz
+from zope.app.dublincore.interfaces import IZopeDublinCore
+
+from bugtracker import tests
+from bugtracker.bug import Bug, BugDependencyAdapter
+from bugtracker.exportimport import XMLExport, XMLImport
+from bugtracker.interfaces import IBug, IBugDependencies
+from bugtracker.tests.placelesssetup import PlacelessSetup, Root
+from bugtracker.tracker import BugTracker
+
+
+class ImportTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        tracker = BugTracker()
+        tracker.__parent__ = Root()
+        tracker.__name__ = 'tracker'
+        file = os.path.join(os.path.split(tests.__file__)[0], 'tracker.xml')
+        XMLImport(tracker).processXML(open(file))
+        self.tracker = tracker
+
+    def test_properties(self):
+        tracker = self.tracker
+        self.assertEqual(tracker.title, u'Bug Tracker')
+
+    def test_bug(self):
+        bug = self.tracker['1']
+        self.assertEqual(bug.title, u'Bug 1')
+        self.assertEqual(bug.submitter, u'anybody')
+        self.assertEqual(bug.status, u'new')
+        self.assertEqual(bug.priority, u'urgent')
+        self.assertEqual(bug.type, u'bug')
+        self.assertEqual(bug.release, u'None')
+        bug.owners.sort()
+        self.assertEqual(bug.owners, [u'zope.jim', u'zope.srichter'])
+        dc = IZopeDublinCore(bug)
+        self.assertEqual(dc.created, parseDatetimetz(u'2003-01-01T23:00:00'))
+        self.assertEqual(dc.modified, parseDatetimetz(u'2003-01-02T23:00:00'))
+        self.assertEqual(bug.description, u'This is Bug 1.')
+        self.assertEqual(bug.description.ttype, u'zope.source.stx')
+
+    def test_comment(self):
+        comment = self.tracker['1']['comment1']
+        dc = IZopeDublinCore(comment)
+        self.assertEqual(dc.created, parseDatetimetz(u'2003-01-01T13:00:00'))
+        self.assertEqual(dc.creators[0], u'zope.srichter')
+        self.assertEqual(comment.body, u'This is a comment.')
+        self.assertEqual(comment.body.ttype, u'zope.source.rest')
+
+    def test_attach(self):
+        attach = self.tracker['1']['document.gif']
+        dc = IZopeDublinCore(attach)
+        self.assertEqual(dc.created, parseDatetimetz(u'2003-01-01T14:00:00'))
+        self.assertEqual(dc.creators[0], u'zope.srichter')
+        # Type was set to 'File'
+        self.assert_(isinstance(attach, File))
+
+
+class ExportTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        tracker = BugTracker()
+        tracker.__parent__ = Root()
+        tracker.__name__ = 'tracker'
+        file = os.path.join(os.path.split(tests.__file__)[0], 'tracker.xml')
+        XMLImport(tracker).processXML(open(file))
+        self.xml = XMLExport(tracker).getXML()
+
+    def test_bugtracker(self):
+        self.assert_('<bugtracker version="1.0" title="Bug Tracker">' in
+                     self.xml)
+
+    def test_vocabulary(self):
+        self.assert_('<vocabulary name="Stati">' in self.xml)
+        self.assert_('<term value="closed" title="Closed"/>' in self.xml)
+        self.assert_('<term value="new" title="New" default=""/>' in self.xml)
+        self.assert_('<vocabulary name="Priorities">' in self.xml)
+        self.assert_('<term value="urgent" title="Urgent"/>' in self.xml)
+        self.assert_('<term value="low" title="Low" default=""/>' in self.xml)
+        self.assert_('<vocabulary name="BugTypes">' in self.xml)
+        self.assert_('<term value="bug" title="Bug" default=""/>' in self.xml)
+        self.assert_('<vocabulary name="Releases">' in self.xml)
+        self.assert_(
+          '<term value="None" title="(not specified)" default=""/>' in self.xml)
+
+    def test_bug(self):
+        self.assert_('id="1"' in self.xml)
+        self.assert_('title="Bug 1"' in self.xml)
+        self.assert_('submitter="anybody"' in self.xml)
+        self.assert_('status="new"' in self.xml)
+        self.assert_('priority="urgent"' in self.xml)
+        self.assert_('type="bug"' in self.xml)
+        self.assert_('release="None' in self.xml)
+        self.assert_('owners="jim, srichter' in self.xml or
+                     'owners="srichter, jim' in self.xml)
+        self.assert_('dependencies=""' in self.xml)
+        self.assert_('created="Jan 1, 2003 11:00:00 PM"' in self.xml)
+        self.assert_('modified="Jan 2, 2003 11:00:00 PM"' in self.xml)
+        self.assert_('<description ttype="zope.source.stx">'
+                     '\nThis is Bug 1.\n      '
+                     '</description>' in self.xml)
+
+    def test_comment(self):
+        self.assert_('id="comment1"' in self.xml)
+        self.assert_('created="Jan 1, 2003 1:00:00 PM"' in self.xml)
+        self.assert_('creator="srichter"' in self.xml)
+        self.assert_('ttype="zope.source.rest"' in self.xml)
+        self.assert_('>\nThis is a comment.\n        </comment>' in self.xml)
+
+    def test_attachment(self):
+        self.assert_('id="document.gif"' in self.xml)
+        self.assert_('created="Jan 1, 2003 2:00:00 PM"' in self.xml)
+        self.assert_('creator="srichter"' in self.xml)
+        self.assert_('type="File"' in self.xml)
+        
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(ImportTest),
+        unittest.makeSuite(ExportTest),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()

Added: Zope3/trunk/src/bugtracker/tests/test_xmlrpc.py
===================================================================
--- Zope3/trunk/src/bugtracker/tests/test_xmlrpc.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/test_xmlrpc.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,157 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""XML-RPC Representation Tests
+
+$Id: test_xmlrpc.py,v 1.4 2003/07/28 20:38:38 srichter Exp $
+"""
+import unittest
+import base64
+
+from zope.publisher.xmlrpc import TestRequest
+
+from zope.app.container.interfaces import IContainer
+from zope.app.file import File
+
+from bugtracker.bug import Bug
+from bugtracker.comment import Comment
+from bugtracker.tracker import BugTracker
+from bugtracker.tests.placelesssetup import PlacelessSetup, Root
+from bugtracker.xmlrpc import BugTrackerMethods, BugMethods
+from bugtracker.xmlrpc import CommentMethods, AttachmentMethods
+
+
+class TrackerMethodsTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+
+        tracker = self.generateTracker()
+        tracker.__parent__ = Root()
+        tracker.__name__ = "tracker"
+        tracker['1'] = self.generateBug('1')
+        tracker['2'] = self.generateBug('2')
+        self.tracker = tracker
+        self.methods = BugTrackerMethods(tracker, TestRequest())
+
+    def test_getBugNames(self):
+        self.assertEqual(list(self.methods.getBugNames()), ['1', '2'])
+
+    def test_addBug(self):
+        self.methods.addBug(u'Bug 3', u'This is bug 3.')
+        self.assertEqual(self.tracker['3'].title, u'Bug 3')
+        self.assertEqual(self.tracker['3'].description, u'This is bug 3.')
+        self.assertEqual(self.tracker['3'].status, u'new')
+        self.methods.addBug(u'Bug 4', u'This is bug 4.', owners=[u'jim'],
+                            dependencies=['3'])
+        self.assertEqual(self.tracker['4'].dependencies, [u'3'])
+        self.assertEqual(self.tracker['4'].owners, [u'zope.jim'])
+
+    def test_deleteBug(self):
+        self.methods.deleteBug('2')
+        self.assertEqual(list(self.tracker), ['1'])
+
+
+class BugMethodsTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        self.bug = self.generateBug('3')
+        self.methods = BugMethods(self.bug, TestRequest())
+
+    def test_getProperties(self):
+        props = self.methods.getProperties()
+        self.assertEqual(props['title'], 'Bug 3')
+        self.assertEqual(props['description'], 'This is Bug 3.')
+        self.assertEqual(props['type'], 'bug')
+        self.assertEqual(props['status'], 'new')
+        self.assertEqual(props['priority'], 'normal')
+        self.assertEqual(props['release'], 'None')
+        self.assertEqual(props['dependencies'], ())
+        self.assertEqual(props['owners'], ['jim', 'stevea'])
+
+    def test_setProperties(self):
+        self.methods.setProperties(type='feature')
+        self.assertEqual(self.bug.type, 'feature')
+        self.assertEqual(self.bug.status, 'new')
+        self.methods.setProperties(status='closed', release='zope_x3')
+        self.assertEqual(self.bug.type, 'feature')
+        self.assertEqual(self.bug.status, 'closed')
+        self.assertEqual(self.bug.release, 'zope_x3')
+        
+    def test_getCommentNames(self):
+        self.assertEqual(self.methods.getCommentNames(), ['comment1'])
+
+    def test_addComment(self):
+        self.methods.addComment('This is comment 2.')
+        self.assertEqual(self.bug['comment2'].body, 'This is comment 2.')
+
+    def test_deleteComment(self):
+        self.methods.deleteComment('comment1')
+        self.assert_('comment1' not in self.bug.keys())
+
+    def test_addAttachment(self):
+        self.methods.addAttachment('hw.txt',
+                                   base64.encodestring('Hello World.'))
+        self.assertEqual(self.bug['hw.txt'].data, 'Hello World.')
+
+    def test_deleteAttachment(self):
+        self.methods.deleteAttachment('attach.txt')
+        self.assert_('attach.txt' not in self.bug.keys())
+
+
+class CommentMethodsTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        self.comment = Comment()
+        self.comment.body = 'Comment 1'
+        self.methods = CommentMethods(self.comment, TestRequest())
+
+    def test_getBody(self):
+        self.assertEqual(self.methods.getBody(), 'Comment 1')
+
+    def test_setBody(self):
+        self.methods.setBody('C1')
+        self.assertEqual(self.comment.body, 'C1')
+
+
+class AttachmentMethodsTest(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        self.attach = File()
+        self.attach.data = 'Data 1'
+        self.methods = AttachmentMethods(self.attach, TestRequest())
+
+    def test_getData(self):
+        self.assertEqual(base64.decodestring(self.methods.getData()),
+                         'Data 1')
+
+    def test_setData(self):
+        self.methods.setData(base64.encodestring('Data 1'))
+        self.assertEqual(self.attach.data, 'Data 1')
+        
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(TrackerMethodsTest),
+        unittest.makeSuite(BugMethodsTest),
+        unittest.makeSuite(CommentMethodsTest),
+        unittest.makeSuite(AttachmentMethodsTest),
+        ))
+
+if __name__ == '__main__':
+    unittest.main()
+        
+    

Added: Zope3/trunk/src/bugtracker/tests/tracker.xml
===================================================================
--- Zope3/trunk/src/bugtracker/tests/tracker.xml	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tests/tracker.xml	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+<bugtracker version="1.0" title="Bug Tracker">
+
+  <vocabularies>
+    <vocabulary name="Stati">
+      <term value="closed" title="Closed"/>
+      <term value="new" title="New" default=""/>
+    </vocabulary>
+    <vocabulary name="Priorities">
+      <term value="urgent" title="Urgent"/>
+      <term value="low" title="Low" default=""/>
+    </vocabulary>
+    <vocabulary name="BugTypes">
+      <term value="bug" title="Bug" default=""/>
+    </vocabulary>
+    <vocabulary name="Releases">
+      <term value="None" title="(not specified)" default=""/>
+    </vocabulary>
+  </vocabularies>
+
+  <bugs>
+    <bug id="1" title="Bug 1" submitter="anybody"
+         status="new" priority="urgent" type="bug"
+         release="None" owners="jim, srichter" dependencies=""
+         created="Jan 1, 2003 11:00:00 PM "
+         modified="Jan 2, 2003 11:00:00 PM ">
+
+      <description ttype="zope.source.stx">
+This is Bug 1.
+      </description>
+
+      <comments>
+        <comment created="Jan 1, 2003 1:00:00 PM "
+                 id="comment1" creator="srichter"
+                 ttype="zope.source.rest">
+This is a comment.
+        </comment>
+      </comments>
+
+      <attachments>
+        <attachment created="Jan 1, 2003 2:00:00 PM "
+                    type="File" id="document.gif"
+                    creator="srichter">R0lGODlhEAAQAOYAAP///////v39/vz9/fr7/Pj7+/f5+vb5+vX4+fP2+PL29/L19/D09vD09e/z
+9e3y9Ovx8+rv8env8uju8ebt8OXt7+Ts7+Pr7eLq7uHq7d/o7N7o693n6tzm6tvl6dvk6dnk6Njk
+59fi5tXh5tTg5dPg5dLf49Hf5NDd48/d4s/c4c7c4c3c4c3c4Mva38ra4MnZ38nY3sjY3cfY3cbX
+3cbX3MTW3MTV28PU2sLU2sLT2cHT2cDS2b/R2L3R2L3Q17zP17vP1rvO1brO1bnN1LjN1LbM1LbL
+07XL0rTK0bPJ0bLI0LHH0LDHz6/Gzq7Ezq3EzavDzKnCy6jByqbAyaS+yKK9x6C7xZ66xJu/zJi2
+wY2uukZncwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwA
+AAAAEAAQAAAHmoAAgoOEhYRbiImIXIYAW4IBAwYLj4yFjwAECAwQmJaDWwIFCQ4RFVtcqZ+OBwoP
+ExcbIFm0q1sNEBQZHSImLDJZthIWGh8jKC41OsGHGBwhJSowNjxCzKAeIictMzg+REnXglskKS81
+O0BGS0/ijisxNz1DSE1RVO5bNDk/RUpOUqpcybcjyBEmUKZYwaIlH62HEN2pmkgRQCAAOw==
+</attachment>
+      </attachments>
+
+    </bug>
+  </bugs>
+
+</bugtracker>

Added: Zope3/trunk/src/bugtracker/tracker.py
===================================================================
--- Zope3/trunk/src/bugtracker/tracker.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/tracker.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,80 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+# 
+##############################################################################
+"""A Bug Tracker implementation
+
+$Id$
+"""
+import re
+from persistent import Persistent
+
+from zope.interface import implements
+
+from zope.app.dublincore.interfaces import IZopeDublinCore
+from zope.app.container.btree import BTreeContainer
+from zope.app.container.interfaces import INameChooser
+
+from bugtracker.interfaces import IBugTracker
+from bugtracker.interfaces import IBugContainer
+
+
+class BugTracker(BTreeContainer):
+    """A BTree-based IBugTracker implementation.
+
+    Internally bugs are identified as integer ids. Unfortunately, the
+    IContainer interface expects ids to be strings, so we convert the integers
+    to strings for this purpose. This is also the reason we do not use
+    BTreeContainer, since it uses OOBTree, but we can use IOBTree now.
+    """
+    implements(IBugContainer, IBugTracker)
+
+    def setTitle(self, title):
+        """See zopeproducts.bugtracker.interfaces.IBugTracker"""
+        dc = IZopeDublinCore(self)
+        dc.title = title
+
+    def getTitle(self):
+        """See zopeproducts.bugtracker.interfaces.IBugTracker"""
+        dc = IZopeDublinCore(self)
+        return dc.title
+
+    # See zopeproducts.bugtracker.interfaces.IBugTracker
+    title = property(getTitle, setTitle)
+
+
+int_re = re.compile('^[0-9]*$')
+    
+class BugTrackerNameChooser:
+    """An adapter to choose names for bugs."""
+
+    implements(INameChooser)
+    __used_for__ = IBugTracker
+
+    def __init__(self, context):
+        self.context = context
+
+    def chooseName(self, name, message):
+        # It is sometimes necessary to force in a name, since bugs might refer
+        # to each other. This is particularly important when importing XML
+        # data.
+        if isinstance(name, (str, unicode)) and name.startswith('bug'):
+            name = name[3:]
+        else:
+            num_ids = [int(id) for id in self.context.keys()
+                       if int_re.match(id) is not None] + [0]
+            name = str(max(num_ids)+1)
+        return name
+
+    def checkName(self, name, message):
+        int(name)
+        return True

Added: Zope3/trunk/src/bugtracker/vocabulary.py
===================================================================
--- Zope3/trunk/src/bugtracker/vocabulary.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/vocabulary.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,255 @@
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+# 
+##############################################################################
+"""Vocabularies for the Bug Tracker
+
+$Id: vocabulary.py,v 1.7 2004/03/18 18:04:54 philikon Exp $
+"""
+from persistent import Persistent
+from persistent.dict import PersistentDict
+
+from zope.interface import implements
+from zope.schema.interfaces import ITokenizedTerm
+from zope.schema.interfaces import IVocabulary, IVocabularyTokenized
+from zope.schema.vocabulary import getVocabularyRegistry
+from zope.security.proxy import trustedRemoveSecurityProxy 
+
+from zope.app import zapi
+from zope.app.annotation.interfaces import IAnnotatable, IAnnotations
+
+from bugtracker.interfaces import IManagableVocabulary, IBugTracker
+from bugtracker.interfaces import IStatusVocabulary
+from bugtracker.interfaces import IReleaseVocabulary
+from bugtracker.interfaces import IPriorityVocabulary 
+from bugtracker.interfaces import IBugTypeVocabulary
+from bugtracker import TrackerMessageID as _
+
+
+class SimpleTerm(Persistent):
+    """A persistent vocabulary term.""" 
+    implements(ITokenizedTerm)
+
+    def __init__(self, value, title):
+        self.value = value
+        self.title = title
+
+    def getToken(self):
+        return self.value
+
+    token = property(getToken)
+    
+
+class ManagableVocabulary(object):
+    """A vocabulary that stores its terms on an object.
+
+    The terms of the vocabulary are persistent, so that they can be stored on
+    the object in the ZODB. Annotations are used to store the terms.
+    """
+    implements(IManagableVocabulary)
+    __used_for__ = IAnnotatable
+
+    key = None
+
+    interface = None
+
+    def __init__(self, context):
+        self.context = self._getRealContext(context)
+        self.annotations = IAnnotations(self.context)
+        if not self.annotations.get(self.key):
+            self.annotations[self.key] = PersistentDict()
+            self.annotations[self.key+'/default'] = None
+    
+    def __contains__(self, value):
+        return value in self.annotations[self.key].keys()
+    
+    def __iter__(self):
+        return iter(self.annotations[self.key].values())
+    
+    def __len__(self):
+        return len(self.annotations[self.key])
+    
+    def getQuery(self):
+        return None
+    
+    def getTerm(self, value):
+        return self.annotations[self.key][value]
+
+    def getTermByToken(self, token):
+        return self.getTerm(token)
+    
+    def add(self, value, title, default=False):
+        self.annotations[self.key][value] = SimpleTerm(value, title)
+        if default:
+            self.default = value
+
+    def delete(self, value):
+        if value == self.default.value:
+            error_msg = _("Cannot delete default value '${value}'.")
+            error_msg.mapping = {'value': value}
+            raise ValueError, error_msg
+        del self.annotations[self.key][value]
+
+    def _getRealContext(self, context):
+        for obj in zapi.getParents(context):
+            if self.interface.providedBy(obj):
+                return obj
+        return context
+
+    def getDefault(self):
+        value = self.annotations[self.key+'/default']
+        if value is None:
+            return None
+        return self.getTerm(self.annotations[self.key+'/default'])
+
+    def setDefault(self, value):
+        """Set the default value/term. Both, a token and a term are
+        accepted."""
+        if ITokenizedTerm.providedBy(value):
+            value = value.value
+        if value not in self:
+            error_msg = _("The value '${value}' was not found in the "
+                          "vocabulary")
+            error_msg.mapping = {'value': value}
+            raise ValueError, error_msg
+        self.annotations[self.key+'/default'] = value
+
+    default = property(getDefault, setDefault)
+
+
+class StatusVocabulary(ManagableVocabulary):
+
+    implements(IStatusVocabulary)
+
+    key = 'bugtracker.status.values'
+    interface = IBugTracker
+
+    title = _('Status Definitions')
+
+
+class ReleaseVocabulary(ManagableVocabulary):
+
+    implements(IReleaseVocabulary)
+
+    key = 'bugtracker.release.values'
+    interface = IBugTracker
+
+    title = _('Release Definitions')
+
+
+class PriorityVocabulary(ManagableVocabulary):
+
+    implements(IPriorityVocabulary)
+
+    key = 'bugtracker.priority.values'
+    interface = IBugTracker
+
+    title = _('Priority Definitions')
+
+
+class BugTypeVocabulary(ManagableVocabulary):
+
+    implements(IBugTypeVocabulary)
+
+    key = 'bugtracke.bugtype.values'
+    interface = IBugTracker
+
+    title = _('Bug Type Definitions')
+
+
+class UserTerm(Persistent):
+
+    implements(ITokenizedTerm)
+
+    def __init__(self, principal):
+        # This is safe here, since we only read non-critical data
+        naked = trustedRemoveSecurityProxy(principal)
+        self.principal = {'id': naked.id,
+                          'login': naked.getLogin(),
+                          'title': naked.title,
+                          'description': naked.description}
+        self.value = naked.id
+        self.token = naked.id
+        self.title = naked.title
+
+
+class UserVocabulary(object):
+
+    implements(IVocabulary, IVocabularyTokenized)
+
+    def __init__(self, context):
+        self.auth = zapi.getService(zapi.servicenames.Authentication)
+    
+    def __contains__(self, value):
+        ids = map(lambda p: p.id, self.auth.getPrincipals(''))
+        return value in ids
+    
+    def __iter__(self):
+        terms = map(lambda p: UserTerm(p), self.auth.getPrincipals(''))
+        return iter(terms)
+    
+    def __len__(self):
+        return len(self.auth.getPrincipals(''))
+    
+    def getQuery(self):
+        return None
+    
+    def getTerm(self, value):
+        return UserTerm(self.auth.getPrincipal(value))
+
+    def getTermByToken(self, token):
+        return self.getTerm(token)
+
+
+class VocabularyPropertyGetter(object):
+    
+    def __init__(self, name, vocab_name):
+        self.name = name
+        self._vocab_name = vocab_name
+
+    def __call__(self, instance):
+        registry = getVocabularyRegistry()
+        try:
+            vocab = registry.get(instance, self._vocab_name)
+            default = vocab.default.value
+        except TypeError:
+            # We cannot assume that the bug will always have a context to
+            # find the vocabulary data. In these cases, we just skip the
+            # default lookup.
+            default = None
+        return getattr(instance, self.name, default)
+
+
+class VocabularyPropertySetter(object):
+    """This generic vocabulary property setter class ensures that the set
+    value is in the vocabulary."""
+    
+    def __init__(self, name, vocab_name):
+        self.name = name
+        self._vocab_name = vocab_name
+
+    def __call__(self, instance, value):
+        registry = getVocabularyRegistry()
+        try:
+            vocab = registry.get(instance, self._vocab_name)
+            if value not in vocab:
+                raise ValueError, \
+                      "The value '%s' was not found in vocabulary '%s'" %(
+                    value, self._vocab_name)
+        except TypeError:
+            # We cannot assume that the bug will always have a context to
+            # find the vocabulary data. In these cases, we just skip the
+            # verification.
+            vocab = None
+
+        # Make sure the value is a message id
+        setattr(instance, self.name, _(value))

Added: Zope3/trunk/src/bugtracker/xmlrpc.py
===================================================================
--- Zope3/trunk/src/bugtracker/xmlrpc.py	2004-07-06 18:37:21 UTC (rev 26116)
+++ Zope3/trunk/src/bugtracker/xmlrpc.py	2004-07-06 18:43:12 UTC (rev 26117)
@@ -0,0 +1,212 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""XML-RPC methods for the Bug Tracker and Bugs
+
+XML-RPC might be the best method to support mail-input. 
+
+$Id: xmlrpc.py,v 1.1 2003/07/28 10:21:06 srichter Exp $
+"""
+import base64
+
+from zope.event import notify
+from zope.publisher.xmlrpc import MethodPublisher
+from zope.schema.vocabulary import getVocabularyRegistry
+
+from zope.app import zapi
+from zope.app.container.contained import contained
+from zope.app.container.interfaces import INameChooser
+from zope.app.event.objectevent import ObjectCreatedEvent, ObjectModifiedEvent
+from zope.app.file import File, Image
+
+from bugtracker.bug import Bug
+from bugtracker.comment import Comment
+from bugtracker.interfaces import IComment, IBugDependencies
+
+
+class UnknownEncoding(Exception):
+    """We specify encodings for the attachment data, since it is expected that
+    they are often binary data. This exception is raised, if none of the
+    available encodings was chosen."""
+    
+
+class BugTrackerMethods(MethodPublisher):
+    """XML-RPC methods for the Bug Tracker object."""
+
+    def getBugNames(self):
+        """Get a list of all bugs."""
+        return list(self.context.keys())
+  
+    def addBug(self, title, description, type=u'bug', status=u'new',
+               priority=u'normal', release=u'None', owners=None,
+               dependencies=None):
+        """Add a message. Returns the id of the bug."""
+        bug = Bug()
+        bug.title = title
+        bug.description = description
+        bug.type = type
+        bug.status = status
+        bug.priority = priority
+        bug.release = release
+        if owners is not None:
+            registry = getVocabularyRegistry()
+            vocab = registry.get(self.context, 'Users')
+            owner_ids = []
+            for term in vocab:
+                if term.principal['login'] in owners:
+                    owner_ids.append(term.value)
+            bug.owners = owner_ids
+        if dependencies is not None:
+            bug.dependencies = dependencies
+        chooser = INameChooser(self.context)
+        self.context[chooser.chooseName('', bug)] = bug
+        return zapi.name(bug)
+  
+    def deleteBug(self, name):
+        """Delete a bug. Return True, if successful."""
+        self.context.__delitem__(name)
+        return True 
+
+
+class BugMethods(MethodPublisher):
+
+    def getProperties(self):
+        registry = getVocabularyRegistry()
+        vocab = registry.get(self.context, 'Users')
+        owners = map(lambda o: vocab.getTerm(o).principal['login'],
+                     self.context.owners)        
+        deps = IBugDependencies(self.context)
+        return {'title' : self.context.title,
+                'description' : self.context.description,
+                'type' : self.context.type,
+                'status' : self.context.status,
+                'priority' : self.context.priority,
+                'release' : self.context.release,
+                'owners' : owners,
+                'dependencies' : deps.dependencies
+                }
+
+    def setProperties(self, title=None, description=None, type=None,
+                      status=None, priority=None, release=None,
+                      owners=None, dependencies=None):
+        """Set the properties of the bug."""
+        bug = self.context
+        if title is not None:
+            bug.title = title
+        if description is not None:
+            bug.description = description
+        if type is not None:
+            bug.type = type
+        if status is not None:
+            bug.status = status
+        if priority is not None:
+            bug.priority = priority
+        if release is not None:
+            bug.release = release
+        if owners is not None:
+            registry = getVocabularyRegistry()
+            vocab = registry.get(self.context, 'Users')
+            owner_ids = []
+            for term in vocab:
+                if term.principal['login'] in owners:
+                    owner_ids.append(term.value)
+            bug.owners = owner_ids
+        if dependencies is not None:
+            deps = IBugDependencies(bug)
+            deps.dependencies = dependencies
+        notify(ObjectModifiedEvent(bug))
+        return True
+
+    def getCommentNames(self):
+        """Get the names (ids) of the comments for this bug."""
+        names = []
+        for name, obj in self.context.items():
+            if IComment.providedBy(obj):
+                names.append(name)
+        return names
+
+    def addComment(self, body):
+        """Add a comment to the bug."""
+        comment = Comment()
+        comment.body = body
+        names = filter(lambda n: n.startswith('comment'), self.context.keys())
+        int_names = map(lambda n: int(n[7:]), names)
+        name = 'comment1'
+        if int_names:
+            name = 'comment' + str(max(int_names)+1)
+        self.context[name] = comment
+        return zapi.name(comment)
+
+    def deleteComment(self, name):
+        """Delete a Comment. Return True, if successful."""
+        self.context.__delitem__(name)
+        return True 
+
+    def getAttachmentNames(self):
+        """Get the names (ids) of the attachments for this bug."""
+        namess = []
+        for name, obj in self.context.items():
+            if not IComment.providedBy(obj):
+                names.append(name)
+        return names
+
+    def addAttachment(self, name, data, type="File", encoding="base64"):
+        """Add an attachment to the bug."""
+        if type == 'Image':
+            attach = Image()
+        else:
+            attach = File()
+        if encoding == 'base64':
+            attach.data = base64.decodestring(data)
+        else:
+            raise UnknownEncoding, 'The encoding is not known: %s' %encoding
+        attach = contained(attach, self.context, name=name)
+        self.context[name] = attach
+        return zapi.name(attach)
+
+    def deleteAttachment(self, name):
+        """Delete an Attachment. Return True, if successful."""
+        self.context.__delitem__(name)
+        return True 
+
+
+class CommentMethods(MethodPublisher):
+    """XML-RPC Methods for a Bug Comment object."""
+
+    def getBody(self):
+        """Get the contents/body of the comment."""
+        return self.context.body
+
+    def setBody(self, body):
+        """Give the comment new contents.""" 
+        self.context.body = body
+        return True
+
+
+class AttachmentMethods(MethodPublisher):
+    """XML-RPC Methods for Bug Attachments."""
+
+    def getData(self, encoding='base64'):
+        """Return the data of the attachment in the specified encoding."""
+        if encoding == 'base64':
+            return base64.encodestring(self.context.data)
+        else:
+            raise UnknownEncoding, 'The encoding is not known: %s' %encoding 
+
+    def setData(self, data, encoding='base64'):
+        """Set the data of the attachment converting from the specified
+        encoding."""
+        if encoding == 'base64':
+            self.context.data = base64.decodestring(data)
+        else:
+            raise UnknownEncoding, 'The encoding is not known: %s' %encoding 



More information about the Zope3-Checkins mailing list