[Zope3-checkins] SVN: Zope3/trunk/ Added the z3checkins product to the Zope 3 tree.

Gintautas Miliauskas gintas at pov.lt
Wed May 19 06:25:42 EDT 2004


Log message for revision 24818:
Added the z3checkins product to the Zope 3 tree.



-=-
Added: Zope3/trunk/package-includes/z3checkins-configure.zcml
===================================================================
--- Zope3/trunk/package-includes/z3checkins-configure.zcml	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/package-includes/z3checkins-configure.zcml	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1 @@
+<include package="z3checkins" />


Property changes on: Zope3/trunk/package-includes/z3checkins-configure.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/README
===================================================================
--- Zope3/trunk/src/z3checkins/README	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/README	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,104 @@
+Zope 3 Checkins
+===============
+
+This is a Zope 3 product for keeping track of <zope3-checkins at zope.org> mailing
+list.  It adds a new content object type: CheckinMessage.  You can upload all
+messages from the mailing list to your Zope 3 instance with a simple procmail
+rule, and then view the latest checkins in your Mozilla sidebar or any news
+aggregator that supports RSS, e.g. Nautilus.
+
+It is also quite usable for other checkin-tracking mailing lists.  However
+since there is no single standard on how the messages should be formatted,
+your mileage may vary.
+
+
+Installation
+------------
+
+Start up Zope 3 and add a Checkin Folder. You will be presented with a form.
+
+  - the first field is a description used for checkins.rss view
+  - the second field is a URL to the traditional mailing list archive.
+  - the third field is a list of icon definitions used for checkins to
+    different parts of the source trees.  Each line should contain four
+    fields:
+
+      prefix  icon-name  alt-text  title
+
+    prefix is matched against the beginning of the checkin directory (put
+    longer, more specific prefixes first.  * is a catch-all prefix, and
+    should be placed last).
+
+    icon-name is a Zope 3 resource name.  z3checkins comes with the following
+    icons: zope3.png, product.png, message.png
+
+    alt-text is a short alternate text
+
+    title is a longer description, usually shown in a tooltip
+
+You would use the following configuration for zope3-checkins at zope.org:
+
+  RSS view description:
+  Latest Zope 3 Checkins
+
+  URL of mailing list:
+  http://mail.zope.org/pipermail/zope3-checkins/
+
+  Icon definitions:
+  Zope3  zope3.png    Z3       Zope 3 core
+  *      product.png  Product  Zope 3 product
+
+Then, go to the Metadata tab and set the title.
+
+Now you can create Checkin Messages in that folder.  In the rest of this
+document I assume Zope 3 is accessible at http://localhost:8080/ and that
+the checkin folder is called 'zope3-checkins'.
+
+
+Upload script
+-------------
+
+Here's an example of a shell script that uses curl to upload a file or a list
+of files, each containing a single RFC822 mail message:
+
+  #!/bin/sh
+  for file in "$@"; do
+      curl -F field.data=@- -F UPDATE_SUBMIT=Submit -s -S \
+           -u username:password \
+           http://localhost:8080/zope3-checkins/+/CheckinMessage \
+           < "$file" > /dev/null
+  done
+
+Replace username:password with the username and password of a Zope 3 user that
+has zope.ManageContent permission in the 'zope3-checkins' folder.
+
+This script may be used to import archives in Maildir format.  Or you can
+use formail or some other tool to split mbox folders available at
+http://mail.zope.org/pipermail/zope3-checkins/
+
+
+Procmail
+--------
+
+Add this to your .procmailrc to upload new messages automatically:
+
+:0:
+* ^List-Id:.*<zope3-checkins\.zope\.org>
+| curl -F field.data=@- -F UPDATE_SUBMIT=Submit -s -S -u username:password \
+       http://localhost:8080/zope3-checkins/+/CheckinMessage > /dev/null
+
+Replace username:password with the username and password of a Zope 3 user that
+has zope.ManageContent permission in the 'zope3-checkins' folder.
+
+
+RSS feed
+--------
+
+Use the following URL to access last checkins in RSS format:
+
+  http://localhost:8080/zope3-checkins/checkins.rss
+
+
+Good luck,
+Marius Gedminas
+<marius at pov.lt>


Property changes on: Zope3/trunk/src/z3checkins/README
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/TODO
===================================================================
--- Zope3/trunk/src/z3checkins/TODO	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/TODO	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,19 @@
+I definitely want to do the following:
+
+- Make sure non-ASCII chars work both in headers and in bodies
+
+I'm not sure I'll find time for these:
+
+- Is storing all the messages in a single folder scalable enough?
+- Highlight branch tags in message body
+- Highlight quoted text in normal message bodies
+- Show checkin times in the user's timezone instead of server's (this probably
+  needs some configuration page and cookies)
+- Replace newlines with <br/> elements in checkin messages in message_part.pt
+- Shorten descriptions in message_part.pt to first 100 chars (words? sentences?
+  lines?)
+- Add links to the specific versions of the files mentioned in the CVS web
+  interface
+- Filtering (by branch, by author, etc)
+- Threading?
+- Verify how all this works in other browsers beside Mozilla


Property changes on: Zope3/trunk/src/z3checkins/TODO
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/__init__.py
===================================================================
--- Zope3/trunk/src/z3checkins/__init__.py	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/__init__.py	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,4 @@
+"""
+Zope 3 product for processing zope3-checkins mailing list messages and
+presenting them in various formats.
+"""


Property changes on: Zope3/trunk/src/z3checkins/__init__.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/bookmark.pt
===================================================================
--- Zope3/trunk/src/z3checkins/bookmark.pt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/bookmark.pt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1 @@
+<hr />


Property changes on: Zope3/trunk/src/z3checkins/bookmark.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/branch.png
===================================================================
(Binary files differ)


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

Added: Zope3/trunk/src/z3checkins/configure.zcml
===================================================================
--- Zope3/trunk/src/z3checkins/configure.zcml	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/configure.zcml	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,256 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser"
+    i18n_domain='z3checkins'
+    >
+
+<!-- CheckinMessage content object -->
+
+  <content class=".message.Message">
+
+    <require permission="zope.View"
+             interface=".interfaces.IMessage" />
+
+    <implements
+      interface="zope.app.annotation.interfaces.IAttributeAnnotatable" />
+
+  </content>
+
+  <content class=".message.CheckinMessage">
+
+    <require permission="zope.View"
+             interface=".interfaces.ICheckinMessage" />
+
+    <implements
+      interface="zope.app.annotation.interfaces.IAttributeAnnotatable" />
+
+  </content>
+
+  <content class=".folder.CheckinFolder">
+
+     <require permission="zope.View"
+              interface="zope.app.container.interfaces.IReadContainer" />
+
+     <require permission="zope.ManageContent"
+              interface="zope.app.container.interfaces.IWriteContainer" />
+
+     <require permission="zope.ManageContent"
+              set_schema=".interfaces.ICheckinFolder" />
+
+     <require permission="zope.View"
+              interface=".interfaces.ICheckinFolderSchema" />
+
+     <factory id="CheckinFolder"
+              title="Checkin Folder"
+              description="A checkin folder" />
+
+     <implements
+       interface="zope.app.annotation.interfaces.IAttributeAnnotatable" />
+
+  </content>
+
+<!-- Utilities -->
+
+  <adapter for="zope.app.container.interfaces.IReadContainer"
+           factory=".message.MessageContainerAdapter"
+           permission="zope.View"
+           provides=".interfaces.IMessageArchive" />
+
+  <adapter for=".interfaces.ICheckinFolder"
+           factory=".folder.MessageNameChooser"
+           permission="zope.View"
+           provides="zope.app.container.interfaces.INameChooser" />
+
+  <adapter for=".interfaces.IMessage"
+           factory=".folder.MessageSized"
+           permission="zope.View"
+           provides="zope.app.size.interfaces.ISized" />
+
+
+  <utility factory=".message.CheckinMessageParser"
+           permission="zope.View"
+           provides=".interfaces.IMessageParser" />
+
+<!-- Generic views for date/time formatting -->
+
+  <!-- XXX: there should be an interface that datetime.datetime implements -->
+
+  <view
+    for="*"
+    name="rfc822"
+    factory=".message.RFCDateTimeFormatter"
+    type="zope.publisher.interfaces.http.IHTTPRequest"
+    permission="zope.Public"
+    />
+
+  <view
+    for="*"
+    name="isodatetime"
+    factory=".message.ISODateTimeFormatter"
+    type="zope.publisher.interfaces.http.IHTTPRequest"
+    permission="zope.Public"
+    />
+
+<!-- Browser views: adding -->
+
+  <browser:addform
+    name="CheckinMessage"
+    schema="zope.app.file.interfaces.IFile"
+    fields="data"
+    label="Upload a checkin message"
+    permission="zope.ManageContent"
+    class=".message.MessageUpload" />
+
+  <browser:addMenuItem
+    title="Checkin message"
+    class=".message.MessageUpload"
+    permission="zope.ManageContent"
+    view="CheckinMessage" />
+
+  <browser:addform
+    name="CheckinFolder"
+    schema=".interfaces.ICheckinFolderSchema"
+    fields="description archive_url icons"
+    label="Create a checkin message folder"
+    permission="zope.ManageContent"
+    content_factory=".folder.CheckinFolder" />
+
+  <browser:addMenuItem
+    title="Checkin Folder"
+    class=".folder.CheckinFolder"
+    permission="zope.ManageContent"
+    view="CheckinFolder" />
+
+  <browser:view
+    name="+"
+    menu="zmi_actions" title="Add"
+    for=".interfaces.ICheckinFolder"
+    permission="zope.ManageContent"
+    class="zope.app.container.browser.adding.Adding">
+
+   <page name="index.html" attribute="index" />
+   <page name="action.html" attribute="action" />
+
+  </browser:view>
+
+  <browser:editform
+    name="EditFolder"
+    schema=".interfaces.ICheckinFolder"
+    label="Change properties of a checkin message folder"
+    menu="zmi_views" title="Edit"
+    permission="zope.ManageContent" />
+
+<!-- Browser views: email message -->
+
+  <browser:page
+    for=".interfaces.IMessage"
+    name="rss"
+    class=".message.MessageRSSView"
+    attribute="index"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.IMessage"
+    name="html"
+    template="message_part.pt"
+    class=".message.MessageView"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.IMessage"
+    name="html-sidebar"
+    template="message_sidebar.pt"
+    class=".message.MessageView"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.IMessage"
+    name="index.html"
+    template="message.pt"
+    class=".message.MessageView"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.IMessage"
+    name="index.txt"
+    template="message_plain.pt"
+    class=".message.MessageView"
+    permission="zope.View" />
+
+<!-- Browser views: checkin message -->
+
+  <browser:page
+    for=".interfaces.ICheckinMessage"
+    name="rss"
+    class=".message.MessageRSSView"
+    attribute="index"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.ICheckinMessage"
+    name="html"
+    template="message_part.pt"
+    class=".message.CheckinMessageView"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.ICheckinMessage"
+    name="html-sidebar"
+    template="message_sidebar.pt"
+    class=".message.CheckinMessageView"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.ICheckinMessage"
+    name="index.html"
+    template="message.pt"
+    class=".message.CheckinMessageView"
+    menu="zmi_views" title="Preview"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.ICheckinMessage"
+    name="index.txt"
+    template="message_plain.pt"
+    class=".message.CheckinMessageView"
+    permission="zope.View" />
+
+<!-- Browser views: bookmark -->
+
+  <browser:page
+    for=".interfaces.IBookmark"
+    name="html"
+    template="bookmark.pt"
+    permission="zope.View" />
+
+<!-- Browser views: containers -->
+
+  <browser:page
+    for=".interfaces.ICheckinFolder"
+    name="checkins.rss"
+    template="rss_container.pt"
+    class=".message.ContainerView"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.ICheckinFolder"
+    name="index.html"
+    template="container.pt"
+    class=".message.ContainerView"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.ICheckinFolder"
+    name="checkins-sidebar.html"
+    template="container_sidebar.pt"
+    class=".message.ContainerView"
+    permission="zope.View" />
+
+<!-- Resources -->
+
+  <browser:resource name="message.png" file="message.png" />
+  <browser:resource name="zope3.png" file="zope3.png" />
+  <browser:resource name="product.png" file="product.png" />
+  <browser:resource name="branch.png" file="branch.png" />
+
+</configure>


