[Zope3-checkins] CVS: Products3/z3checkins - message.png:1.1 TODO:1.8 configure.zcml:1.8 interfaces.py:1.5 message.pt:1.5 message.py:1.14 message_part.pt:1.9 message_plain.pt:1.2 rss_container.pt:1.3 rss_message.pt:1.2

Marius Gedminas mgedmin@codeworks.lt
Fri, 1 Aug 2003 05:43:54 -0400


Update of /cvs-repository/Products3/z3checkins
In directory cvs.zope.org:/tmp/cvs-serv820

Modified Files:
	TODO configure.zcml interfaces.py message.pt message.py 
	message_part.pt message_plain.pt rss_container.pt 
	rss_message.pt 
Added Files:
	message.png 
Log Message:
It is now possible to add regular email messages to z3checkins.  Unfortunately
this breaks backwards compatibility with old pickles.



=== Added File Products3/z3checkins/message.png ===
  <Binary-ish file>

=== Products3/z3checkins/TODO 1.7 => 1.8 ===
--- Products3/z3checkins/TODO:1.7	Thu Jun  5 08:21:22 2003
+++ Products3/z3checkins/TODO	Fri Aug  1 05:43:16 2003
@@ -1,12 +1,17 @@
 I definitely want to do the following:
 
-- Highlight branch tags in message body
-- Detect URLs in checkin comments and make those into <a> elements
-- Include all messages sent to zope3-checkins, not just checkin messages
+- Make sure non-ASCII chars work both in headers and in bodies
 - A way to add multiple messages with a single upload (Unix mbox file)
+- Create an interface ICheckinsFolder extending IFolder and write a factory
+  to create folders marked with this interface.  Register z3checkins container
+  views only for ICheckinsFolders.
 
 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
+- Detect URLs in checkin comments and make those into <a> elements
 - 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
@@ -15,9 +20,5 @@
 - 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
-- Is storing all the messages in a single folder scalable enough?
-- Create an interface ICheckinsFolder extending IFolder and write a factory
-  to create folders marked with this interface.  Register z3checkins container
-  views only for ICheckinsFolders.  Perhaps wait until the interfacegheddon
-  before starting this.


=== Products3/z3checkins/configure.zcml 1.7 => 1.8 ===
--- Products3/z3checkins/configure.zcml:1.7	Tue Jul 29 15:35:22 2003
+++ Products3/z3checkins/configure.zcml	Fri Aug  1 05:43:16 2003
@@ -3,6 +3,16 @@
 
 <!-- CheckinMessage content object -->
 
+  <content class=".message.Message">
+
+    <require permission="zope.View"
+             interface=".interfaces.IMessage" />
+
+    <implements
+      interface="zope.app.interfaces.annotation.IAttributeAnnotatable" />
+
+  </content>
+
   <content class=".message.CheckinMessage">
 
     <require permission="zope.View"
@@ -16,13 +26,13 @@
 <!-- Utilities -->
 
   <adapter for="zope.app.interfaces.container.IReadContainer"
-           factory=".message.CheckinMessageAdapter"
+           factory=".message.MessageContainerAdapter"
            permission="zope.View"
-           provides=".interfaces.ICheckinMessageArchive" />
+           provides=".interfaces.IMessageArchive" />
 
   <utility factory=".message.CheckinMessageParser"
            permission="zope.View"
-           provides=".interfaces.ICheckinMessageParser" />
+           provides=".interfaces.IMessageParser" />
 
 <!-- Generic views for date/time formatting -->
 
@@ -44,7 +54,7 @@
     permission="zope.Public"
     />
 
-<!-- Browser views -->
+<!-- Browser views: adding -->
 
   <browser:addform
     name="CheckinMessage"
@@ -56,10 +66,51 @@
     permission="zope.ManageContent"
     class=".message.MessageUpload" />
 
+<!-- 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_part.pt"
+    usage="sidebar"
+    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"
-    template="rss_message.pt"
+    class=".message.MessageRSSView"
+    attribute="index"
     permission="zope.View" />
 
   <browser:page
@@ -78,12 +129,6 @@
     permission="zope.View" />
 
   <browser:page
-    for=".interfaces.ICheckinBookmark"
-    name="html"
-    template="bookmark.pt"
-    permission="zope.View" />
-
-  <browser:page
     for=".interfaces.ICheckinMessage"
     name="index.html"
     template="message.pt"
@@ -97,6 +142,16 @@
     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="zope.app.interfaces.container.IContainer"
     name="checkins.rss"
@@ -121,6 +176,7 @@
 
 <!-- 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" />


