[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('&', '&') \
+ .replace('<', '<') \
+ .replace('>', '>') \
+ .replace('"', '"')
+ # 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" />