Property changes on: Zope3/trunk/src/z3checkins/configure.zcml
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/container.pt
===================================================================
--- Zope3/trunk/src/z3checkins/container.pt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/container.pt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,69 @@
+<html>
+<head>
+<title tal:content="view/title">Zope 3 Checkins</title>
+<style type="text/css">
+  * { font-size: small; }
+  h1 { font-size: medium; margin-bottom: 0.5ex; }
+  div.toolbar { font-size: xx-small; margin-left: 1em; margin-bottom: 1.5em; }
+  div.toolbar > a { display: block; width: 100%; }
+  div.navigation { margin-top: 1em; }
+  div.navigation > a { display: block; width: 100%; text-align: right; }
+  .message { margin-top: 1ex; }
+  .description { font-size: small; margin-left: 1em; }
+  .same { color: gray; margin-top: 0.5ex; }
+  .author { font-weight: bold; }
+  a.title { display: block; width: 100%; }
+  a:hover { background: #e0e6ff; }
+  img.icon { float: right; padding: 0ex; margin: 2px; border: none; }
+  hr { width: 60%; border: none; background: gray; height: 1px;
+       margin-top: 1ex; margin-bottom: 0ex; }
+</style>
+<style type="text/css">
+  .description { white-space: pre; }
+</style>
+</head>
+<body tal:define="dummy view/placeBookmark;
+                  start python:int(request.get('start', '0'));
+                  size python:int(request.get('size', '20'));
+                  opt_size python:(size != 20) and '&amp;size=%d' % size or ''">
+<h1 tal:replace="view/title">Zope 3 Checkins</h1>
+
+<div class="toolbar"
+     tal:define="first_batch python:start <= 0">
+<tal:block tal:replace='structure string:
+<script language="JavaScript" type="text/javascript">
+<!--
+if ((typeof window.sidebar == "object") && (typeof window.sidebar.addPanel == "function")) {
+  url = "${context/@@absolute_url}/@@checkins-sidebar.html";
+  document.write("&lt;a href=\"javascript:window.sidebar.addPanel(&#39;${view/title}&#39;, &#39;" + url + "&#39;, &#39;&#39;);\">Add to sidebar&lt;/a>");
+}
+//-->
+</script>
+'/>
+<a tal:condition="view/archive_url"
+   tal:attributes="href view/archive_url"
+   href="http://mail.zope.org/pipermail/zope3-checkins/">List archives</a>
+<a tal:condition="first_batch"
+   href="javascript:window.location.reload()">Refresh</a>
+<a tal:condition="not: first_batch"
+   tal:attributes="href request/URL">Newest checkins</a>
+</div>
+
+<div class="navigation">
+<a tal:define="prev python:max(0, start - size)"
+   tal:condition="python: start > 0"
+   tal:attributes="href string:${request/URL}?start=${prev}${opt_size}">Previous
+   <span tal:replace="size">20</span></a>
+</div>
+
+<div tal:replace="structure view/renderCheckins" />
+
+<div class="navigation">
+<a tal:define="next python:start + size"
+   tal:condition="python: next < view.count()"
+   tal:attributes="href string:${request/URL}?start=${next}${opt_size}">Next
+   <span tal:replace="size">20</span></a>
+</div>
+
+</body>
+</html>


Property changes on: Zope3/trunk/src/z3checkins/container.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/container_sidebar.pt
===================================================================
--- Zope3/trunk/src/z3checkins/container_sidebar.pt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/container_sidebar.pt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,57 @@
+<html>
+<head>
+<title tal:content="view/title">Zope 3 Checkins</title>
+<style type="text/css">
+  * { font-size: small; }
+  h1 { font-size: medium; margin-bottom: 0.5ex; }
+  div.toolbar { font-size: xx-small; margin-left: 1em; margin-bottom: 1.5em; }
+  div.toolbar > a { display: block; width: 100%; }
+  div.navigation { margin-top: 1em; }
+  div.navigation > a { display: block; width: 100%; text-align: right; }
+  .message { margin-top: 1ex; }
+  .description { font-size: small; margin-left: 1em; }
+  .same { color: gray; margin-top: 0.5ex; }
+  .author { font-weight: bold; }
+  a.title { display: block; width: 100%; }
+  a:hover { background: #e0e6ff; }
+  img.icon { float: right; padding: 0ex; margin: 2px; border: none; }
+  hr { width: 60%; border: none; background: gray; height: 1px;
+       margin-top: 1ex; margin-bottom: 0ex; }
+</style>
+</head>
+<body tal:define="dummy view/placeBookmark;
+                  start python:int(request.get('start', '0'));
+                  size python:int(request.get('size', '20'));
+                  opt_size python:(size != 20) and '&amp;size=%d' % size or ''">
+<h1 tal:replace="view/title">Zope 3 Checkins</h1>
+
+<div class="toolbar"
+     tal:define="first_batch python:start <= 0">
+<a tal:condition="view/archive_url"
+   tal:attributes="href view/archive_url"
+   target="_content"
+   href="http://mail.zope.org/pipermail/zope3-checkins/">List archives</a>
+<a tal:condition="first_batch"
+   href="javascript:window.location.reload()">Refresh</a>
+<a tal:condition="not: first_batch"
+   tal:attributes="href request/URL">Newest checkins</a>
+</div>
+
+<div class="navigation">
+<a tal:define="prev python:max(0, start - size)"
+   tal:condition="python: start > 0"
+   tal:attributes="href string:${request/URL}?start=${prev}${opt_size}">Previous
+   <span tal:replace="size">20</span></a>
+</div>
+
+<div tal:replace="structure view/renderCheckins" />
+
+<div class="navigation">
+<a tal:define="next python:start + size"
+   tal:condition="python: next < view.count()"
+   tal:attributes="href string:${request/URL}?start=${next}${opt_size}">Next
+   <span tal:replace="size">20</span></a>
+</div>
+
+</body>
+</html>


Property changes on: Zope3/trunk/src/z3checkins/container_sidebar.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/folder.py
===================================================================
--- Zope3/trunk/src/z3checkins/folder.py	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/folder.py	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,52 @@
+"""
+Python code for z3checkins product.
+
+Checkin message folder handling.
+
+$Id: folder.py,v 1.5 2004/03/14 10:56:48 gintautasm Exp $
+"""
+
+from zope.interface import implements
+from zope.app.container.btree import BTreeContainer
+from zope.app.container.interfaces import INameChooser
+from zope.app.container.interfaces import IContainerNamesContainer
+from zope.app.size.interfaces import ISized
+from interfaces import ICheckinFolder
+
+class CheckinFolder(BTreeContainer):
+    """A message folder."""
+
+    implements(ICheckinFolder, IContainerNamesContainer)
+
+
+class MessageNameChooser:
+    """An adapter to choose names for messages."""
+
+    implements(INameChooser)
+
+    def __init__(self, context):
+        pass
+
+    def chooseName(self, name, message):
+        return message.message_id
+
+    def checkName(self, name, message):
+        return name == message.message_id
+
+
+class MessageSized:
+    """An adapter to calculate size of a message."""
+    implements(ISized)
+
+    def __init__(self, message):
+        self._message = message
+
+    def sizeForSorting(self):
+        return len(self._message.full_text)
+
+    def sizeForDisplay(self):
+        bytes = len(self._message.full_text)
+        if bytes < 1024:
+            return u'%d bytes' % bytes
+        else:
+            return u'%d KB' % (bytes / 1024)


Property changes on: Zope3/trunk/src/z3checkins/folder.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/ftests/__init__.py
===================================================================
--- Zope3/trunk/src/z3checkins/ftests/__init__.py	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/ftests/__init__.py	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,5 @@
+"""
+Functional tests for z3checkins.
+
+$Id: __init__.py,v 1.1 2003/08/01 09:43:20 mgedmin Exp $
+"""


Property changes on: Zope3/trunk/src/z3checkins/ftests/__init__.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/ftests/msg1.txt
===================================================================
--- Zope3/trunk/src/z3checkins/ftests/msg1.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/ftests/msg1.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,34 @@
+From: Jim =?ISO-8859-13?Q?=CD?= <jim at example.org>
+To: zope3-checkins at example.com
+Date: Wed, 30 Jul 2003 23:40:11 +0100
+Subject: [Zope3-checkins] CVS: Zope3/src/app/frobulator - frobulator.py:1.5
+Message-Id: <msg1 at example.org>
+
+Update of /cvs-repository/Zope3/src/app/frobulator
+In directory cvs.zope.org:/tmp/cvs-serv12345
+
+Modified files:
+        frobulator.py
+Log message:
+Update the frobulator time conductor API implementation to match the new
+specification.
+
+
+=== Zope3/src/app/frobulator/frobulator.py 1.4 => 1.5 ===
+--- Zope3/src/app/frobulator/frobulator.py:1.4    Tue Mar 25 15:21:29 2003
++++ Zope3/src/app/frobulator/frobulator.py        Fri Mar 28 11:57:34 2003
+@@@ -123,7 +123,7 @@
+         z = self._transponder_matrix[0][3] ** 0.5 + epsilon
+         return self.postprocessCochraneCoefficients(x, y, z)
+ 
+-    def performTimeTravel(self, duration):
++    def performTimeTravel(self, duration, avoidParadoxes=True):
+         """
+         The basic time travel function.
+ 
+
+
+_______________________________________________
+Zope3-Checkins mailing list
+Zope3-Checkins at zope.org
+http://mail.zope.org/mailman/listinfo/zope3-checkins


Property changes on: Zope3/trunk/src/z3checkins/ftests/msg1.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/ftests/msg2.txt
===================================================================
--- Zope3/trunk/src/z3checkins/ftests/msg2.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/ftests/msg2.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,13 @@
+From: Fred <fred at example.org>
+To: zope3-checkins at example.com
+Date: Thu, 31 Jul 2003 17:00:23 +0300
+Subject: CVS: your checkin on Wednesday
+Message-Id: <msg2 at example.org>
+In-Reply-To: <msg1 at example.org>
+
+I did not particuarily understand the change to frobulator.py regarding the
+superconducting time traveller specification.  It looks like you omitted at
+least a part of the implementation.  Could you double check?
+
+-- 
+Fred


Property changes on: Zope3/trunk/src/z3checkins/ftests/msg2.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/ftests/test_z3checkins.py
===================================================================
--- Zope3/trunk/src/z3checkins/ftests/test_z3checkins.py	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/ftests/test_z3checkins.py	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,117 @@
+#!/usr/bin/python
+"""
+Functional tests for z3checkins.
+
+$Id: test_z3checkins.py,v 1.9 2004/05/15 13:23:59 gintautasm Exp $
+"""
+
+import unittest
+import os
+from zope.app.tests.functional import BrowserTestCase
+
+
+class TestCheckins(BrowserTestCase):
+
+    container_views = ('index.html', 'checkins-sidebar.html', 'checkins.rss')
+    message_views = ('index.html', 'index.txt')
+    resources = ('zope3.png', 'product.png', 'branch.png', 'message.png')
+
+    def open(self, filename):
+        """Open a file relative to the location of this module."""
+        base = os.path.dirname(__file__)
+        return open(os.path.join(base, filename))
+
+    def setUp(self):
+        BrowserTestCase.setUp(self)
+        response = self.publish('/+/action.html', basic='mgr:mgrpw',
+                form={'type_name': u'CheckinFolder', 'id': u'z3c'})
+        self.assertEqual(response.getStatus(), 302)
+
+        response = self.publish(
+            '/+/CheckinFolder=z3c',
+            basic='mgr:mgrpw',
+            form={'field.description': u'Some description',
+                  'field.archive_url': u'http://void',
+                  'field.icons': u'icon\nanother one',
+                  'UPDATE_SUBMIT': 'Add'})
+        self.assertEqual(response.getStatus(), 302)
+        z3c = self.getRootFolder()['z3c']
+        self.assertEqual(z3c.description, u'Some description')
+        self.assertEqual(z3c.archive_url, u'http://void')
+        self.assertEqual(z3c.icons, u'icon\nanother one')
+
+    def test_empty(self):
+        for view in self.container_views:
+            response = self.publish('/z3c/@@%s' % view)
+            self.assertEqual(response.getStatus(), 200)
+
+    def test_resources(self):
+        for resource in self.resources:
+            response = self.publish('/z3c/++resource++%s' % resource)
+            self.assertEqual(response.getStatus(), 200)
+
+    def test_add_checkin_message(self):
+        response = self.publish('/z3c/@@+',
+                                basic='mgr:mgrpw',
+                                form={'field.data': self.open('msg1.txt'),
+                                      'UPDATE_SUBMIT': u'Submit'})
+        self.assertEqual(response.getStatus(), 200)
+        self.assertEqual(response.getBody().count("Checkin message"), 1)
+
+        response = self.publish('/z3c/+/CheckinMessage',
+                                basic='mgr:mgrpw',
+                                form={'field.data': self.open('msg1.txt'),
+                                      'UPDATE_SUBMIT': u'Submit'})
+        self.assertEqual(response.getStatus(), 302)
+
+        for view in self.container_views:
+            response = self.publish('/z3c/@@%s' % view)
+            self.assertEqual(response.getStatus(), 200)
+        for view in self.message_views:
+            response = self.publish('/z3c/msg1 at example.org/@@%s' % view)
+            self.assertEqual(response.getStatus(), 200)
+
+        response = self.publish('/z3c/@@checkins.rss')
+        self.assertEqual(response.getStatus(), 200)
+        body = response.getBody()
+        xml_directive = '<?xml '
+        self.assert_(body.startswith(xml_directive),
+                     'checkins.rss has no XML directive:\n%s...' % body[:70])
+        # Make sure the XML directive is not repeated
+        self.assert_(body[len(xml_directive):].find(xml_directive) == -1,
+                     '%s appears more than once in checkins.rss' % xml_directive)
+
+    def test_add_simple_message(self):
+        response = self.publish('/z3c/+/CheckinMessage',
+                                basic='mgr:mgrpw',
+                                form={'field.data': self.open('msg2.txt'),
+                                      'UPDATE_SUBMIT': u'Submit'})
+        self.assertEqual(response.getStatus(), 302)
+
+        for view in self.container_views:
+            response = self.publish('/z3c/@@%s' % view)
+            self.assertEqual(response.getStatus(), 200)
+        for view in self.message_views:
+            response = self.publish('/z3c/msg2 at example.org/@@%s' % view)
+            self.assertEqual(response.getStatus(), 200)
+
+        response = self.publish('/z3c/@@checkins.rss')
+        self.assertEqual(response.getStatus(), 200)
+        body = response.getBody()
+        xml_directive = '<?xml '
+        self.assert_(body.startswith(xml_directive),
+                     'checkins.rss has no XML directive:\n%s...' % body[:70])
+        # Make sure the XML directive is not repeated
+        self.assert_(body[len(xml_directive):].find(xml_directive) == -1,
+                     '%s appears more than once in checkins.rss' % xml_directive)
+
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TestCheckins))
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main()


Property changes on: Zope3/trunk/src/z3checkins/ftests/test_z3checkins.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/interfaces.py
===================================================================
--- Zope3/trunk/src/z3checkins/interfaces.py	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/interfaces.py	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,110 @@
+"""
+Interfaces for the z3checkins product.
+
+$Id: interfaces.py,v 1.14 2004/05/15 13:23:57 gintautasm Exp $
+"""
+
+from zope.interface import Interface, Attribute
+from zope.app.folder.interfaces import IFolder
+from zope.app.container.interfaces import IContainer, IContained
+from zope.app.container.constraints import ContainerTypesConstraint
+from zope.app.container.constraints import ItemTypePrecondition
+from zope.schema import Field, Text, TextLine
+
+
+class IMessageUpload(Interface):
+    pass
+
+
+class IMessage(IMessageUpload):
+    """Mail message."""
+
+    message_id = Attribute("Unique message ID")
+    author_name = Attribute("Author's real name")
+    author_email = Attribute("Author's email address")
+    subject = Attribute("Subject line of the message")
+    date = Attribute("Date and time of the message")
+    body = Attribute("Body of the message")
+    full_text = Attribute("Full message text (headers and body)")
+
+
+class ICheckinMessage(IMessage):
+    """Checkin message."""
+
+    directory = Attribute("Directory that was updated")
+    branch = Attribute("Branch tag if this was commited to a branch")
+    log_message = Attribute("Checkin log message")
+    # Maybe added_files, modified_files, removed_files listing files and their
+    # revisions
+
+
+class IBookmark(Interface):
+    """Bookmark placed between messages."""
+
+
+class FormatError(Exception):
+    """Ill-formed message exception"""
+
+
+class IMessageParser(Interface):
+    """Parser for RFC-822 checkin messages"""
+
+    def parse(input):
+        """Parses an RFC-822 format message from a 'input' (which can be a
+        string or a file-like object) and returns an IMessage.
+
+        If the message is a checkin message, returns an ICheckinMessage.
+
+        May raise a FormatError if the message is ill-formed.
+        """
+
+
+class ICheckinFolderSchema(Interface):
+    """Checkin folder properties"""
+
+    description = TextLine(title=u"RSS view description",
+                           required=False)
+    archive_url = TextLine(title=u"URL of mailing list archive",
+                           required=False)
+    icons = Text(title=u"Icon definitions", required=False)
+
+
+class ICheckinFolder(IFolder, ICheckinFolderSchema):
+    """A marker interface for the checkins folder."""
+
+    def __setitem__(name, object):
+        """Add a message"""
+
+    __setitem__.precondition = ItemTypePrecondition(IMessageUpload)
+
+
+class IMessageContained(IContained):
+    """A contained message."""
+
+    __parent__ = Field(constraint=ContainerTypesConstraint(ICheckinFolder))
+
+
+class IMessageArchive(Interface):
+    """A chronologically ordered sequence of messages.
+
+    Implements the Python sequence procotol.
+    """
+
+    def __len__():
+        """Returns the number of messages in the archive."""
+
+    def __getitem__(index):
+        """Returns a given message."""
+
+    def __getslice__(start, stop):
+        """Returns a range of messages."""
+
+    def __iter__():
+        """Returns an iterator."""
+
+    def index(message):
+        """Returns the index of a given message.
+
+        Raises ValueError if message is not in the archive.
+        """
+


Property changes on: Zope3/trunk/src/z3checkins/interfaces.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/message.png
===================================================================
(Binary files differ)


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

Added: Zope3/trunk/src/z3checkins/message.pt
===================================================================
--- Zope3/trunk/src/z3checkins/message.pt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/message.pt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,52 @@
+<html>
+<head tal:define="next view/next; prev view/previous; first view/first; last view/last">
+  <title tal:content="string: ${context/author_name} - ${context/subject}" />
+  <link rel="next" tal:condition="next"
+        tal:attributes="href next/@@absolute_url" />
+  <link rel="previous" tal:condition="prev"
+        tal:attributes="href prev/@@absolute_url" />
+  <link rel="first" tal:condition="first"
+        tal:attributes="href first/@@absolute_url" />
+  <link rel="last" tal:condition="last"
+        tal:attributes="href last/@@absolute_url" />
+  <style type="text/css">
+    body { color: black; background: white; }
+    .headers { margin-bottom: 2em; }
+    .headers p { margin: 0ex; text-indent: -6em; padding-left: 6em;
+                font-family: monospace; }
+    .header { font-weight: bold; color: blue; }
+    div.body > pre { margin: 0ex; }
+    div.log { border: 1px solid gray; padding: 0.5ex;
+              margin-top: 0.5ex; margin-bottom: 2em; }
+    div.log p { margin: 0ex; text-indent: -4em; padding-left: 4em;
+                font-family: monospace; }
+    .file { background: #ddd; }
+    .oldfile { background: #ddd; color: #65c; }
+    .newfile { background: #ddd; color: #182; }
+    .chunk { background: #eee; color: #a22; }
+    .old { background: #e3e0ff; color: red; }
+    .new { background: #e0ffe6; color: green; }
+    .signature { color: gray; }
+    .trail { color: gray; }
+    .tab { color: gray; }
+    img.icon { float: right; padding: 0ex; margin: 2px; border: none; }
+  </style>
+</head>
+<body>
+<img class="icon"
+     tal:define="icon view/icon"
+     tal:attributes="src icon/src; alt icon/alt; title icon/title" />
+<img tal:condition="context/branch | nothing"
+     class="icon" src="++resource++branch.png" alt="Branch"
+     tal:attributes="title string:Branch: ${context/branch}"/>
+<div class="headers">
+<p><span class="header">From:</span>
+    <span class="value" tal:content="context/author_name" />
+    &lt;<span class="value" tal:content="context/author_email" />&gt;
+</p>
+<p><span class="header">Date:</span> <span class="value" tal:content="context/date/@@rfc822" /></p>
+<p><span class="header">Subject:</span> <span class="value" tal:content="context/subject" /></p>
+</div>
+<div class="body" tal:content="structure view/body" />
+</body>
+</html>


Property changes on: Zope3/trunk/src/z3checkins/message.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/message.py
===================================================================
--- Zope3/trunk/src/z3checkins/message.py	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/message.py	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,747 @@
+"""
+Python code for z3checkins product.
+
+# This module could be split into three: timeutils.py, message.py and views.py
+# but it is small enough IMHO.
+
+$Id: message.py,v 1.40 2004/05/14 19:56:05 gintautasm Exp $
+"""
+
+import re
+import email
+import email.Utils
+import mailbox
+import time
+from StringIO import StringIO
+from datetime import datetime, tzinfo, timedelta
+
+from persistence import Persistent
+from zope.app.form import CustomWidgetFactory
+from zope.app.form.browser import FileWidget
+from zope.app.container.interfaces import IReadContainer
+from zope.app.datetimeutils import parseDatetimetz, DateTimeError
+from zope.app.dublincore.interfaces import IZopeDublinCore
+from zope.app.pagetemplate import ViewPageTemplateFile
+from zope.component import getUtility, getAdapter, queryAdapter
+from zope.component import getView
+from zope.exceptions import DuplicationError
+from zope.interface import implements
+from zope.proxy import removeAllProxies
+from zope.app.publisher.browser import BrowserView
+
+from interfaces import IMessage, ICheckinMessage, IMessageContained
+from interfaces import IMessageUpload, IBookmark
+from interfaces import IMessageParser, IMessageArchive
+from interfaces import FormatError
+
+__metaclass__ = type
+
+#
+# Date/time utils
+#
+
+class FixedTimezone(tzinfo):
+    """Timezone with a fixed UTC offset"""
+
+    def __init__(self, offset=None):
+        """Creates a timezone with a given UTC offset (minutes east of UTC)."""
+        self._offset = offset
+
+    def tzname(self, dt):
+        if self._offset >= 0:
+            sign = '+'
+            h, m = divmod(self._offset, 60)
+        else:
+            sign = '-'
+            h, m = divmod(-self._offset, 60)
+        return '%c%02d%02d' % (sign, h, m)
+
+    def utcoffset(self, dt):
+        return timedelta(minutes=self._offset)
+
+    def dst(self, dt):
+        return timedelta(0)
+
+
+class RFCDateTimeFormatter:
+    """RFC822 view for datetime objects."""
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __str__(self):
+        """Renders datetime objects in RFC822 format."""
+        return self.context.strftime("%a, %d %b %Y %H:%M:%S %z")
+
+    __call__ = __str__
+
+
+class ISODateTimeFormatter:
+    """ISO 8601 view for datetime objects."""
+
+    if time.localtime()[-1]:
+        userstz = FixedTimezone(-time.altzone / 60)
+    else:
+        userstz = FixedTimezone(-time.timezone / 60)
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __str__(self):
+        """Renders datetime objects as "YYYY-MM-DD hh:mm" in the local time
+        zone."""
+        return self.context.astimezone(self.userstz).strftime("%Y-%m-%d %H:%M")
+
+    __call__ = __str__
+
+
+#
+# Checkin message content object
+#
+
+def find_body_start(full_text):
+    """Find the body of an RFC-822 message and return its index in full_text."""
+    pos1 = full_text.find('\n\n')
+    pos2 = full_text.find('\r\n\r\n')
+    if pos1 == -1:
+        pos1 = len(full_text)
+    else:
+        pos1 += 2
+    if pos2 == -1:
+        pos2 = len(full_text)
+    else:
+        pos2 += 4
+    return min(pos1, pos2)
+
+
+class Message(Persistent):
+    """Persistent email message."""
+
+    implements(IMessage, IMessageContained)
+
+    __parent__ = __name__ = None
+
+    def __init__(self, message_id=None, author_name=None,
+                 author_email=None, subject=None, date=None,
+                 full_text=None):
+        self.message_id = message_id
+        self.author_name = author_name
+        self.author_email = author_email
+        self.subject = subject
+        self.date = date
+        self.full_text = full_text
+
+    def _getBody(self):
+        if self.full_text is None:
+            return None
+        else:
+            return self.full_text[find_body_start(self.full_text):]
+
+    body = property(_getBody)
+
+    def __eq__(self, other):
+        """Messages with the same message_id compare identical."""
+        if not IMessage.providedBy(other):
+            return False
+        return self.message_id == other.message_id
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+class CheckinMessage(Message):
+    """Persistent checkin message."""
+
+    implements(ICheckinMessage)
+
+    def __init__(self, message_id=None, author_name=None,
+                 author_email=None, subject=None, date=None, full_text=None,
+                 directory=None, log_message=None, branch=None):
+        super(CheckinMessage, self).__init__(message_id=message_id,
+            author_name=author_name, author_email=author_email,
+            subject=subject, date=date, full_text=full_text)
+        self.directory = directory
+        self.log_message = log_message
+        self.branch = branch
+
+
+class CheckinMessageParser:
+    """Parser for RFC822 mail messages."""
+
+    implements(IMessageParser)
+
+    def parse(self, input):
+        """See IMessageParser."""
+
+        if not hasattr(input, 'readline'):
+            full_text = str(input)
+        elif hasattr(input, 'seek') and hasattr(input, 'tell'):
+            old_pos = input.tell()
+            full_text = input.read()
+            input.seek(old_pos)
+        else:
+            full_text = input.read()
+
+        m = email.message_from_string(full_text)
+        subject = m.get('Subject', '').replace('\n', '')
+        message_id = m.get('Message-Id', None)
+        if message_id is None:
+            raise FormatError("Message does not have a message id")
+        if message_id[0] == "<" and message_id[-1] == ">":
+            message_id = message_id[1:-1] # strip angle brackets
+        author = m.get('From', '')
+        author_name, author_email = email.Utils.parseaddr(author)
+        date = m.get('Date', '')
+
+        # Fix incorrect timezones (+XX:XX instead of RFC-822 mandated +XXXX)
+        if date[-3] == ':':
+            date = date[:-3] + date[-2:]
+
+        (year, month, day, hours, minutes, seconds,
+         weekday, yearday, dst, tzoffset) = email.Utils.parsedate_tz(date)
+
+        # XXX a workaround to deal with messages that don't specify a timezone
+        if tzoffset is None:
+            tzoffset = 0
+
+        date = datetime(year, month, day, hours, minutes, seconds,
+                        tzinfo=FixedTimezone(tzoffset / 60))
+
+        checkin_info = self.tryToParseCheckinMessage(subject, m)
+        if checkin_info is not None:
+            directory, log_message, branch = checkin_info
+            return CheckinMessage(message_id=message_id,
+                                  author_name=author_name,
+                                  author_email=author_email, subject=subject,
+                                  date=date, full_text=full_text,
+                                  directory=directory, log_message=log_message,
+                                  branch=branch)
+
+        return Message(message_id=message_id,
+                       author_name=author_name,
+                       author_email=author_email, subject=subject,
+                       date=date, full_text=full_text)
+
+    def tryToParseCheckinMessage(self, subject, msg):
+        """Detect and parse CVS/Subversion checkin messages.
+
+        Returns a tuple (directory, log_message, branch) for checkin
+        messages, and None if the message is not a checkin message.
+        """
+
+        if subject.startswith("Re:"):
+            return None
+
+        if "CVS:" in subject:
+            parts = subject.split("CVS: ", 1)
+            if len(parts) < 2:
+                return None
+            subject = parts[1]
+            directory = subject.split(' - ')[0]
+        elif "rev " in subject:
+            parts = subject.split(' - ')
+            if len(parts) < 2:
+                return None
+            directory = parts[1]
+        else:
+            return None
+
+        body_lines = msg.get_payload().splitlines()
+        try:
+            log_message, branch = self.extract_log(body_lines)
+        except FormatError:
+            return None
+
+        return directory, log_message, branch
+
+    def extract_log(self, lines):
+        log_message = []
+        branch = None
+        in_log_msg = False
+        for line in lines:
+            if in_log_msg:
+                if (line.startswith('=== ')
+                    or line.startswith("Added:")
+                    or line.startswith("Modified:")
+                    or line.startswith("Removed:")
+                    or line.startswith("Deleted:")
+                    or line.startswith("Property changes on:")
+                    or line == "Status:"
+                    ):
+                    break
+                else:
+                    log_message.append(line)
+            else:
+                if (line.lower().startswith('log message:') or
+                    line.startswith("Log:")):
+                    in_log_msg = True
+                elif line.startswith('      Tag: '):
+                    branch = line[len('      Tag: '):].strip()
+        if not in_log_msg:
+            raise FormatError("Could not find log message")
+        return "\n".join(log_message).strip(), branch
+
+
+class MessageContainerAdapter:
+    """Adapts a container to a message archive."""
+
+    implements(IMessageArchive)
+    __used_for__ = IReadContainer
+
+    def __init__(self, context):
+        self.context = context
+        items = []
+        for key, item in self.context.items():
+            if IMessage.providedBy(item):
+                items.append((item.date, key, item))
+        items.sort()
+        self.messages = []
+        for date, key, item in items:
+            # XXX is this nice?
+            #item.__parent__ = self.context
+            #item.__name__ = key
+            self.messages.append(item)
+
+    def __len__(self):
+        return len(self.messages)
+
+    def __getitem__(self, index):
+        return self.messages[index]
+
+    def __getslice__(self, start, stop):
+        return self.messages[start:stop]
+
+    def __iter__(self):
+        return iter(self.messages)
+
+    def index(self, message):
+        return self.messages.index(message)
+
+
+class Bookmark:
+
+    implements(IBookmark)
+
+
+#
+# Browser views
+#
+
+class MessageUpload:
+    """Adding view mixin for uploading checkin messages."""
+
+    implements(IMessageUpload, IMessageContained)
+    data_widget = CustomWidgetFactory(FileWidget)
+
+    def createAndAdd(self, data):
+        if data.has_key('data'): # XXX should we bark if no data is given?
+            msg_raw = data['data']
+            parser = getUtility(self.context, IMessageParser)
+            if msg_raw.startswith("From "):
+                # detected an mbox file
+                mbox = StringIO(msg_raw)
+                messages = mailbox.PortableUnixMailbox(mbox,
+                        factory=parser.parse)
+                for message in messages:
+                    try:
+                        self.add(message)
+                        dc = queryAdapter(message, IZopeDublinCore)
+                        if dc is not None:
+                            # XXX should handle RFC-2047
+                            dc.title = unicode(message.subject)
+                            dc.created = message.date
+                    except DuplicationError:
+                        pass # leave the old mesage unchanged
+            else:
+                message = parser.parse(msg_raw)
+                self.add(message)
+
+
+class ContainerView:
+    """View mixin for locating checkin messages in a container."""
+
+    max_bookmarks = 5
+
+    def title(self):
+        """Returns the title of this archive.
+
+        Title is obtained from Dublin Core metadata of the folder.  If it is
+        empty, "Zope 3 Checkins" is used.
+        """
+        dc = queryAdapter(self.context, IZopeDublinCore)
+        if dc is not None:
+            title = dc.title
+        else:
+            title = ''
+        return title or "Zope 3 Checkins"
+
+    def description(self):
+        """Returns the description of this archive.
+        """
+        return self.context.description
+
+    def archive_url(self):
+        """Returns the URL for mailing list archives.
+        """
+        return self.context.archive_url
+
+    def bookmarks(self):
+        """Returns a list of bookmarks from a cookie.  Each bookmark is
+        expressed as a datetime object.
+        """
+        bookmarks = []
+        cookie = self.request.get('bookmarks', '')
+        for item in cookie.split():
+            try:
+                bookmarks.append(parseDatetimetz(item))
+            except (DateTimeError, IndexError):
+                pass
+        return bookmarks
+
+    def placeBookmark(self):
+        """Place a new bookmark after the latest checkin message in a
+        cookie."""
+        if int(self.request.get('start', 0)) > 0:
+            return # The user can't see the newest checkins
+        if not hasattr(self, '_archive'):
+            self._archive = getAdapter(self.context, IMessageArchive)
+        if not self._archive:
+            return # No messages -- no bookmarks
+        bookmarks = self.bookmarks()
+        bookmarks.sort()
+        # Do not insert a bookmark if there were no checkins since the last
+        # bookmark
+        if (bookmarks and bookmarks[-1] >= self._archive[-1].date):
+            return
+        bookmarks.append(self._archive[-1].date)
+        if len(bookmarks) > self.max_bookmarks:
+            del bookmarks[:-self.max_bookmarks]
+        cookie = " ".join([dt.isoformat() for dt in bookmarks])
+        self.request.response.setCookie('bookmarks', cookie,
+                                        max_age=365*24*60*60) # 1 year
+
+    def checkins(self, start=None, size=None):
+        """Returns a list of the last 'size' checkin messages in
+        self.context, newest first, skipping the first 'start' messages.
+        """
+        if start is None: start = int(self.request.get('start', 0))
+        if size is None: size = int(self.request.get('size', 20))
+        if not hasattr(self, '_archive'):
+            self._archive = getAdapter(self.context, IMessageArchive)
+        idx = len(self._archive) - start
+        items = self._archive[max(0, idx-size):idx]
+        items = removeAllProxies(items)
+        # insert bookmarks
+        def bookmarkBetween(msg1, msg2, bookmarks=self.bookmarks()):
+            for b in bookmarks:
+                if msg1.date <= b < msg2.date:
+                    return True
+            return False
+        n = 1
+        while n < len(items):
+            if bookmarkBetween(items[n-1], items[n]):
+                items.insert(n, Bookmark())
+                n += 2
+            else:
+                n += 1
+        # insert bookmarks before the first/after the last batch item
+        if items:
+            before = self._archive[max(0, idx-size-1):max(0, idx-size)]
+            if before and bookmarkBetween(before[0], items[0]):
+                items.insert(0, Bookmark())
+            after = self._archive[idx:idx+1]
+            if after and bookmarkBetween(items[-1], after[0]):
+                items.insert(len(items), Bookmark())
+        # reverse order to present newest checkins first
+        items.reverse()
+        return items
+
+    def renderCheckins(self, start=None, size=None):
+        """Returns a list of checkins rendered into HTML.  See `checkins` for
+        description of parameters."""
+        html = []
+        previous_message = None
+        for item in self.checkins(start=start, size=size):
+            if ICheckinMessage.providedBy(item):
+                same_as_previous = item.log_message == previous_message
+                previous_message = item.log_message
+            else:
+                same_as_previous = None
+            view = getView(item, 'html', self.request)
+            output = view(same_as_previous=same_as_previous)
+            html.append(output)
+        return "".join(html)
+
+    def count(self):
+        """Returns the number of checkin messages in the archive."""
+        if not hasattr(self, '_archive'):
+            self._archive = getAdapter(self.context, IMessageArchive)
+        return len(self._archive)
+
+
+class MessageRSSView(BrowserView):
+    """View for messages.
+
+    Makes sure the page template is treated as XML.
+    """
+
+    index = ViewPageTemplateFile('rss_message.pt', content_type='text/xml')
+
+
+class MessageView:
+    """View mixin for messages."""
+
+    def _calc_index(self):
+        if not hasattr(self, '_archive'):
+            container = self.context.__parent__
+            self._archive = container and queryAdapter(container,
+                                                       IMessageArchive)
+        if not self._archive:
+            self._index = None
+        elif not hasattr(self, '_index'):
+            self._index = self._archive.index(self.context)
+
+    def next(self):
+        """Returns the next message in archive."""
+        self._calc_index()
+        if self._index is not None and self._index < len(self._archive) - 1:
+            return self._archive[self._index + 1]
+        else:
+            return None
+
+    def previous(self):
+        """Returns the previous message in archive."""
+        self._calc_index()
+        if self._index is not None and self._index > 0:
+            return self._archive[self._index - 1]
+        else:
+            return None
+
+    def first(self):
+        """Returns the first message in archive."""
+        self._calc_index()
+        if self._archive:
+            return self._archive[0]
+        else:
+            return None
+
+    def last(self):
+        """Returns the last message in archive."""
+        self._calc_index()
+        if self._archive:
+            return self._archive[-1]
+        else:
+            return None
+
+    def icon(self):
+        """Returns a mapping describing an icon for this checkin.  The mapping
+        contains 'src', 'alt' and 'title' attributes."""
+        return {'src': '++resource++message.png',
+                'alt': 'Message',
+                'title': 'Email message'}
+
+    def body(self):
+        """Colorizes message body."""
+
+        text = self.context.body.replace('\r', '')\
+                                .replace('&', '&amp;') \
+                                .replace('<', '&lt;') \
+                                .replace('>', '&gt;') \
+                                .replace('"', '&quot;')
+        # It would be nice to highlight quoted text here
+        return '<pre>%s</pre>' % text
+
+
+class CheckinMessageView(MessageView):
+    """View mixin for checkin messages."""
+
+    _subtrees = None
+    def subtrees(self):
+        """Returns a sequence of tuples (prefix, icon, alt, title).
+
+        (icon, alt, title) are the resource name, alt text and tooltip used
+        for any checkin messages that have directory starting with prefix.
+
+        This information is currently taken from Dublin Core metadata
+        description field, third paragraph.  Every line in that paragraph
+        defines a subtree, with all fields separated by spaces or tabs.
+        """
+        if self._subtrees is not None:
+            return self._subtrees
+        self._subtrees = []
+        container = self.context.__parent__
+        description = container.description
+        if not description:
+            return self._subtrees
+        for line in description.splitlines():
+            items = line.split(None, 3)
+            if len(items) < 4:
+                continue
+            if items[0] == '*':  # catch-all
+                items[0] = ''
+            self._subtrees.append(items)
+        return self._subtrees
+
+    def icon(self):
+        """Returns a mapping describing an icon for this checkin.  The mapping
+        contains 'src', 'alt' and 'title' attributes."""
+        for prefix, icon, alt, title in self.subtrees():
+            if self.context.directory.startswith(prefix):
+                return {'src': '++resource++%s' % icon,
+                        'alt': alt,
+                        'title': title}
+        return {'src': '++resource++product.png',
+                'alt': 'Checkin',
+                'title': 'Checkin'}
+
+    def body(self):
+        """Colorizes checkin message body."""
+
+        text = self.context.body.replace('\r', '')\
+                                .replace('&', '&amp;') \
+                                .replace('<', '&lt;') \
+                                .replace('>', '&gt;') \
+                                .replace('"', '&quot;')
+
+        text = re.sub(r'(https?://.+?)'
+                      r'($|[ \t\r\n)]|&gt;|&quot;|[.,](?:$|[ \t\r\n]))',
+                      r'<a href="\1">\1</a>\2', text)
+
+        log_idx = text.find('\nLog message:\n')
+        if log_idx == -1:
+            log_idx = text.find('\nLog Message:\n')
+        if log_idx != -1:
+            log_idx += len('\nLog message:\n')
+        if log_idx == -1:
+            log_idx = text.find('\nLog:\n')
+            if log_idx != -1:
+                log_idx += len('\nLog:\n')
+        if log_idx == -1:
+            return '<pre>%s</pre>' % text
+
+        sig_idx = text.rfind(
+                        '\n_______________________________________________')
+        if sig_idx == -1:
+            sig_idx = len(text)
+
+        diff_idx = text.find('\n===')
+        if diff_idx == -1:
+            diff_idx = sig_idx
+
+        status_idx = text.find('\nStatus:\n')
+        if status_idx == -1:
+            if text[diff_idx:diff_idx+5] == "\n====":
+                # Subversion
+                status_idx = text.rfind('\n', 0, diff_idx)
+            else:
+                status_idx = diff_idx
+
+            propchange_idx = text.find('\nProperty changes on:')
+            if propchange_idx != -1 and propchange_idx < status_idx:
+                status_idx = propchange_idx
+
+        assert log_idx <= status_idx <= diff_idx <= sig_idx
+
+        intro = text[:log_idx]
+        log = text[log_idx:status_idx].strip()
+        import_status = text[status_idx:diff_idx]
+        diff = text[diff_idx:sig_idx]
+        sig = text[sig_idx:]
+
+        def empty2nbsp(s):
+            if not s:
+                return '&nbsp;'
+            n = 0
+            while n < len(s) and s[n] == ' ':
+                n += 1
+            if n:
+                return '&nbsp;' * n + s[n:]
+            else:
+                return s
+        log = '<p>%s</p>' % '</p>\n<p>'.join(map(empty2nbsp, log.splitlines()))
+
+        if import_status is None:
+            import_status = ''
+
+        if diff is None:
+            diff = ''
+
+        diff = "\n".join(map(self.mark_whitespace, diff.splitlines()))
+
+        def colorize(style):
+            return r'<div class="%s">\1</div>' % style
+
+        # Unified diff
+        diff = re.sub(r'(?m)^(===.*)$', colorize("file"), diff)
+        diff = re.sub(r'(?m)^(---.*)$', colorize("oldfile"), diff)
+        diff = re.sub(r'(?m)^(\+\+\+.*)$', colorize("newfile"), diff)
+        diff = re.sub(r'(?m)^(@@.*)$', colorize("chunk"), diff)
+        diff = re.sub(r'(?m)^(-.*)$', colorize("old"), diff)
+        diff = re.sub(r'(?m)^(\+.*)$', colorize("new"), diff)
+
+        # Postprocess for Mozilla
+        diff = re.sub('</div>\n', '\n</div>', diff)
+
+        if sig:
+            sig = '<div class="signature">%s</div>' % sig
+
+        text = '<pre>%s</pre><div class="log">%s</div><pre>%s%s%s</pre>' \
+               % (intro, log, import_status, diff, sig)
+        # XXX: find out the actual encoding instead of assuming UTF-8
+        return unicode(text, 'UTF-8', 'replace')
+
+    def mark_whitespace(self, line, tab=('>', '-'), trail='.'):
+        """Mark whitespace in diff lines.
+
+        Suggested values for tab: ('>', '-'), ('&#187;', ' '),
+                                  ('&#187;', '&#x2010;')
+
+        Suggested values for trail: '.', '&#x2423;'
+        """
+        if line == ' ' or (not line.endswith(' ') and '\t' not in line):
+            return line
+        m = re.search('\s+\Z', line)
+        if m:
+            n = m.start()
+            if n == 0:
+                n = 1 # don't highlight the first space in a diff
+            line = '%s<span class="trail">%s</span>' % (line[:n],
+                            line[n:].replace(' ', trail))
+        if '\t' in line:
+            NORMAL, TAG, ENTITY = 0, 1, 2
+            idx = col = 0
+            mode = NORMAL
+            tabs = []
+            for c in line[1:]: # ignore first space in a diff
+                idx += 1
+                if mode == TAG:
+                    if c == '>':
+                        mode = NORMAL
+                elif mode == ENTITY:
+                    if c == ';':
+                        col += 1
+                        mode = NORMAL
+                else:
+                    if c == '<':
+                        mode = TAG
+                    elif c == '&':
+                        mode = ENTITY
+                    elif c == '\t':
+                        width = 8 - (col % 8)
+                        tabs.append((idx, width))
+                        col += width
+                    else:
+                        col += 1
+            if tabs:
+                parts = []
+                last = 0
+                for idx, width in tabs:
+                    parts.append(line[last:idx])
+                    parts.append('<span class="tab">%s%s</span>'
+                                 % (tab[0], tab[1] * (width - 1)))
+                    last = idx + 1
+                parts.append(line[last:])
+                line = "".join(parts)
+        return line


Property changes on: Zope3/trunk/src/z3checkins/message.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/message_part.pt
===================================================================
--- Zope3/trunk/src/z3checkins/message_part.pt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/message_part.pt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,20 @@
+<div class="message">
+<a class="title" tal:attributes="href context/@@absolute_url">
+  <img class="icon"
+       tal:define="icon view/icon"
+       tal:attributes="src icon/src; alt icon/alt; title icon/title" />
+  <img tal:condition="context/branch | nothing"
+       class="icon" src="++resource++branch.png" alt="Branch"
+       tal:attributes="title string:Branch: ${context/branch}"/>
+  <span class="date" tal:content="context/date/@@isodatetime" />:
+  <span class="author" tal:content="context/author_name" />
+  - <span class="subject" tal:content="context/subject" />
+</a>
+<tal:if condition="context/log_message | nothing">
+<div class="same description" tal:condition="options/same_as_previous">
+(Same as above)
+</div>
+<div class="description" tal:condition="not:options/same_as_previous"
+     tal:content="context/log_message" />
+</tal:if>
+</div>


Property changes on: Zope3/trunk/src/z3checkins/message_part.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/message_plain.pt
===================================================================
--- Zope3/trunk/src/z3checkins/message_plain.pt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/message_plain.pt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1 @@
+<tal:block tal:replace="structure context/full_text" />


Property changes on: Zope3/trunk/src/z3checkins/message_plain.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/message_sidebar.pt
===================================================================
--- Zope3/trunk/src/z3checkins/message_sidebar.pt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/message_sidebar.pt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,20 @@
+<div class="message">
+<a class="title" tal:attributes="href context/@@absolute_url" target="_content">
+  <img class="icon"
+       tal:define="icon view/icon"
+       tal:attributes="src icon/src; alt icon/alt; title icon/title" />
+  <img tal:condition="context/branch | nothing"
+       class="icon" src="++resource++branch.png" alt="Branch"
+       tal:attributes="title string:Branch: ${context/branch}"/>
+  <span class="date" tal:content="context/date/@@isodatetime" />:
+  <span class="author" tal:content="context/author_name" />
+  - <span class="subject" tal:content="context/subject" />
+</a>
+<tal:if condition="context/log_message | nothing">
+<div class="same description" tal:condition="options/same_as_previous">
+(Same as above)
+</div>
+<div class="description" tal:condition="not:options/same_as_previous"
+     tal:content="context/log_message" />
+</tal:if>
+</div>


Property changes on: Zope3/trunk/src/z3checkins/message_sidebar.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/product.png
===================================================================
(Binary files differ)


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

Added: Zope3/trunk/src/z3checkins/rss_container.pt
===================================================================
--- Zope3/trunk/src/z3checkins/rss_container.pt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/rss_container.pt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<rss version="2.0" xmlns:tal="http://xml.zope.org/namespaces/tal">
+  <channel tal:define="webmaster context/webmaster_email | nothing">
+    <title tal:content="view/title">Zope 3 Checkins</title>
+    <link tal:content="string:${context/@@absolute_url}"></link>
+    <description tal:content="view/description" tal:condition="view/description">Latest Zope 3 Checkins</description>
+    <language>en-us</language>
+    <docs>http://backend.userland.com/rss</docs>
+    <generator>z3checkins</generator>
+    <webMaster tal:condition="webmaster" tal:content="webmaster" />
+<item tal:repeat="item view/checkins" tal:replace="structure item/@@rss|nothing" />
+  </channel>
+</rss>


Property changes on: Zope3/trunk/src/z3checkins/rss_container.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/rss_message.pt
===================================================================
--- Zope3/trunk/src/z3checkins/rss_message.pt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/rss_message.pt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,8 @@
+<item xmlns:tal="http://xml.zope.org/namespaces/tal">
+  <title tal:content="string:${context/author_name} - ${context/subject}" />
+  <link tal:content="context/@@absolute_url" />
+  <description tal:content="context/log_message | nothing" />
+  <guid tal:content="context/@@absolute_url" />
+  <author><tal:block tal:replace="context/author_name" /> &lt;<tal:block tal:replace="context/author_email" />&gt;</author>
+  <pubDate tal:content="context/date/@@rfc822" />
+</item>


Property changes on: Zope3/trunk/src/z3checkins/rss_message.pt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/__init__.py
===================================================================
--- Zope3/trunk/src/z3checkins/tests/__init__.py	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/__init__.py	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,5 @@
+"""
+Unit tests for z3checkins.
+
+$Id: __init__.py,v 1.2 2003/08/01 09:43:23 mgedmin Exp $
+"""


Property changes on: Zope3/trunk/src/z3checkins/tests/__init__.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/mbox.txt
===================================================================
--- Zope3/trunk/src/z3checkins/tests/mbox.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/mbox.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,193 @@
+From steve at cat-box.net  Sat Jun  1 00:31:53 2002
+From: steve at cat-box.net (Steve Alexander)
+Date: Fri, 31 May 2002 19:31:53 -0400
+Subject: [Zope-Checkins] CVS: Zope3/lib/python/Zope/App/Security - PermissionRegistry.py:1.1.2.16.14.1
+Message-ID: <200205312331.g4VNVr927566 at cvs.baymountain.com>
+
+Update of /cvs-repository/Zope3/lib/python/Zope/App/Security
+In directory cvs.zope.org:/tmp/cvs-serv27552
+
+Modified Files:
+      Tag: Zope3InWonderland-branch
+        PermissionRegistry.py 
+Log Message:
+Permission ids must not start with a dot.
+
+
+=== Zope3/lib/python/Zope/App/Security/PermissionRegistry.py 1.1.2.16 => 1.1.2.16.14.1 ===
+         """Define a new permission object, register, and return it.
+ 
+-        name is the permission name, must be globally unique
++        permission is the permission name, must be globally unique
+ 
+         title is the permission title, human readable.
+ 
+         description (optional) is human readable
+         """
++        if permission.startswith('.'):
++            raise ValueError("permissions must not start with a '.'")
+         return self.register(permission, title, description)
+ 
+     def definedPermission(self, permission_id):
+
+
+
+
+From steve at cat-box.net  Sat Jun  1 00:31:53 2002
+From: steve at cat-box.net (Steve Alexander)
+Date: Fri, 31 May 2002 19:31:53 -0400
+Subject: [Zope-Checkins] CVS: Zope3/lib/python/Zope/App/Security/tests - testPermissionRegistry.py:1.1.2.13.14.1
+Message-ID: <200205312331.g4VNVr027568 at cvs.baymountain.com>
+
+Update of /cvs-repository/Zope3/lib/python/Zope/App/Security/tests
+In directory cvs.zope.org:/tmp/cvs-serv27552/tests
+
+Modified Files:
+      Tag: Zope3InWonderland-branch
+        testPermissionRegistry.py 
+Log Message:
+Permission ids must not start with a dot.
+
+
+=== Zope3/lib/python/Zope/App/Security/tests/testPermissionRegistry.py 1.1.2.13 => 1.1.2.13.14.1 ===
+         self.assertEqual(None, permissionRegistry.getPermission('Foo'))
+         self.failIf(permissionRegistry.definedPermission('Foo'))
++        
++    def testPermissionStartsWithDot(self):
++        self.assertRaises(ValueError, permissionRegistry.definePermission,
++                          '.Foo', 'dot foo title')
+ 
+     def testPermissionIsAnIPermission(self):
+         permissionRegistry.definePermission('Foo', 'foo title')
+@@ -56,7 +60,7 @@
+         permission = permissionRegistry.getPermission('Foo')
+         eq(permission.getTitle(), 'Foo-able')
+         eq(permission.getDescription(), 'A foo-worthy permission')
+-    
++
+ 
+ def test_suite():
+     loader=unittest.TestLoader()
+
+
+
+
+From tim.one at comcast.net  Sat Jun  1 00:46:53 2002
+From: tim.one at comcast.net (Tim Peters)
+Date: Fri, 31 May 2002 19:46:53 -0400
+Subject: [Zope-Checkins] CVS: Zope/lib/python/BTrees/tests - testSetOps.py:1.3
+Message-ID: <200205312346.g4VNkr731412 at cvs.baymountain.com>
+
+Update of /cvs-repository/Zope/lib/python/BTrees/tests
+In directory cvs.zope.org:/tmp/cvs-serv30763/tests
+
+Modified Files:
+        testSetOps.py 
+Log Message:
+testBigInput():  This spent almost all of its time building an IISet
+from a sequence of ints in reverse-sorted order (a quadratic-time
+proposition).  That doesn't test anything interesting in context, though.
+So fiddled it to do a larger input, but it runs much faster now.
+
+
+=== Zope/lib/python/BTrees/tests/testSetOps.py 1.2 => 1.3 ===
+ 
+     def testBigInput(self):
+-        input = IISet(range(50000))
+-        reversed = range(50000)
+-        reversed.reverse()
+-        reversed = IISet(reversed)
+-        output = multiunion([input, reversed] * 5)
+-        self.assertEqual(len(output), 50000)
+-        self.assertEqual(list(output), range(50000))
++        N = 100000
++        input = IISet(range(N))
++        output = multiunion([input] * 10)
++        self.assertEqual(len(output), N)
++        self.assertEqual(output.minKey(), 0)
++        self.assertEqual(output.maxKey(), N-1)
++        self.assertEqual(list(output), range(N))
+ 
+     def testLotsOfLittleOnes(self):
+         from random import shuffle
+
+
+
+
+From tim.one at comcast.net  Sat Jun  1 01:49:19 2002
+From: tim.one at comcast.net (Tim Peters)
+Date: Fri, 31 May 2002 20:49:19 -0400
+Subject: [Zope-Checkins] CVS: Zope/lib/python/BTrees - SetOpTemplate.c:1.16
+Message-ID: <200206010049.g510nJR14723 at cvs.baymountain.com>
+
+Update of /cvs-repository/Zope/lib/python/BTrees
+In directory cvs.zope.org:/tmp/cvs-serv14639
+
+Modified Files:
+        SetOpTemplate.c 
+Log Message:
+multiunion():  For an input that's IIBucket-based (IIBucket and IISet),
+this now copies the keys into the work area in one gulp via memcpy,
+instead of iterating over them one at a time.  Yields a nice speedup when
+it applies (and it usually should apply!).
+
+
+=== Zope/lib/python/BTrees/SetOpTemplate.c 1.15 => 1.16 ===
+        set.  At this point, we ignore the possibility of duplicates. */
+     for (i = 0; i < n; ++i) {
+-        SetIteration setiter = {0, 0, 0};
+-        int merge;  /* dummy needed for initSetIteration */
+-
+         set = PySequence_GetItem(seq, i);
+         if (set == NULL)
+             goto Error;
+ 
+-        /* XXX TODO: If set is a bucket, do a straight resize+memcpy instead.
+-        */
+-        if (initSetIteration(&setiter, set, 1, &merge) < 0)
+-            goto Error;
+-        if (setiter.next(&setiter) < 0)
+-            goto Error;
+-        while (setiter.position >= 0) {
+-            if (result->len >= result->size && Bucket_grow(result, -1, 1) < 0)
++        /* If set is a bucket, do a straight resize + memcpy. */
++        if (set->ob_type == (PyTypeObject*)&SetType ||
++            set->ob_type == (PyTypeObject*)&BucketType) {
++            const int setsize = SIZED(set)->len;
++            int size_desired = result->len + setsize;
++            /* If there are more to come, overallocate by 25% (arbitrary). */
++            if (i < n-1)
++                size_desired += size_desired >> 2;
++            if (size_desired && size_desired > result->size) {
++                if (Bucket_grow(result, size_desired, 1) < 0)
++                    goto Error;
++            }
++            memcpy(result->keys + result->len,
++                   BUCKET(set)->keys,
++                   setsize * sizeof(KEY_TYPE));
++            result->len += setsize;
++        }
++        else {
++            /* No cheap way:  iterate over set's elements one at a time. */
++            SetIteration setiter = {0, 0, 0};
++            int merge;  /* dummy needed for initSetIteration */
++            
++            if (initSetIteration(&setiter, set, 1, &merge) < 0)
+                 goto Error;
+-            COPY_KEY(result->keys[result->len], setiter.key);
+-            ++result->len;
+-            /* We know the key is an int, so no need to incref it. */
+             if (setiter.next(&setiter) < 0)
+                 goto Error;
++            while (setiter.position >= 0) {
++                if (result->len >= result->size && Bucket_grow(result, -1, 1) < 0)
++                    goto Error;
++                COPY_KEY(result->keys[result->len], setiter.key);
++                ++result->len;
++                /* We know the key is an int, so no need to incref it. */
++                if (setiter.next(&setiter) < 0)
++                    goto Error;
++            }
+         }
+         Py_DECREF(set);
+         set = NULL;


Property changes on: Zope3/trunk/src/z3checkins/tests/mbox.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/mbox_with_dupes.txt
===================================================================
--- Zope3/trunk/src/z3checkins/tests/mbox_with_dupes.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/mbox_with_dupes.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,193 @@
+From steve at cat-box.net  Sat Jun  1 00:31:53 2002
+From: steve at cat-box.net (Steve Alexander)
+Date: Fri, 31 May 2002 19:31:53 -0400
+Subject: [Zope-Checkins] CVS: Zope3/lib/python/Zope/App/Security - PermissionRegistry.py:1.1.2.16.14.1
+Message-ID: <200205312331.g4VNVr927566 at cvs.baymountain.com>
+
+Update of /cvs-repository/Zope3/lib/python/Zope/App/Security
+In directory cvs.zope.org:/tmp/cvs-serv27552
+
+Modified Files:
+      Tag: Zope3InWonderland-branch
+        PermissionRegistry.py 
+Log Message:
+Permission ids must not start with a dot.
+
+
+=== Zope3/lib/python/Zope/App/Security/PermissionRegistry.py 1.1.2.16 => 1.1.2.16.14.1 ===
+         """Define a new permission object, register, and return it.
+ 
+-        name is the permission name, must be globally unique
++        permission is the permission name, must be globally unique
+ 
+         title is the permission title, human readable.
+ 
+         description (optional) is human readable
+         """
++        if permission.startswith('.'):
++            raise ValueError("permissions must not start with a '.'")
+         return self.register(permission, title, description)
+ 
+     def definedPermission(self, permission_id):
+
+
+
+
+From steve at cat-box.net  Sat Jun  1 00:31:53 2002
+From: steve at cat-box.net (Steve Alexander)
+Date: Fri, 31 May 2002 19:31:53 -0400
+Subject: [Zope-Checkins] CVS: Zope3/lib/python/Zope/App/Security/tests - testPermissionRegistry.py:1.1.2.13.14.1
+Message-ID: <200205312331.g4VNVr927566 at cvs.baymountain.com>
+
+Update of /cvs-repository/Zope3/lib/python/Zope/App/Security/tests
+In directory cvs.zope.org:/tmp/cvs-serv27552/tests
+
+Modified Files:
+      Tag: Zope3InWonderland-branch
+        testPermissionRegistry.py 
+Log Message:
+Permission ids must not start with a dot.
+
+
+=== Zope3/lib/python/Zope/App/Security/tests/testPermissionRegistry.py 1.1.2.13 => 1.1.2.13.14.1 ===
+         self.assertEqual(None, permissionRegistry.getPermission('Foo'))
+         self.failIf(permissionRegistry.definedPermission('Foo'))
++        
++    def testPermissionStartsWithDot(self):
++        self.assertRaises(ValueError, permissionRegistry.definePermission,
++                          '.Foo', 'dot foo title')
+ 
+     def testPermissionIsAnIPermission(self):
+         permissionRegistry.definePermission('Foo', 'foo title')
+@@ -56,7 +60,7 @@
+         permission = permissionRegistry.getPermission('Foo')
+         eq(permission.getTitle(), 'Foo-able')
+         eq(permission.getDescription(), 'A foo-worthy permission')
+-    
++
+ 
+ def test_suite():
+     loader=unittest.TestLoader()
+
+
+
+
+From tim.one at comcast.net  Sat Jun  1 00:46:53 2002
+From: tim.one at comcast.net (Tim Peters)
+Date: Fri, 31 May 2002 19:46:53 -0400
+Subject: [Zope-Checkins] CVS: Zope/lib/python/BTrees/tests - testSetOps.py:1.3
+Message-ID: <200205312331.g4VNVr927566 at cvs.baymountain.com>
+
+Update of /cvs-repository/Zope/lib/python/BTrees/tests
+In directory cvs.zope.org:/tmp/cvs-serv30763/tests
+
+Modified Files:
+        testSetOps.py 
+Log Message:
+testBigInput():  This spent almost all of its time building an IISet
+from a sequence of ints in reverse-sorted order (a quadratic-time
+proposition).  That doesn't test anything interesting in context, though.
+So fiddled it to do a larger input, but it runs much faster now.
+
+
+=== Zope/lib/python/BTrees/tests/testSetOps.py 1.2 => 1.3 ===
+ 
+     def testBigInput(self):
+-        input = IISet(range(50000))
+-        reversed = range(50000)
+-        reversed.reverse()
+-        reversed = IISet(reversed)
+-        output = multiunion([input, reversed] * 5)
+-        self.assertEqual(len(output), 50000)
+-        self.assertEqual(list(output), range(50000))
++        N = 100000
++        input = IISet(range(N))
++        output = multiunion([input] * 10)
++        self.assertEqual(len(output), N)
++        self.assertEqual(output.minKey(), 0)
++        self.assertEqual(output.maxKey(), N-1)
++        self.assertEqual(list(output), range(N))
+ 
+     def testLotsOfLittleOnes(self):
+         from random import shuffle
+
+
+
+
+From tim.one at comcast.net  Sat Jun  1 01:49:19 2002
+From: tim.one at comcast.net (Tim Peters)
+Date: Fri, 31 May 2002 20:49:19 -0400
+Subject: [Zope-Checkins] CVS: Zope/lib/python/BTrees - SetOpTemplate.c:1.16
+Message-ID: <200206010049.g510nJR14723 at cvs.baymountain.com>
+
+Update of /cvs-repository/Zope/lib/python/BTrees
+In directory cvs.zope.org:/tmp/cvs-serv14639
+
+Modified Files:
+        SetOpTemplate.c 
+Log Message:
+multiunion():  For an input that's IIBucket-based (IIBucket and IISet),
+this now copies the keys into the work area in one gulp via memcpy,
+instead of iterating over them one at a time.  Yields a nice speedup when
+it applies (and it usually should apply!).
+
+
+=== Zope/lib/python/BTrees/SetOpTemplate.c 1.15 => 1.16 ===
+        set.  At this point, we ignore the possibility of duplicates. */
+     for (i = 0; i < n; ++i) {
+-        SetIteration setiter = {0, 0, 0};
+-        int merge;  /* dummy needed for initSetIteration */
+-
+         set = PySequence_GetItem(seq, i);
+         if (set == NULL)
+             goto Error;
+ 
+-        /* XXX TODO: If set is a bucket, do a straight resize+memcpy instead.
+-        */
+-        if (initSetIteration(&setiter, set, 1, &merge) < 0)
+-            goto Error;
+-        if (setiter.next(&setiter) < 0)
+-            goto Error;
+-        while (setiter.position >= 0) {
+-            if (result->len >= result->size && Bucket_grow(result, -1, 1) < 0)
++        /* If set is a bucket, do a straight resize + memcpy. */
++        if (set->ob_type == (PyTypeObject*)&SetType ||
++            set->ob_type == (PyTypeObject*)&BucketType) {
++            const int setsize = SIZED(set)->len;
++            int size_desired = result->len + setsize;
++            /* If there are more to come, overallocate by 25% (arbitrary). */
++            if (i < n-1)
++                size_desired += size_desired >> 2;
++            if (size_desired && size_desired > result->size) {
++                if (Bucket_grow(result, size_desired, 1) < 0)
++                    goto Error;
++            }
++            memcpy(result->keys + result->len,
++                   BUCKET(set)->keys,
++                   setsize * sizeof(KEY_TYPE));
++            result->len += setsize;
++        }
++        else {
++            /* No cheap way:  iterate over set's elements one at a time. */
++            SetIteration setiter = {0, 0, 0};
++            int merge;  /* dummy needed for initSetIteration */
++            
++            if (initSetIteration(&setiter, set, 1, &merge) < 0)
+                 goto Error;
+-            COPY_KEY(result->keys[result->len], setiter.key);
+-            ++result->len;
+-            /* We know the key is an int, so no need to incref it. */
+             if (setiter.next(&setiter) < 0)
+                 goto Error;
++            while (setiter.position >= 0) {
++                if (result->len >= result->size && Bucket_grow(result, -1, 1) < 0)
++                    goto Error;
++                COPY_KEY(result->keys[result->len], setiter.key);
++                ++result->len;
++                /* We know the key is an int, so no need to incref it. */
++                if (setiter.next(&setiter) < 0)
++                    goto Error;
++            }
+         }
+         Py_DECREF(set);
+         set = NULL;


Property changes on: Zope3/trunk/src/z3checkins/tests/mbox_with_dupes.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/sample_import_msg.txt
===================================================================
--- Zope3/trunk/src/z3checkins/tests/sample_import_msg.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/sample_import_msg.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,22 @@
+From: Foo Bar <foo.bar at bar.com>
+Subject: [Zope3-checkins] CVS: Zope3/src/foo/bar - Imported sources
+Date: Fri, 28 Mar 2003 11:58:05 +03:00
+Message-Id: <42 at bar.com>
+
+Update of /cvs-repository/Zope3/src/foo/bar
+In directory cvs.zope.org:/tmp/cvs-serv12345
+
+Log message:
+Ipsum suum dolores quantum est er nonsensicum textum writum esmum inum tuum
+lineum furum testum logum messageum.
+
+Status:
+
+Vendor Tag:\tbarfulator
+Release Tags:\tyo-yo
+
+N Zope3/src/foo/bar/baz.py
+N Zope3/src/foo/bar/bar.pt
+
+No conflicts created by this import
+


Property changes on: Zope3/trunk/src/z3checkins/tests/sample_import_msg.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/sample_msg1.txt
===================================================================
--- Zope3/trunk/src/z3checkins/tests/sample_msg1.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/sample_msg1.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,33 @@
+From: Foo Bar <foo.bar at bar.com>
+Subject: [Zope3-checkins] CVS: Zope3/src/foo/bar - baz.py:1.2
+Date: Fri, 28 Mar 2003 11:58:05 +03:00
+Message-Id: <42 at bar.com>
+
+Update of /cvs-repository/Zope3/src/foo/bar
+In directory cvs.zope.org:/tmp/cvs-serv12345
+
+Modified files:
+        baz.py
+Log message:
+Ipsum suum dolores quantum est er nonsensicum textum writum esmum inum tuum
+lineum furum testum logum messageum.
+
+
+=== Zope3/src/foo/bar/baz.py 1.1 => 1.2 ===
+--- Zope3/src/foo/bar/baz.py:1.1    Tue Mar 25 15:21:29 2003
++++ Zope3/src/foo/bar/baz.py        Fri Mar 28 11:57:34 2003
+@@@ -123,7 +123,7 @@
+ lalala
+ burbur
+ barbar
+-xxx
++yyy
+ www
+ quux
+ furumburum
+
+
+_______________________________________________
+Zope3-Checkins mailing list
+Zope3-Checkins at zope.org
+http://mail.zope.org/mailman/listinfo/zope3-checkins


Property changes on: Zope3/trunk/src/z3checkins/tests/sample_msg1.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/sample_msg2.txt
===================================================================
--- Zope3/trunk/src/z3checkins/tests/sample_msg2.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/sample_msg2.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,21 @@
+From: Foo Bar <foo.bar at bar.com>
+Subject: [Zope3-checkins] CVS: Zope3/src/foo/bar - baz.py:1.2
+Date: Fri, 28 Mar 2003 11:58:05 +03:00
+Message-Id: <42 at bar.com>
+
+Update of /cvs-repository/Zope3/src/foo/bar
+In directory cvs.zope.org:/tmp/cvs-serv12345
+
+Added files:
+      Tag: foo-branch
+        baz.py
+Log Message:
+Ipsum suum dolores quantum est er nonsensicum textum writum esmum inum tuum
+lineum furum testum logum messageum.
+
+
+=== Added File Zope3/src/foo/bar/baz.py 1.1 ===
+lalala
+burbur
+barbar
+


Property changes on: Zope3/trunk/src/z3checkins/tests/sample_msg2.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/simple_msg.txt
===================================================================
--- Zope3/trunk/src/z3checkins/tests/simple_msg.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/simple_msg.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,7 @@
+From: John Doe <john at example.com>
+Subject: Something happened!
+Date: Wed, 29 Jul 2003 14:42:11 +0200
+Message-Id: <q$w$e$r$t$y at example.com>
+
+This is just a simple message.
+


Property changes on: Zope3/trunk/src/z3checkins/tests/simple_msg.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/svn_msg.txt
===================================================================
--- Zope3/trunk/src/z3checkins/tests/svn_msg.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/svn_msg.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,37 @@
+To: checkins at lists.schooltool.org
+From: Albertas Agejevas <alga at pov.lt>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Message-Id: <20030908101551.6F900C32F at mail.pov.lt>
+Date: Mon,  8 Sep 2003 13:15:51 +0300 (EEST)
+Subject: [schooltool-checkins] rev 10 - trunk/schooltool
+
+Author: alga
+Date: 2003-09-08 13:15:50 +0300 (Mon, 08 Sep 2003)
+New Revision: 10
+
+Modified:
+   trunk/schooltool/README
+Log:
+Added a period.
+
+
+Modified: trunk/schooltool/README
+===================================================================
+--- trunk/schooltool/README	2003-09-05 16:54:07 UTC (rev 9)
++++ trunk/schooltool/README	2003-09-08 10:15:50 UTC (rev 10)
+@@ -1,7 +1,7 @@
+ SchoolTool
+ ==========
+ 
+-SchoolTool - common information systems platform for school administration
++SchoolTool - common information systems platform for school administration.
+ 
+ Website: http://www.schooltool.org/
+ 
+
+_______________________________________________
+Checkins mailing list
+Checkins at lists.schooltool.org
+http://lists.schooltool.org/mailman/listinfo/checkins
+


Property changes on: Zope3/trunk/src/z3checkins/tests/svn_msg.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/svn_msg2.txt
===================================================================
--- Zope3/trunk/src/z3checkins/tests/svn_msg2.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/svn_msg2.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,38 @@
+To: checkins at lists.schooltool.org
+From: Albertas Agejevas <alga at pov.lt>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Message-Id: <20030908101551.6F900C32F at mail.pov.lt>
+Date: Mon,  8 Sep 2003 13:15:51 +0300 (EEST)
+Subject: [schooltool-checkins] rev 10 -
+ trunk/schooltool
+
+Author: alga
+Date: 2003-09-08 13:15:50 +0300 (Mon, 08 Sep 2003)
+New Revision: 10
+
+Modified:
+   trunk/schooltool/README
+Log:
+Added a period.
+
+
+Modified: trunk/schooltool/README
+===================================================================
+--- trunk/schooltool/README	2003-09-05 16:54:07 UTC (rev 9)
++++ trunk/schooltool/README	2003-09-08 10:15:50 UTC (rev 10)
+@@ -1,7 +1,7 @@
+ SchoolTool
+ ==========
+ 
+-SchoolTool - common information systems platform for school administration
++SchoolTool - common information systems platform for school administration.
+ 
+ Website: http://www.schooltool.org/
+ 
+
+_______________________________________________
+Checkins mailing list
+Checkins at lists.schooltool.org
+http://lists.schooltool.org/mailman/listinfo/checkins
+


Property changes on: Zope3/trunk/src/z3checkins/tests/svn_msg2.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/svn_msg3.txt
===================================================================
--- Zope3/trunk/src/z3checkins/tests/svn_msg3.txt	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/svn_msg3.txt	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,15 @@
+To: checkins at lists.schooltool.org
+From: Albertas Agejevas <alga at pov.lt>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Message-Id: <20030909101551.6F900C32F at mail.pov.lt>
+Date: Mon,  8 Sep 2003 13:15:51 +0300 (EEST)
+Subject: [schooltool-checkins] rev 10 had a bug in it
+
+Blah blah blah
+
+_______________________________________________
+Checkins mailing list
+Checkins at lists.schooltool.org
+http://lists.schooltool.org/mailman/listinfo/checkins
+


Property changes on: Zope3/trunk/src/z3checkins/tests/svn_msg3.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/tests/test_message.py
===================================================================
--- Zope3/trunk/src/z3checkins/tests/test_message.py	2004-05-19 09:45:58 UTC (rev 24817)
+++ Zope3/trunk/src/z3checkins/tests/test_message.py	2004-05-19 10:25:41 UTC (rev 24818)
@@ -0,0 +1,1124 @@
+#!/usr/bin/python
+"""
+Unit tests for message.py
+
+$Id: test_message.py,v 1.26 2004/03/26 22:18:08 gintautasm Exp $
+"""
+
+import unittest
+import os
+import sys
+import time
+from difflib import SequenceMatcher
+from datetime import datetime, timedelta
+from zope.app.tests.placelesssetup import PlacelessSetup
+from zope.component import getService, servicenames
+from zope.interface import Interface, implements
+from zope.interface.verify import verifyObject
+from zope.exceptions import DuplicationError
+
+from z3checkins.interfaces import IMessage, IMessageContained, \
+        ICheckinMessage, IBookmark, IMessageParser, IMessageArchive
+
+
+class TestFixedTimezone(unittest.TestCase):
+
+    def test_timezone(self):
+        from z3checkins.message import FixedTimezone
+        for tzoff, name in ((30, "+0030"), (-300, "-0500")):
+            tz = FixedTimezone(tzoff)
+            self.assertEquals(tz.tzname(None), name)
+            self.assertEquals(tz.utcoffset(None), timedelta(minutes=tzoff))
+            self.assertEquals(tz.dst(None), timedelta(0))
+
+
+class TestRFCDateTimeFormatter(unittest.TestCase):
+
+    times = ((2003, 4, 2, 12, 33, 41, 3*60, "Wed, 02 Apr 2003 12:33:41 +0300"),
+             (2000, 1, 2, 17, 41, 33, -5*60, "Sun, 02 Jan 2000 17:41:33 -0500"))
+
+    def test_rfctime(self):
+        from z3checkins.message import FixedTimezone, RFCDateTimeFormatter
+        for Y, M, D, h, m, s, tz, res in self.times:
+            dt = datetime(Y, M, D, h, m, s, tzinfo=FixedTimezone(tz))
+            view = RFCDateTimeFormatter(dt, None)
+            self.assertEquals(str(view), res)
+            self.assertEquals(view(), res)
+
+
+class TestISODateTimeFormatter(unittest.TestCase):
+
+    times = ((2003, 4, 2, 12, 33, 41, 3*60, "2003-04-02 09:33"),
+             (2000, 1, 2, 17, 41, 33, -5*60, "2000-01-02 22:41"))
+
+    def test_usertz(self):
+        from z3checkins.message import ISODateTimeFormatter
+        t = time.time()
+        delta = ISODateTimeFormatter.userstz._offset * 60
+        self.assertEquals(time.gmtime(t)[:8], time.localtime(t - delta)[:8])
+
+    def test_isotime(self):
+        from z3checkins.message import FixedTimezone, ISODateTimeFormatter
+        for Y, M, D, h, m, s, tz, res in self.times:
+            dt = datetime(Y, M, D, h, m, s, tzinfo=FixedTimezone(tz))
+            dt -= ISODateTimeFormatter.userstz.utcoffset(None)
+            view = ISODateTimeFormatter(dt, None)
+            self.assertEquals(str(view), res)
+            self.assertEquals(view(), res)
+
+
+class TestCheckinMessage(unittest.TestCase):
+
+    def test_find_body_start(self):
+        from z3checkins.message import find_body_start
+        self.assertEquals(find_body_start("Foo: X\nBar: Y\n  Z\n\nQQQ"), 19)
+        self.assertEquals(find_body_start("Foo: X\r\nBar: Y\r\n  Z\r\n\r\nQQQ"),
+                          23)
+        self.assertEquals(find_body_start("Foo: X\n\nQQQ\n\nWWW"), 8)
+        self.assertEquals(find_body_start("Foo: X\n\n\nQQQ\n\nWWW"), 8)
+        self.assertEquals(find_body_start("Foo: X\n\nQQQ\r\n\r\nWWW"), 8)
+        self.assertEquals(find_body_start("Foo: X\r\n\r\nQQQ\n\nWWW"), 10)
+        self.assertEquals(find_body_start("Foo: X\n\n"), 8)
+        self.assertEquals(find_body_start("\r\n\r\n"), 4)
+        self.assertEquals(find_body_start("xyzzy"), 5)
+
+    def test_interface(self):
+        from z3checkins.message import Message
+        from z3checkins.message import CheckinMessage
+        verifyObject(IMessage, Message())
+        verifyObject(ICheckinMessage, CheckinMessage())
+
+    def test_body(self):
+        from z3checkins.message import Message
+        m = Message(full_text="Subject: foo\n\nBody text\n")
+        self.assertEquals(m.body, "Body text\n")
+
+    def test_equality(self):
+        from z3checkins.message import Message
+        a = Message(message_id="abc")
+        b = Message(message_id="abc")
+        c = Message(message_id="xyz")
+        self.assertEquals(a, b)
+        self.assertNotEquals(a, c)
+        self.assertNotEquals(b, c)
+
+
+class TestCheckinMessageParser(unittest.TestCase):
+
+    def test_interface(self):
+        from z3checkins.message import CheckinMessageParser
+        verifyObject(IMessageParser, CheckinMessageParser())
+
+    def test_parser1(self):
+        from z3checkins.message import CheckinMessageParser
+        from z3checkins.message import FixedTimezone
+        sample_msg1 = open_test_data("sample_msg1.txt")
+        sample_msg1_text = sample_msg1.read()
+        sample_msg1.seek(0)
+        parser = CheckinMessageParser()
+        msg = parser.parse(sample_msg1)
+        self.assert_(ICheckinMessage.providedBy(msg))
+        self.assertEquals(msg.message_id, "42 at bar.com")
+        self.assertEquals(msg.author_name, "Foo Bar")
+        self.assertEquals(msg.author_email, "foo.bar at bar.com")
+        self.assertEquals(msg.date, datetime(2003, 03, 28, 11, 58, 05,
+                                             tzinfo=FixedTimezone(3*60)))
+        self.assertEquals(msg.directory, "Zope3/src/foo/bar")
+        self.assertEquals(msg.branch, None)
+        self.assertEquals(msg.log_message, """\
+Ipsum suum dolores quantum est er nonsensicum textum writum esmum inum tuum
+lineum furum testum logum messageum.""")
+        self.assertEquals(msg.body,
+                          sample_msg1_text.split("\n\n", 1)[1])
+
+    def test_parser2(self):
+        from z3checkins.message import CheckinMessageParser
+        from z3checkins.message import FixedTimezone
+        sample_msg2 = open_test_data("sample_msg2.txt")
+        sample_msg2_text = sample_msg2.read()
+        sample_msg2.seek(0)
+        parser = CheckinMessageParser()
+        msg = parser.parse(sample_msg2)
+        self.assert_(ICheckinMessage.providedBy(msg))
+        self.assertEquals(msg.message_id, "42 at bar.com")
+        self.assertEquals(msg.author_name, "Foo Bar")
+        self.assertEquals(msg.author_email, "foo.bar at bar.com")
+        self.assertEquals(msg.date, datetime(2003, 03, 28, 11, 58, 05,
+                                             tzinfo=FixedTimezone(3*60)))
+        self.assertEquals(msg.directory, "Zope3/src/foo/bar")
+        self.assertEquals(msg.branch, "foo-branch")
+        self.assertEquals(msg.log_message, """\
+Ipsum suum dolores quantum est er nonsensicum textum writum esmum inum tuum
+lineum furum testum logum messageum.""")
+        self.assertEquals(msg.body,
+                          sample_msg2_text.split("\n\n", 1)[1])
+
+    def test_parser_empty(self):
+        from z3checkins.message import CheckinMessageParser
+        from z3checkins.interfaces import FormatError
+        parser = CheckinMessageParser()
+        self.assertRaises(FormatError, parser.parse, '')
+
+    def test_parser_importmsg(self):
+        from z3checkins.message import CheckinMessageParser
+        from z3checkins.message import FixedTimezone
+        sample_import_msg = open_test_data("sample_import_msg.txt")
+        sample_import_msg_text = sample_import_msg.read()
+        sample_import_msg.seek(0)
+        parser = CheckinMessageParser()
+        msg = parser.parse(sample_import_msg)
+        self.assert_(ICheckinMessage.providedBy(msg))
+        self.assertEquals(msg.message_id, "42 at bar.com")
+        self.assertEquals(msg.author_name, "Foo Bar")
+        self.assertEquals(msg.author_email, "foo.bar at bar.com")
+        self.assertEquals(msg.date, datetime(2003, 03, 28, 11, 58, 05,
+                                             tzinfo=FixedTimezone(3*60)))
+        self.assertEquals(msg.directory, "Zope3/src/foo/bar")
+        self.assertEquals(msg.branch, None)
+        self.assertEquals(msg.log_message, """\
+Ipsum suum dolores quantum est er nonsensicum textum writum esmum inum tuum
+lineum furum testum logum messageum.""")
+        self.assertEquals(msg.body,
+                          sample_import_msg_text.split("\n\n", 1)[1])
+
+    def test_parser_simplemsg(self):
+        from z3checkins.message import CheckinMessageParser
+        from z3checkins.message import FixedTimezone
+        simple_msg = open_test_data("simple_msg.txt")
+        simple_msg_text = simple_msg.read()
+        simple_msg.seek(0)
+        parser = CheckinMessageParser()
+        msg = parser.parse(simple_msg)
+        self.assert_(IMessage.providedBy(msg))
+        self.assert_(not ICheckinMessage.providedBy(msg))
+        self.assertEquals(msg.message_id, "q$w$e$r$t$y at example.com")
+        self.assertEquals(msg.author_name, "John Doe")
+        self.assertEquals(msg.author_email, "john at example.com")
+        self.assertEquals(msg.date, datetime(2003, 07, 29, 14, 42, 11,
+                                             tzinfo=FixedTimezone(2*60)))
+        self.assertEquals(msg.body, simple_msg_text.split("\n\n", 1)[1])
+
+    def test_parser_svnmsg(self):
+        from z3checkins.message import CheckinMessageParser
+        from z3checkins.message import FixedTimezone
+        svn_msg = open_test_data("svn_msg.txt")
+        svn_msg_text = svn_msg.read()
+        svn_msg.seek(0)
+        parser = CheckinMessageParser()
+        msg = parser.parse(svn_msg)
+        self.assert_(ICheckinMessage.providedBy(msg))
+        self.assertEquals(msg.message_id, "20030908101551.6F900C32F at mail.pov.lt")
+        self.assertEquals(msg.author_name, "Albertas Agejevas")
+        self.assertEquals(msg.author_email, "alga at pov.lt")
+        self.assertEquals(msg.date, datetime(2003, 9, 8, 13, 15, 51,
+                                             tzinfo=FixedTimezone(3*60)))
+        self.assertEquals(msg.directory, "trunk/schooltool")
+        self.assertEquals(msg.branch, None)
+        self.assertEquals(msg.log_message, """Added a period.""")
+        self.assertEquals(msg.body,
+                          svn_msg_text.split("\n\n", 1)[1])
+
+    def test_parser_svnmsg_with_split_subject(self):
+        from z3checkins.message import CheckinMessageParser
+        from z3checkins.message import FixedTimezone
+        svn_msg2 = open_test_data("svn_msg2.txt")
+        svn_msg2_text = svn_msg2.read()
+        svn_msg2.seek(0)
+        parser = CheckinMessageParser()
+        msg = parser.parse(svn_msg2)
+        self.assert_(ICheckinMessage.providedBy(msg))
+        self.assertEquals(msg.message_id, "20030908101551.6F900C32F at mail.pov.lt")
+        self.assertEquals(msg.author_name, "Albertas Agejevas")
+        self.assertEquals(msg.author_email, "alga at pov.lt")
+        self.assertEquals(msg.date, datetime(2003, 9, 8, 13, 15, 51,
+                                             tzinfo=FixedTimezone(3*60)))
+        self.assertEquals(msg.directory, "trunk/schooltool")
+        self.assertEquals(msg.branch, None)
+        self.assertEquals(msg.log_message, """Added a period.""")
+        self.assertEquals(msg.body,
+                          svn_msg2_text.split("\n\n", 1)[1])
+
+    def test_parser_svnmsg_with_rev(self):
+        from z3checkins.message import CheckinMessageParser
+        from z3checkins.message import FixedTimezone
+        svn_msg3 = open_test_data("svn_msg3.txt")
+        svn_msg3_text = svn_msg3.read()
+        svn_msg3.seek(0)
+        parser = CheckinMessageParser()
+        msg = parser.parse(svn_msg3)
+        self.assert_(IMessage.providedBy(msg))
+        self.assertEquals(msg.message_id, "20030909101551.6F900C32F at mail.pov.lt")
+        self.assertEquals(msg.author_name, "Albertas Agejevas")
+        self.assertEquals(msg.author_email, "alga at pov.lt")
+        self.assertEquals(msg.date, datetime(2003, 9, 8, 13, 15, 51,
+                                             tzinfo=FixedTimezone(3*60)))
+        self.assertEquals(msg.body, svn_msg3_text.split("\n\n", 1)[1])
+
+
+class MessageStub:
+
+    implements(ICheckinMessage, IMessageContained)
+
+    __name__ = __parent__ = None
+
+    def __init__(self, data=None, date=None, body=None, log_message='',
+                       subject='', message_id="message at id"):
+        self.data = data
+        self.date = date
+        self.body = body
+        self.subject = subject
+        self.log_message = log_message
+        self.message_id = message_id
+
+
+class TestMessageContainerAdapter(unittest.TestCase):
+
+    def test_interface(self):
+        from z3checkins.message import MessageContainerAdapter
+        verifyObject(IMessageArchive, MessageContainerAdapter({}))
+
+    def test_len(self):
+        from z3checkins.message import MessageContainerAdapter
+        a = MessageContainerAdapter({})
+        self.assertEquals(len(a), 0)
+        a = MessageContainerAdapter({'1': 2, '3': 'abc'})
+        self.assertEquals(len(a), 0)
+        a = MessageContainerAdapter({'1': 2, '3': 'abc', 4: MessageStub()})
+        self.assertEquals(len(a), 1)
+
+    def test_getitem(self):
+        from z3checkins.message import MessageContainerAdapter
+        a = MessageContainerAdapter({'1': 2, '3': 'abc',
+                                     '4': MessageStub(date=1, message_id='1'),
+                                     '5': MessageStub(date=4, message_id='2'),
+                                     '6': MessageStub(date=3, message_id='3'),
+                                     '7': MessageStub(date=2, message_id='4')})
+        self.assertEquals(a[0].message_id, '1')
+        self.assertEquals(a[1].message_id, '4')
+        self.assertEquals(a[2].message_id, '3')
+        self.assertEquals(a[3].message_id, '2')
+        self.assertEquals(a[-1].message_id, '2')
+        self.assertRaises(IndexError, a.__getitem__, 4)
+        self.assertRaises(IndexError, a.__getitem__, -5)
+        self.assertRaises(TypeError, a.__getitem__, 'xyzzy')
+        self.assertRaises(TypeError, a.__getitem__, None)
+        self.assertEquals(len(a[1:3]), 2)
+        self.assertEquals(len(a[1:-1]), 2)
+        self.assertEquals(len(a[3:1]), 0)
+
+    def test_iter(self):
+        from z3checkins.message import MessageContainerAdapter
+        a = MessageContainerAdapter({'1': 2, '3': 'abc',
+                                     '4': MessageStub(date=1, message_id='1'),
+                                     '5': MessageStub(date=4, message_id='2'),
+                                     '6': MessageStub(date=3, message_id='3'),
+                                     '7': MessageStub(date=2, message_id='4')})
+        b = [x.message_id for x in a]
+        self.assertEquals(b, ['1', '4', '3', '2'])
+        self.assert_(MessageStub(message_id='5') not in a)
+        self.assert_(a.context['6'] in a)
+
+    def test_index(self):
+        from z3checkins.message import MessageContainerAdapter
+        m1 = MessageStub(date=1, message_id='1')
+        m2 = MessageStub(date=4, message_id='2')
+        m3 = MessageStub(date=3, message_id='3')
+        m4 = MessageStub(date=2, message_id='4')
+        a = MessageContainerAdapter({'1': 2, '3': 'abc',
+                                     '4': m1, '5': m2, '6': m3, '7': m4})
+        self.assertEquals(a.index(m1), 0)
+        self.assertEquals(a.index(m4), 1)
+        self.assertEquals(a.index(m3), 2)
+        self.assertEquals(a.index(m2), 3)
+        self.assertRaises(ValueError, a.index, MessageStub)
+
+
+class ParserStub:
+
+    implements(IMessageParser)
+
+    def parse(self, data):
+        if hasattr(data, 'read'):
+            full_text = data.read()
+        else:
+            full_text = data
+
+        message_id = "message at id"
+        id_lines = filter(lambda s: s.lower().startswith("message-id: "),
+                full_text.splitlines())
+        if len(id_lines) == 1:
+            message_id = id_lines[0][len("message-id: "):]
+        return MessageStub(data=full_text, message_id=message_id)
+
+class AddingStub:
+
+    def __init__(self):
+        self.added = []
+
+    def add(self, obj):
+        # ignore duplicates happening with default messages
+        if obj.message_id != "message at id":
+            for message in self.added:
+                if message.message_id == obj.message_id:
+                    raise DuplicationError()
+        self.added.append(obj)
+
+class TestMessageUpload(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        getService(None, 'Utilities').provideUtility(IMessageParser,
+                                                     ParserStub())
+
+    def test_createAndAdd(self):
+        from z3checkins.message import MessageUpload
+        view = MessageUpload()
+        view.context = AddingStub()
+        view.add = view.context.add
+        added = view.context.added
+        self.assertEquals(len(added), 0)
+        view.createAndAdd({})
+        self.assertEquals(len(added), 0)
+        view.createAndAdd({'data': 'Ipsum suum'})
+        self.assertEquals(len(added), 1)
+        self.assertEquals(added[0].__class__, MessageStub)
+        self.assertEquals(added[0].message_id, "message at id")
+        self.assertEquals(added[0].data, "Ipsum suum")
+
+    def test_createAndAdd_mbox(self):
+        from z3checkins.message import MessageUpload
+        view = MessageUpload()
+        view.context = AddingStub()
+        view.add = view.context.add
+        added = view.context.added
+        data = open_test_data('mbox.txt').read()
+        self.assertEquals(len(added), 0)
+        view.createAndAdd({'data': data})
+        self.assertEquals(len(added), 4)
+        for message in added:
+            self.assertEquals(message.__class__, MessageStub)
+        self.assertEquals(added[0].data.count("Steve Alexander"), 1)
+        self.assertEquals(added[1].data.count("Steve Alexander"), 1)
+        self.assertEquals(added[2].data.count("Tim Peters"), 1)
+        self.assertEquals(added[3].data.count("Tim Peters"), 1)
+
+    def test_createAndAdd_mbox_with_dupes(self):
+        from z3checkins.message import MessageUpload
+        view = MessageUpload()
+        view.context = AddingStub()
+        view.add = view.context.add
+        added = view.context.added
+        data = open_test_data('mbox_with_dupes.txt').read()
+        self.assertEquals(len(added), 0)
+        view.createAndAdd({'data': data})
+        self.assertEquals(len(added), 2)
+        for message in added:
+            self.assertEquals(message.__class__, MessageStub)
+        self.assertEquals(added[0].data.count("Steve Alexander"), 1)
+        self.assertEquals(added[1].data.count("Tim Peters"), 1)
+
+
+class IUnitTestPresentation(Interface):
+    pass
+
+class IRequest(Interface):
+    pass
+
+class MessageTestView:
+    def __init__(self, context, request):
+        self.context = context
+    def __call__(self, same_as_previous=False):
+        result = 'msg%d' % self.context.date
+        if same_as_previous:
+            result += '*'
+        return result + '\n'
+
+class BookmarkTestView:
+    def __init__(self, context, request):
+        self.context = context
+    def __call__(self, same_as_previous=False):
+        return '-\n'
+
+class RequestStub(dict):
+
+    implements(IRequest)
+
+    _cookies = ()
+
+    def __init__(self, **kw):
+        super(RequestStub, self).__init__()
+        self.update(kw)
+        self.response = self
+
+    def setCookie(self, name, value, **kw):
+        self._cookies += (name, value, kw)
+
+    def getPresentationType(self):
+        return IUnitTestPresentation
+
+    def getPresentationSkin(self):
+        return ''
+
+class TestContainerView(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        from z3checkins.message import MessageContainerAdapter
+        getService(None, servicenames.Adapters).register(
+                [None], IMessageArchive, "", MessageContainerAdapter)
+
+    def test_checkins(self):
+        from z3checkins.message import ContainerView
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(), 'z': MessageStub(date=1),
+                        'a': MessageStub(date=2), 'c': MessageStub(date=3)}
+        view.request = {}
+        res = view.checkins()
+        self.assertEquals(len(res), 3)
+        self.assertEquals(view.count(), 3)
+        self.assertEquals(res[0].date, 3)
+        self.assertEquals(res[1].date, 2)
+        self.assertEquals(res[2].date, 1)
+
+    def test_checkins_limited(self):
+        from z3checkins.message import ContainerView
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(), 'z': MessageStub(date=1),
+                        'a': MessageStub(date=2), 'c': MessageStub(date=3)}
+        view.request = {}
+        res = view.checkins(size=2)
+        self.assertEquals(len(res), 2)
+        self.assertEquals(res[0].date, 3)
+        self.assertEquals(res[1].date, 2)
+
+        res = view.checkins(start=1, size=3)
+        self.assertEquals(len(res), 2)
+        self.assertEquals(res[0].date, 2)
+        self.assertEquals(res[1].date, 1)
+
+    def test_checkins_bookmarks(self):
+        from z3checkins.message import ContainerView
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(), 'z': MessageStub(date=1),
+                        'a': MessageStub(date=2), 'c': MessageStub(date=4)}
+        view.request = {}
+        view.bookmarks = lambda: [3]
+        res = view.checkins()
+        self.assertEquals(len(res), 4)
+        self.assertEquals(res[0].date, 4)
+        self.assert_(IBookmark.providedBy(res[1]))
+        self.assertEquals(res[2].date, 2)
+        self.assertEquals(res[3].date, 1)
+
+        view.bookmarks = lambda: [2]
+        res = view.checkins()
+        self.assertEquals(len(res), 4)
+        self.assertEquals(res[0].date, 4)
+        self.assert_(IBookmark.providedBy(res[1]))
+        self.assertEquals(res[2].date, 2)
+        self.assertEquals(res[3].date, 1)
+
+        view.bookmarks = lambda: [0, 1, 2, 3, 4, 5, 6, 2, 3, 1]
+        res = view.checkins()
+        self.assertEquals(len(res), 5)
+        self.assertEquals(res[0].date, 4)
+        self.assert_(IBookmark.providedBy(res[1]))
+        self.assertEquals(res[2].date, 2)
+        self.assert_(IBookmark.providedBy(res[3]))
+        self.assertEquals(res[4].date, 1)
+
+        res = view.checkins(start=1, size=1)
+        self.assertEquals(len(res), 3)
+        self.assert_(IBookmark.providedBy(res[0]))
+        self.assertEquals(res[1].date, 2)
+        self.assert_(IBookmark.providedBy(res[2]))
+
+    def test_bookmarks(self):
+        from z3checkins.message import ContainerView
+        from z3checkins.message import FixedTimezone
+        view = ContainerView()
+        view.request = {}
+        self.assertEquals(view.bookmarks(), [])
+        view.request = {'bookmarks': '2003-01-04T21:33:04-05:00'}
+        self.assertEquals(view.bookmarks(),
+                          [datetime(2003, 01, 04, 21, 33, 04,
+                                    tzinfo=FixedTimezone(-5*60))])
+        view.request = {'bookmarks': '2003-01-04T21:33:04-05:00 '
+                                     'errors are ignored '
+                                     '2004-05-06T07:08:09+10:00 '
+                                     '2002-02-02T02:02:02+02:00 '
+                                     '2005-02-29T07:08:09+10:00'}
+        self.assertEquals(view.bookmarks(),
+                          [datetime(2003, 1, 4, 21, 33, 4,
+                                    tzinfo=FixedTimezone(-5*60)),
+                           datetime(2004, 5, 6, 7, 8, 9,
+                                    tzinfo=FixedTimezone(10*60)),
+                           datetime(2002, 2, 2, 2, 2, 2,
+                                    tzinfo=FixedTimezone(2*60))])
+
+    def test_placeBookmark_empty_archive(self):
+        from z3checkins.message import ContainerView
+        from z3checkins.message import FixedTimezone
+        view = ContainerView()
+        view.context = {}
+        view.request = RequestStub()
+        view.placeBookmark()
+        self.assertEquals(view.request._cookies, ())
+
+    def test_placeBookmark_empty_bookmarks(self):
+        from z3checkins.message import ContainerView
+        from z3checkins.message import FixedTimezone
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(),
+                        'z': MessageStub(date=datetime(2003, 1, 4, 21, 33, 4,
+                                                tzinfo=FixedTimezone(-5*60)))}
+        view.request = RequestStub()
+        view.placeBookmark()
+        self.assertEquals(view.request._cookies,
+                          ('bookmarks', '2003-01-04T21:33:04-05:00',
+                           {'max_age': 31536000}))
+
+    def test_placeBookmark_not_at_start(self):
+        from z3checkins.message import ContainerView
+        from z3checkins.message import FixedTimezone
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(),
+                        'z': MessageStub(date=datetime(2003, 1, 4, 21, 33, 4,
+                                                tzinfo=FixedTimezone(-5*60)))}
+        view.request = RequestStub(start=1)
+        view.placeBookmark()
+        self.assertEquals(view.request._cookies, ())
+
+    def test_placeBookmark_no_new_checkins(self):
+        from z3checkins.message import ContainerView
+        from z3checkins.message import FixedTimezone
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(),
+                        'z': MessageStub(date=datetime(2003, 1, 4, 21, 33, 4,
+                                                tzinfo=FixedTimezone(-5*60)))}
+        view.request = RequestStub(bookmarks='2003-01-04T21:33:04-05:00 '
+                                             'errors are ignored '
+                                             '2002-02-02T02:02:02+02:00')
+        view.placeBookmark()
+        self.assertEquals(view.request._cookies, ())
+
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(),
+                        'z': MessageStub(date=datetime(2003, 1, 4, 21, 33, 4,
+                                                tzinfo=FixedTimezone(-5*60)))}
+        view.request = RequestStub(bookmarks='2004-01-04T21:33:04-05:00 '
+                                             'errors are ignored '
+                                             '2002-02-02T02:02:02+02:00')
+        view.placeBookmark()
+        self.assertEquals(view.request._cookies, ())
+
+    def test_placeBookmark_new_checkins(self):
+        from z3checkins.message import ContainerView
+        from z3checkins.message import FixedTimezone
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(),
+                        'z': MessageStub(date=datetime(2003, 1, 4, 21, 33, 4,
+                                                tzinfo=FixedTimezone(-5*60))),
+                        'w': MessageStub(date=datetime(2003, 1, 6, 22, 33, 44,
+                                                tzinfo=FixedTimezone(+3*60)))}
+        view.request = RequestStub(bookmarks='2003-01-04T21:33:04-05:00 '
+                                             'errors are ignored '
+                                             '2002-02-02T02:02:02+02:00')
+        view.placeBookmark()
+        self.assertEquals(view.request._cookies,
+                          ('bookmarks', '2002-02-02T02:02:02+02:00 '
+                                        '2003-01-04T21:33:04-05:00 '
+                                        '2003-01-06T22:33:44+03:00',
+                           {'max_age': 31536000}))
+
+    def test_placeBookmark_new_checkins_overflow(self):
+        from z3checkins.message import ContainerView
+        from z3checkins.message import FixedTimezone
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(),
+                        'z': MessageStub(date=datetime(2003, 1, 4, 21, 33, 4,
+                                                tzinfo=FixedTimezone(-5*60))),
+                        'w': MessageStub(date=datetime(2003, 1, 6, 22, 33, 44,
+                                                tzinfo=FixedTimezone(+3*60)))}
+        view.request = RequestStub(bookmarks='2003-01-04T21:33:04-05:00 '
+                                             'errors are ignored '
+                                             '2002-01-01T02:02:02+02:00 '
+                                             '2002-01-02T02:02:02+02:00 '
+                                             '2002-01-03T02:02:02+02:00 '
+                                             '2002-01-04T02:02:02+02:00 '
+                                             '2002-02-02T02:02:02+02:00 ')
+        view.placeBookmark()
+        self.assertEquals(view.request._cookies,
+                          ('bookmarks', '2002-01-03T02:02:02+02:00 '
+                                        '2002-01-04T02:02:02+02:00 '
+                                        '2002-02-02T02:02:02+02:00 '
+                                        '2003-01-04T21:33:04-05:00 '
+                                        '2003-01-06T22:33:44+03:00',
+                           {'max_age': 31536000}))
+
+    def test_renderCheckins(self):
+        from z3checkins.message import ContainerView
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(),
+                        'z': MessageStub(date=1, log_message='xxx'),
+                        'a': MessageStub(date=2, log_message='xxx'),
+                        'c': MessageStub(date=3, log_message='yyy')}
+        view.request = RequestStub()
+        view.index = view
+        getService(None, servicenames.Presentation).provideView(
+                ICheckinMessage, 'html', IRequest, MessageTestView)
+
+        res = view.renderCheckins()
+        self.assertEquals(res, 'msg3\nmsg2\nmsg1*\n')
+        res = view.renderCheckins(start=1, size=1)
+        self.assertEquals(res, 'msg2\n')
+
+    def test_renderCheckins_with_bookmarks(self):
+        from z3checkins.message import ContainerView
+        view = ContainerView()
+        view.context = {'x': 123, 'y': object(),
+                        'z': MessageStub(date=1, log_message='xxx'),
+                        'a': MessageStub(date=2, log_message='xxx'),
+                        'c': MessageStub(date=3, log_message='yyy')}
+        view.request = RequestStub()
+        view.index = view
+        view.bookmarks = lambda: [1]
+        getService(None, servicenames.Presentation).provideView(
+                ICheckinMessage, 'html', IRequest, MessageTestView)
+        getService(None, servicenames.Presentation).provideView(
+                IBookmark, 'html', IRequest, BookmarkTestView)
+
+        res = view.renderCheckins()
+        self.assertEquals(res, 'msg3\nmsg2\n-\nmsg1*\n')
+
+
+def diff(a, b):
+    "Compare the differences of two sequences of strings"
+
+    if isinstance(a, (str, unicode)): a = a.splitlines()
+    if isinstance(b, (str, unicode)): b = b.splitlines()
+
+    diff = []
+    def dump(tag, x, lo, hi, diff=diff):
+        for i in xrange(lo, hi):
+            diff.append(tag + x[i])
+
+    differ = SequenceMatcher(a=a, b=b)
+    for tag, alo, ahi, blo, bhi in differ.get_opcodes():
+        if tag == 'replace':
+            dump('-', a, alo, ahi)
+            dump('+', b, blo, bhi)
+        elif tag == 'delete':
+            dump('-', a, alo, ahi)
+        elif tag == 'insert':
+            dump('+', b, blo, bhi)
+        elif tag == 'equal':
+            dump(' ', a, alo, ahi)
+        else:
+            raise ValueError, 'unknown tag ' + `tag`
+    return "\n".join(diff)
+
+class TestCheckinMessageView(PlacelessSetup, unittest.TestCase):
+
+    def setUp(self):
+        PlacelessSetup.setUp(self)
+        from z3checkins.message import MessageContainerAdapter
+        getService(None, servicenames.Adapters).register(
+                [None], IMessageArchive, "", MessageContainerAdapter)
+
+    def test_body_strange(self):
+        from z3checkins.message import CheckinMessageView
+        view = CheckinMessageView()
+        view.context = MessageStub(body="Something & strange")
+        self.assertEquals(view.body(), "<pre>Something &amp; strange</pre>")
+
+    def test_body(self):
+        from z3checkins.message import CheckinMessageView
+        view = CheckinMessageView()
+        view.context = MessageStub(body="Blah blah\n"
+                          "blah\n"
+                          "Log message:\n"
+                          "Blurb blurb\n"
+                          "blurb.\n"
+                          "\n"
+                          "=== foo.py: 1.2 -> 1.3 ===\n"
+                          "--- foo.py:1.2\tdatetime\n"
+                          "+++ foo.py\tdatetime\n"
+                          "@@@ -123,4 +567,8 @@@\n"
+                          " fwoosh <>&\"\n"
+                          "-fouoww\n"
+                          "+fruuuh\n"
+                          " fargle\n"
+                          "_______________________________________________\n"
+                          "signature\n")
+        result = view.body()
+        expected = ('<pre>Blah blah\n'
+                    'blah\n'
+                    'Log message:\n'
+                    '</pre>'
+                    '<div class="log">'
+                    '<p>Blurb blurb</p>\n'
+                    '<p>blurb.</p>'
+                    '</div>'
+                    '<pre>\n'
+                    '<div class="file">=== foo.py: 1.2 -&gt; 1.3 ===\n</div>'
+                    '<div class="oldfile">--- foo.py:1.2<span class="tab">>--</span>datetime\n</div>'
+                    '<div class="newfile">+++ foo.py<span class="tab">>------</span>datetime\n</div>'
+                    '<div class="chunk">@@@ -123,4 +567,8 @@@\n</div>'
+                    ' fwoosh &lt;&gt;&amp;&quot;\n'
+                    '<div class="old">-fouoww\n</div>'
+                    '<div class="new">+fruuuh\n</div>'
+                    ' fargle'
+                    '<div class="signature">\n'
+                    '_______________________________________________\n'
+                    'signature\n'
+                    '</div>'
+                    '</pre>')
+        self.assertEquals(result, expected, diff(expected, result))
+
+    def test_body_svn(self):
+        from z3checkins.message import CheckinMessageView
+        view = CheckinMessageView()
+        view.context = MessageStub(body="""\
+Author: mg
+Date: 2003-09-09 21:21:09 +0300 (Tue, 09 Sep 2003)
+New Revision: 15
+
+Added:
+   trunk/schooltool/schooltool/ftests/
+   trunk/schooltool/schooltool/ftests/__init__.py
+   trunk/schooltool/schooltool/ftests/test_rest.py
+   trunk/schooltool/schooltool/main.py
+   trunk/schooltool/schooltool/tests/test_main.py
+Removed:
+   trunk/schooltool/schooltool/tests/test_rest.py
+Modified:
+   trunk/schooltool/schooltool/tests/__init__.py
+Log:
+First prototype of SchoolTool HTTP server that serves RESTful pages, complete
+with unit and functional tests.
+
+
+
+Added: trunk/schooltool/schooltool/ftests/__init__.py
+===================================================================
+--- trunk/schooltool/schooltool/ftests/__init__.py	2003-09-09 18:03:43 UTC (rev 14)
++++ trunk/schooltool/schooltool/ftests/__init__.py	2003-09-09 18:21:09 UTC (rev 15)
+@@ -0,0 +1,21 @@
++#
++# SchoolTool - common information systems platform for school administration
+"""
+)
+        result = view.body()
+        expected = ("""\
+<pre>Author: mg
+Date: 2003-09-09 21:21:09 +0300 (Tue, 09 Sep 2003)
+New Revision: 15
+
+Added:
+   trunk/schooltool/schooltool/ftests/
+   trunk/schooltool/schooltool/ftests/__init__.py
+   trunk/schooltool/schooltool/ftests/test_rest.py
+   trunk/schooltool/schooltool/main.py
+   trunk/schooltool/schooltool/tests/test_main.py
+Removed:
+   trunk/schooltool/schooltool/tests/test_rest.py
+Modified:
+   trunk/schooltool/schooltool/tests/__init__.py
+Log:
+</pre><div class="log"><p>First prototype of SchoolTool HTTP server that serves RESTful pages, complete</p>
+<p>with unit and functional tests.</p></div><pre>
+Added: trunk/schooltool/schooltool/ftests/__init__.py
+<div class="file">===================================================================
+</div><div class="oldfile">--- trunk/schooltool/schooltool/ftests/__init__.py<span class="tab">>------</span>2003-09-09 18:03:43 UTC (rev 14)
+</div><div class="newfile">+++ trunk/schooltool/schooltool/ftests/__init__.py<span class="tab">>------</span>2003-09-09 18:21:09 UTC (rev 15)
+</div><div class="chunk">@@ -0,0 +1,21 @@
+</div><div class="new">+#
+</div><div class="new">+# SchoolTool - common information systems platform for school administration</div></pre>"""
+                    )
+        self.assertEquals(result, expected, diff(expected, result))
+
+    def test_body_crlf(self):
+        from z3checkins.message import CheckinMessageView
+        view = CheckinMessageView()
+        view.context = MessageStub(body="Blah blah\r\n"
+                          "blah\r\n"
+                          "Log message:\r\n"
+                          "Blurb blurb\r\n"
+                          "blurb.\r\n"
+                          "\r\n"
+                          "=== foo.py: 1.2 -> 1.3 ===\r\n"
+                          "--- foo.py:1.2\tdatetime\r\n"
+                          "+++ foo.py\tdatetime\r\n"
+                          "@@@ -123,4 +567,8 @@@\r\n"
+                          " fwoosh <>&\"\r\n"
+                          "-fouoww  \r\n"
+                          "+fruuuh\r\n"
+                          " fargle\r\n"
+                          "   \r\n"
+                          " \r\n"
+                          "_______________________________________________\r\n"
+                          "signature\r\n")
+        result = view.body()
+        expected = ('<pre>Blah blah\n'
+                    'blah\n'
+                    'Log message:\n'
+                    '</pre>'
+                    '<div class="log">'
+                    '<p>Blurb blurb</p>\n'
+                    '<p>blurb.</p>'
+                    '</div>'
+                    '<pre>\n'
+                    '<div class="file">=== foo.py: 1.2 -&gt; 1.3 ===\n</div>'
+                    '<div class="oldfile">--- foo.py:1.2<span class="tab">>--</span>datetime\n</div>'
+                    '<div class="newfile">+++ foo.py<span class="tab">>------</span>datetime\n</div>'
+                    '<div class="chunk">@@@ -123,4 +567,8 @@@\n</div>'
+                    ' fwoosh &lt;&gt;&amp;&quot;\n'
+                    '<div class="old">-fouoww<span class="trail">..</span>\n</div>'
+                    '<div class="new">+fruuuh\n</div>'
+                    ' fargle\n'
+                    ' <span class="trail">..</span>\n'
+                    ' '
+                    '<div class="signature">\n'
+                    '_______________________________________________\n'
+                    'signature\n'
+                    '</div>'
+                    '</pre>')
+        self.assertEquals(result, expected, diff(expected, result))
+
+    def test_body_nosig(self):
+        from z3checkins.message import CheckinMessageView
+        view = CheckinMessageView()
+        view.context = MessageStub(body="Blah blah\n"
+                          "blah\n"
+                          "Log message:\n"
+                          "Blurb blurb\n"
+                          "blurb.\n"
+                          "\n"
+                          "=== foo.py: 1.2 -> 1.3 ===\n"
+                          "--- foo.py:1.2\tdatetime\n"
+                          "+++ foo.py\tdatetime\n"
+                          "@@@ -123,4 +567,8 @@@\n"
+                          " fwoosh <>&\"\n"
+                          "-fouoww\n"
+                          "+fruuuh\n"
+                          " fargle")
+        result = view.body()
+        expected = ('<pre>Blah blah\n'
+                    'blah\n'
+                    'Log message:\n'
+                    '</pre>'
+                    '<div class="log">'
+                    '<p>Blurb blurb</p>\n'
+                    '<p>blurb.</p>'
+                    '</div>'
+                    '<pre>\n'
+                    '<div class="file">=== foo.py: 1.2 -&gt; 1.3 ===\n</div>'
+                    '<div class="oldfile">--- foo.py:1.2<span class="tab">>--</span>datetime\n</div>'
+                    '<div class="newfile">+++ foo.py<span class="tab">>------</span>datetime\n</div>'
+                    '<div class="chunk">@@@ -123,4 +567,8 @@@\n</div>'
+                    ' fwoosh &lt;&gt;&amp;&quot;\n'
+                    '<div class="old">-fouoww\n</div>'
+                    '<div class="new">+fruuuh\n</div>'
+                    ' fargle'
+                    '</pre>')
+        self.assertEquals(result, expected, diff(expected, result))
+
+    def test_body_importmsg(self):
+        from z3checkins.message import CheckinMessageView
+        view = CheckinMessageView()
+        view.context = MessageStub(body="Blah blah\n"
+                          "blah\n"
+                          "Log message:\n"
+                          "Blurb blurb\n"
+                          "blurb.\n"
+                          "\n"
+                          "Status:\n"
+                          "\n"
+                          "Vendor Tag:\tnovendor\n"
+                          "Release Tags:\tstart\n"
+                          "\n"
+                          "N foo/bar.py\n"
+                          "N foo/baz.pt\n"
+                          "\n"
+                          "No conflicts created by this import\n")
+        result = view.body()
+        expected = ('<pre>Blah blah\n'
+                    'blah\n'
+                    'Log message:\n'
+                    '</pre>'
+                    '<div class="log">'
+                    '<p>Blurb blurb</p>\n'
+                    '<p>blurb.</p>'
+                    '</div>'
+                    '<pre>\n'
+                    'Status:\n'
+                    '\n'
+                    'Vendor Tag:\tnovendor\n'
+                    'Release Tags:\tstart\n'
+                    '\n'
+                    'N foo/bar.py\n'
+                    'N foo/baz.pt\n'
+                    '\n'
+                    'No conflicts created by this import\n'
+                    '</pre>')
+        self.assertEquals(result, expected, diff(expected, result))
+
+    def test_markwitespace(self):
+        from z3checkins.message import CheckinMessageView
+        view = CheckinMessageView()
+        m = view.mark_whitespace
+        self.assertEquals(m(''), '')
+        self.assertEquals(m('xyzzy'), 'xyzzy')
+        self.assertEquals(m('  '), ' <span class="trail">.</span>')
+        self.assertEquals(m('  xy z  '), '  xy z<span class="trail">..</span>')
+        self.assertEquals(m('  xy z \t '), '  xy z<span class="trail">.<span class="tab">>-</span>.</span>')
+        self.assertEquals(m(' \t|'), ' <span class="tab">>-------</span>|')
+        self.assertEquals(m(' |\t|'), ' |<span class="tab">>------</span>|')
+        self.assertEquals(m(' xxxxxx|\t|'), ' xxxxxx|<span class="tab">></span>|')
+        self.assertEquals(m(' x<tag\t>xxxxx|\t|'), ' x<tag\t>xxxxx|<span class="tab">></span>|')
+        self.assertEquals(m(' x&ent;xxxx|\t|'), ' x&ent;xxxx|<span class="tab">></span>|')
+
+    def test_urls(self):
+        from z3checkins.message import CheckinMessageView
+        view = CheckinMessageView()
+        prefixes = ['', 'A link: ', '(', '<']
+        suffixes = ['', ' etc.', ')', '>', '.', ',', '\n']
+        urls = ['http://www.example.com', 'https://www.example.com', 'http://localhost:8080/foo?q=a&w=b']
+
+        def quote(text):
+            return (text.replace('&', '&amp;')
+                        .replace('<', '&lt;')
+                        .replace('>', '&gt;')
+                        .replace('"', '&quot;'))
+
+        for prefix in prefixes:
+            for link in urls:
+                for suffix in suffixes:
+                    view.context = MessageStub(body="%s%s%s" % (prefix, link, suffix))
+                    prefix = quote(prefix)
+                    link = quote(link)
+                    suffix = quote(suffix)
+                    self.assertEquals(view.body(), '<pre>%s<a href="%s">%s</a>%s</pre>' % (prefix, link, link, suffix))
+
+    def test_navigation_no_archive(self):
+        from z3checkins.message import CheckinMessageView
+        view = CheckinMessageView()
+        view.context = MessageStub()
+        self.assertEquals(view.first(), None)
+        self.assertEquals(view.last(), None)
+        self.assertEquals(view.next(), None)
+        self.assertEquals(view.previous(), None)
+
+    def test_navigation_empty_archive(self):
+        from z3checkins.message import CheckinMessageView
+        view = CheckinMessageView()
+        view.context = MessageStub()
+        view.context.__parent__ = {'1': 2}
+        self.assertEquals(view.first(), None)
+        self.assertEquals(view.last(), None)
+        self.assertEquals(view.next(), None)
+        self.assertEquals(view.previous(), None)
+
+    def test_navigation(self):
+        from z3checkins.message import CheckinMessageView
+        m1 = MessageStub(date=1, message_id='1')
+        m2 = MessageStub(date=2, message_id='2')
+        m3 = MessageStub(date=3, message_id='3')
+        m4 = MessageStub(date=4, message_id='4')
+
+        folder = {'1': 2, '3': 'abc', '4': m1, '5': m2, '6': m3, '7': m4}
+        view = CheckinMessageView()
+        view.context = m3
+        m3.__parent__ = folder
+        self.assertEquals(view.first(), m1)
+        self.assertEquals(view.last(), m4)
+        self.assertEquals(view.next(), m4)
+        self.assertEquals(view.previous(), m2)
+
+        view = CheckinMessageView()
+        view.context = m1
+        m1.__parent__ = folder
+        self.assertEquals(view.first(), m1)
+        self.assertEquals(view.last(), m4)
+        self.assertEquals(view.next(), m2)
+        self.assertEquals(view.previous(), None)
+
+        view = CheckinMessageView()
+        view.context = m4
+        m4.__parent__ = folder
+        self.assertEquals(view.first(), m1)
+        self.assertEquals(view.last(), m4)
+        self.assertEquals(view.next(), None)
+        self.assertEquals(view.previous(), m3)
+
+
+class TestMessageNameChooser(unittest.TestCase):
+
+    def test_chooseName(self):
+        from z3checkins.folder import MessageNameChooser
+        msg = MessageStub(message_id="msg at id")
+        chooser = MessageNameChooser(None)
+        self.assertEquals(chooser.chooseName(None, msg), msg.message_id)
+        msg2 = MessageStub(message_id="foo at bar")
+        self.assertEquals(chooser.chooseName(None, msg2), msg2.message_id)
+
+    def test_checkName(self):
+        from z3checkins.folder import MessageNameChooser
+        msg = MessageStub(message_id="msg at id")
+        chooser = MessageNameChooser(None)
+        self.assertEquals(chooser.checkName("msg at id", msg), True)
+
+
+class TestMessageSized(unittest.TestCase):
+
+    def test_interface(self):
+        from z3checkins.folder import MessageSized
+        from zope.app.size.interfaces import ISized
+        self.assert_(ISized.providedBy(MessageSized(MessageStub())))
+
+    def test_sizeForSorting(self):
+        from z3checkins.folder import MessageSized
+        msg = MessageStub()
+        sized = MessageSized(msg)
+        msg.full_text = '*' * 42;
+        self.assertEquals(sized.sizeForSorting(), 42)
+        msg.full_text = '*' * 32768;
+        self.assertEquals(sized.sizeForSorting(), 32768)
+
+    def test_sizeForDisplay(self):
+        from z3checkins.folder import MessageSized
+        msg = MessageStub()
+        sized = MessageSized(msg)
+        msg.full_text = '*' * 42;
+        self.assertEquals(sized.sizeForDisplay(), u"42 bytes")
+        msg.full_text = '*' * 32767;
+        self.assertEquals(sized.sizeForDisplay(), u"31 KB")
+        msg.full_text = '*' * 32768;
+        self.assertEquals(sized.sizeForDisplay(), u"32 KB")
+
+
+def open_test_data(filename):
+    """Open a file relative to the location of this module."""
+    base = os.path.dirname(__file__)
+    return open(os.path.join(base, filename))
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TestFixedTimezone))
+    suite.addTest(unittest.makeSuite(TestRFCDateTimeFormatter))
+    suite.addTest(unittest.makeSuite(TestISODateTimeFormatter))
+    suite.addTest(unittest.makeSuite(TestCheckinMessage))
+    suite.addTest(unittest.makeSuite(TestCheckinMessageParser))
+    suite.addTest(unittest.makeSuite(TestMessageContainerAdapter))
+    suite.addTest(unittest.makeSuite(TestMessageUpload))
+    suite.addTest(unittest.makeSuite(TestContainerView))
+    suite.addTest(unittest.makeSuite(TestCheckinMessageView))
+    suite.addTest(unittest.makeSuite(TestMessageNameChooser))
+    suite.addTest(unittest.makeSuite(TestMessageSized))
+    return suite
+
+
+if __name__ == "__main__":
+    unittest.main()


Property changes on: Zope3/trunk/src/z3checkins/tests/test_message.py
___________________________________________________________________
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/z3checkins/zope3.png
===================================================================
(Binary files differ)


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




More information about the Zope3-Checkins mailing list