=== Products3/z3checkins/interfaces.py 1.4 => 1.5 ===
--- Products3/z3checkins/interfaces.py:1.4	Wed Apr 16 18:01:48 2003
+++ Products3/z3checkins/interfaces.py	Fri Aug  1 05:43:16 2003
@@ -1,48 +1,57 @@
 """
-Interfaces for the z3-checkins product.
+Interfaces for the z3checkins product.
+
+$Id$
 """
 
 from zope.interface import Interface, Attribute
 
-class ICheckinMessage(Interface):
-    """Checkin message."""
+class IMessage(Interface):
+    """Mail message."""
 
-    message_id = Attribute("Unique checkin message ID")
+    message_id = Attribute("Unique message ID")
     author = Attribute("Author's name and email address in RFC822 format")
     author_name = Attribute("Author's real name")
     author_email = Attribute("Author's email address")
-    subject = Attribute("Subject line of the checking message")
+    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")
-    date = Attribute("Date and time of the checkin")
-    body = Attribute("Body of the message")
     # Maybe added_files, modified_files, removed_files listing files and their
     # revisions
-    # Maybe store full headers as well?
 
 
-class ICheckinBookmark(Interface):
-    """Bookmark placed between checkin messages."""
+class IBookmark(Interface):
+    """Bookmark placed between messages."""
 
 
 class FormatError(Exception):
-    """Ill-formed checkin message exception"""
+    """Ill-formed message exception"""
 
 
-class ICheckinMessageParser(Interface):
+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 ICheckinMessage.
+        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 or if it is not a
-        checkin message.
+        May raise a FormatError if the message is ill-formed.
         """
 
-class ICheckinMessageArchive(Interface):
-    """A chronologically ordered sequence of checkin messages.
+
+class IMessageArchive(Interface):
+    """A chronologically ordered sequence of messages.
 
     Implements the Python sequence procotol.
     """
@@ -62,5 +71,6 @@
     def index(message):
         """Returns the index of a given message.
 
-        Raises ValueError if message is not in the archive."""
+        Raises ValueError if message is not in the archive.
+        """
 


=== Products3/z3checkins/message.pt 1.4 => 1.5 ===
--- Products3/z3checkins/message.pt:1.4	Wed Apr 16 07:46:06 2003
+++ Products3/z3checkins/message.pt	Fri Aug  1 05:43:16 2003
@@ -36,7 +36,7 @@
 <img class="icon"
      tal:define="icon view/icon"
      tal:attributes="src icon/src; alt icon/alt; title icon/title" />
-<img tal:condition="context/branch"
+<img tal:condition="context/branch | nothing"
      class="icon" src="++resource++branch.png" alt="Branch"
      tal:attributes="title string:Branch: ${context/branch}"/>
 <div class="headers">


=== Products3/z3checkins/message.py 1.13 => 1.14 ===
--- Products3/z3checkins/message.py:1.13	Thu Jun  5 08:09:50 2003
+++ Products3/z3checkins/message.py	Fri Aug  1 05:43:16 2003
@@ -1,8 +1,10 @@
 """
-Python code for z3-checkins product.
+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$
 """
 
 import re
@@ -11,6 +13,7 @@
 from StringIO import StringIO
 from datetime import datetime, tzinfo, timedelta
 from persistence import Persistent
+from zope.interface import implements
 from zope.component import getUtility, getAdapter, queryAdapter
 from zope.app.browser.form.widget import FileWidget
 from zope.app.form.widget import CustomWidget
@@ -20,9 +23,12 @@
 from zope.proxy import removeAllProxies
 from zope.app.datetimeutils import parseDatetimetz, DateTimeError
 from zope.component import getView
+from zope.publisher.browser import BrowserView
+from zope.app.pagetemplate import ViewPageTemplateFile
 
-from interfaces import ICheckinMessage, ICheckinMessageParser, FormatError
-from interfaces import ICheckinMessageArchive, ICheckinBookmark
+from interfaces import IMessage, ICheckinMessage, IBookmark
+from interfaces import IMessageParser, IMessageArchive
+from interfaces import FormatError
 
 __metaclass__ = type
 
@@ -91,61 +97,89 @@
 # Checkin message content object
 #
 
-class CheckinMessage(Persistent):
-    """Persistent checkin message."""
+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)
+
 
-    __implements__ = ICheckinMessage
+class Message(Persistent):
+    """Persistent email message."""
+
+    implements(IMessage)
 
     def __init__(self, message_id=None, author=None, author_name=None,
-                 author_email=None, subject=None, date=None, directory=None,
-                 log_message=None, body=None, branch=None):
+                 author_email=None, subject=None, date=None,
+                 full_text=None):
         self.message_id = message_id
         self.author = author
         self.author_name = author_name
         self.author_email = author_email
         self.subject = subject
         self.date = date
-        self.directory = directory
-        self.log_message = log_message
-        self.body = body
-        self.branch = branch
+        self.full_text = full_text
 
-    def __setstate__(self, state):
-        super(CheckinMessage, self).__setstate__(state)
-        if 'branch' not in state:
-            self.branch = None
-            log_idx = self.body.index("\nLog Message:")
-            if log_idx == -1:
-                log_idx = self.body.index("\nLog message:")
-            if log_idx != -1:
-                m = re.search("(?m)^      Tag: (.*)$", self.body[:log_idx])
-                if m:
-                    self.branch = m.group(1)
+    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 __cmp__(self, other):
         """Messages with the same message_id compare identical."""
-        if not isinstance(other, CheckinMessage):
+        if not IMessage.isImplementedBy(other):
             raise NotImplementedError
         return cmp(self.message_id, other.message_id)
 
 
+class CheckinMessage(Message):
+    """Persistent checkin message."""
+
+    implements(ICheckinMessage)
+
+    def __init__(self, message_id=None, author=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=author, 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__ = ICheckinMessageParser
+    implements(IMessageParser)
 
     _subject_prefix = '[Zope3-checkins] CVS: '
 
     def parse(self, input):
         if not hasattr(input, 'readline'):
-            input = StringIO(input)
+            full_text = str(input)
+            input = StringIO(full_text)
+        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()
+            input = StringIO(full_text)
+
         m = rfc822.Message(input)
         subject = m.getheader('Subject')
-        if not subject.startswith(self._subject_prefix):
-            raise FormatError("Subject line does not indicate"
-                              " this is a checkin message")
-        subject = subject[len(self._subject_prefix):]
-        directory = subject.split(' - ')[0]
         message_id = m.getheader('Message-Id')
         author = m.getheader('From')
         author_name, author_email = m.getaddr('From')
@@ -157,15 +191,26 @@
          weekday, yearday, dst, tzoffset) = rfc822.parsedate_tz(date)
         date = datetime(year, month, day, hours, minutes, seconds,
                         tzinfo=FixedTimezone(tzoffset / 60))
+
+        if not subject.startswith(self._subject_prefix):
+            return Message(message_id=message_id, author=author,
+                           author_name=author_name,
+                           author_email=author_email, subject=subject,
+                           date=date, full_text=full_text)
+
+        subject = subject[len(self._subject_prefix):]
+        directory = subject.split(' - ')[0]
         m.rewindbody()
         body_lines = input.readlines()
         log_message, branch = self.extract_log(body_lines)
-        body = "".join(body_lines).strip()
+        # XXX perhaps if log message is not found (extract_log raises
+        #     FormatError), fall back to adding a simple Message
         return CheckinMessage(message_id=message_id, author=author,
                               author_name=author_name,
                               author_email=author_email, subject=subject,
-                              date=date, directory=directory,
-                              log_message=log_message, body=body, branch=branch)
+                              date=date, full_text=full_text,
+                              directory=directory, log_message=log_message,
+                              branch=branch)
 
     def extract_log(self, lines):
         log_message = []
@@ -187,17 +232,17 @@
         return "".join(log_message).strip(), branch
 
 
-class CheckinMessageAdapter:
-    """Adapts a container to a checkin message archive."""
+class MessageContainerAdapter:
+    """Adapts a container to a message archive."""
 
-    __implements__ = ICheckinMessageArchive
+    implements(IMessageArchive)
     __used_for__ = IReadContainer
 
     def __init__(self, context):
         self.context = context
         items = []
         for key, item in self.context.items():
-            if ICheckinMessage.isImplementedBy(item):
+            if IMessage.isImplementedBy(item):
                 items.append((item.date, key, item))
         items.sort()
         self.messages = [ContextWrapper(item, self.context, name=key)
@@ -219,9 +264,9 @@
         return self.messages.index(message)
 
 
-class CheckinBookmark:
+class Bookmark:
 
-    __implements__ = ICheckinBookmark
+    implements(IBookmark)
 
 
 #
@@ -231,10 +276,10 @@
 class MessageUpload:
     """Adding view mixin for uploading checkin messages."""
 
-    data = CustomWidget(FileWidget)
+    data_widget = CustomWidget(FileWidget)
 
     def createAndAdd(self, data):
-        parser = getUtility(self.context, ICheckinMessageParser)
+        parser = getUtility(self.context, IMessageParser)
         message = parser.parse(data['data'])
         if not self.context.contentName:
             self.context.contentName = message.message_id
@@ -266,7 +311,7 @@
         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, ICheckinMessageArchive)
+            self._archive = getAdapter(self.context, IMessageArchive)
         if not self._archive:
             return # No messages -- no bookmarks
         bookmarks = self.bookmarks()
@@ -289,7 +334,7 @@
         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, ICheckinMessageArchive)
+            self._archive = getAdapter(self.context, IMessageArchive)
         idx = len(self._archive) - start
         items = self._archive[max(0, idx-size):idx]
         items = removeAllProxies(items)
@@ -302,7 +347,7 @@
         n = 1
         while n < len(items):
             if bookmarkBetween(items[n-1], items[n]):
-                items.insert(n, CheckinBookmark())
+                items.insert(n, Bookmark())
                 n += 2
             else:
                 n += 1
@@ -310,10 +355,10 @@
         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, CheckinBookmark())
+                items.insert(0, Bookmark())
             after = self._archive[idx:idx+1]
             if after and bookmarkBetween(items[-1], after[0]):
-                items.insert(len(items), CheckinBookmark())
+                items.insert(len(items), Bookmark())
         # reverse order to present newest checkins first
         items.reverse()
         return items
@@ -339,20 +384,27 @@
     def count(self):
         """Returns the number of checkin messages in the archive."""
         if not hasattr(self, '_archive'):
-            self._archive = getAdapter(self.context, ICheckinMessageArchive)
+            self._archive = getAdapter(self.context, IMessageArchive)
         return len(self._archive)
 
 
-class CheckinMessageView:
-    """View mixin for checkin messages.
+class MessageRSSView(BrowserView):
+    """View for messages.
+
+    Makes sure the page template is treated as XML.
+    """
+
+    index = ViewPageTemplateFile('rss_message.pt', content_type='text/xml')
 
-    Provides a syntax-highlighted body."""
+
+class MessageView:
+    """View mixin for messages."""
 
     def _calc_index(self):
         if not hasattr(self, '_archive'):
             container = getWrapperContainer(self.context)
             self._archive = container and queryAdapter(container,
-                                                       ICheckinMessageArchive)
+                                                       IMessageArchive)
         if not self._archive:
             self._index = None
         elif not hasattr(self, '_index'):
@@ -389,6 +441,28 @@
             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': ''}
+
+    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."""
 
     def icon(self):
         """Returns a mapping describing an icon for this checkin.  The mapping


=== Products3/z3checkins/message_part.pt 1.8 => 1.9 ===
--- Products3/z3checkins/message_part.pt:1.8	Wed May 28 02:57:10 2003
+++ Products3/z3checkins/message_part.pt	Fri Aug  1 05:43:16 2003
@@ -4,16 +4,18 @@
   <img class="icon"
        tal:define="icon view/icon"
        tal:attributes="src icon/src; alt icon/alt; title icon/title" />
-  <img tal:condition="context/branch"
+  <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>


=== Products3/z3checkins/message_plain.pt 1.1 => 1.2 ===
--- Products3/z3checkins/message_plain.pt:1.1	Thu Apr  3 07:08:24 2003
+++ Products3/z3checkins/message_plain.pt	Fri Aug  1 05:43:16 2003
@@ -1,6 +1 @@
-From: <span tal:replace="structure context/author" />
-Date: <span tal:replace="structure context/date/@@rfc822" />
-Subject: <span tal:replace="structure string:[Zope3-checkins] CVS: ${context/subject}" />
-Message-Id: <span tal:replace="structure context/message_id" />
-
-<div tal:replace="structure context/body" />
+<tal:block tal:replace="structure context/full_text" />


=== Products3/z3checkins/rss_container.pt 1.2 => 1.3 ===
--- Products3/z3checkins/rss_container.pt:1.2	Tue Apr 15 09:26:22 2003
+++ Products3/z3checkins/rss_container.pt	Fri Aug  1 05:43:16 2003
@@ -2,7 +2,7 @@
 <rss version="2.0" xmlns:tal="http://xml.zope.org/namespaces/tal">
   <channel tal:define="webmaster context/webmaster_email | nothing">
     <title>Zope 3 Checkins</title>
-    <link tal:content="string:${context/@@absolute_url}/@@checkins.html" />
+    <link tal:content="string:${context/@@absolute_url}/@@checkins.html"></link>
     <description>Latest Zope 3 Checkins</description>
     <language>en-us</language>
     <docs>http://backend.userland.com/rss</docs>


=== Products3/z3checkins/rss_message.pt 1.1.1.1 => 1.2 ===
--- Products3/z3checkins/rss_message.pt:1.1.1.1	Thu Apr  3 03:19:59 2003
+++ Products3/z3checkins/rss_message.pt	Fri Aug  1 05:43:16 2003
@@ -1,7 +1,7 @@
-<item>
+<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" />
+  <description tal:content="context/log_message | nothing" />
   <guid tal:content="context/@@absolute_url" />
   <author tal:content="context/author" />
   <pubDate tal:content="context/date/@@rfc822" />