[Zope3-checkins] CVS: zopeproducts/zwiki - diff.py:1.1 READMDE.txt:1.4 TODO.txt:1.14 configure.zcml:1.20 interfaces.py:1.7 wiki.py:1.4 wikipage.py:1.3

Stephan Richter srichter@cbu.edu
Thu, 10 Apr 2003 21:38:16 -0400


Update of /cvs-repository/zopeproducts/zwiki
In directory cvs.zope.org:/tmp/cvs-serv29237

Modified Files:
	READMDE.txt TODO.txt configure.zcml interfaces.py wiki.py 
	wikipage.py 
Added Files:
	diff.py 
Log Message:
- Checked in a diff modules for making diffs between the old and the new
  version of the Wiki Page contents

- Added very simple mail subscription

- Some more Tweaks in the older code.

Ok, I am too tired to write more senseful things right now...


=== Added File zopeproducts/zwiki/diff.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Browser View Components for WikiPages

$Id: diff.py,v 1.1 2003/04/11 01:37:45 srichter Exp $
"""
from difflib import ndiff

MAX_OLD_LINES_DISPLAY = 40
MAX_NEW_LINES_DISPLAY = 40


def textdiff(old_text, new_text, verbose=1):
    """
    generate a plain text diff, optimized for human readability,
    between two revisions of this page, numbering back from the latest.
    Alternately, a and/or b texts can be specified.
    """

    old = split(old_text, '\n')
    new = split(new_text, '\n')
    cruncher=ndiff.SequenceMatcher(
        isjunk=lambda x: x in " \\t",
        a=old,
        b=new)

    r = []
    for tag, old_lo, old_hi, new_lo, new_hi in cruncher.get_opcodes():
        if tag == 'replace':
            if verbose: r.append('??changed:')
            r = r + _abbreviateDiffLines(old[old_lo:old_hi],'-',
                                         MAX_OLD_LINES_DISPLAY)
            r = r + _abbreviateDiffLines(new[new_lo:new_hi],'',
                                         MAX_NEW_LINES_DISPLAY)
            r.append('')
        elif tag == 'delete':
            if verbose: r.append('--removed:')
            r = r + _abbreviateDiffLines(old[old_lo:old_hi],'-',
                                         MAX_OLD_LINES_DISPLAY)
            r.append('')
        elif tag == 'insert':
            if verbose: r.append('++added:')
            r = r + _abbreviateDiffLines(new[new_lo:new_hi],'',
                                         MAX_NEW_LINES_DISPLAY)
            r.append('')
        elif tag == 'equal':
            pass
        else:
            raise ValueError, 'unknown tag ' + `tag`

    return '\n' + join(r, '\n')


def _abbreviateDiffLines(lines, prefix, maxlines=5):
    output = []
    if maxlines and len(lines) > maxlines:
        extra = len(lines) - maxlines
        for i in xrange(maxlines - 1):
            output.append(prefix + lines[i])
        output.append(prefix + "[%d more line%s...]" %
                      (extra, ((extra == 1) and '') or 's')) # not working
    else:
        for line in lines:
            output.append(prefix + line)
    return output


=== zopeproducts/zwiki/READMDE.txt 1.3 => 1.4 ===
--- zopeproducts/zwiki/READMDE.txt:1.3	Tue Apr  8 00:15:02 2003
+++ zopeproducts/zwiki/READMDE.txt	Thu Apr 10 21:37:45 2003
@@ -6,23 +6,45 @@
   much more work needs to be done.
 
   Features
+  --------
 
-    - Wiki object. Container of all Wiki pages.
+    Rendering
 
-    - WikiPage content object, holding the data, which turned out to be tiny.
+      - Plain Text
 
-    - Beginnings of the PageHierarchyAdapter...no view and not tested.
+      - Structured Text (STX)
 
-    - Rendered code recognizes Wiki names, the escaping '!' and the '[]' for
-      all lower case Wiki names.
+      - reStructured Text (reST)
 
-    - Generated Links bring you either to another Wiki page or to an add page.
 
-    - Assinging parents, allowing to create a "virtual" hierarchy.
+    Wiki
 
-    - Writing comments about the Wiki Page.
+      - Table of Contents
 
-    - Somewhat sophistocated rendering mechanism. New source types and their
+      - Mail Subscription for entire Wiki
+
+      - Full-text Search
+
+
+    Wiki Page
+
+      - Proper rendering of Wiki Links
+
+      - Edit Wiki Page
+
+      - Comment on a Wiki Page
+
+      - Declare Wiki Hierarchy (Parents)
+
+      - Local, WikiPage-based Mail Subscription
+
+      - Jumping to other Wikis
+
+
+    Miscellaneous
+
+    - Somewhat sophisticated rendering mechanism. New source types and their
       render methods can now be configured (added) via ZCML.
 
-    - Implemented minimal STX support.
\ No newline at end of file
+    - A fully independent skin called 'wiki'; Note that this skill will be
+      only useful in the context of a Wiki Page. 
\ No newline at end of file


=== zopeproducts/zwiki/TODO.txt 1.13 => 1.14 ===
--- zopeproducts/zwiki/TODO.txt:1.13	Thu Apr 10 07:31:38 2003
+++ zopeproducts/zwiki/TODO.txt	Thu Apr 10 21:37:45 2003
@@ -9,6 +9,16 @@
 
     - Add tests for plain text, STX, and ReST formatter.
 
+    - Add tests for WikiMailer
+
+    - Add tests for MailSubscriptions
+
+    - Add tests for WikiPageReadFile, WikiPageWriteFile, SearchableText
+
+    - Write tests for diff module
+
+    - Add a simple test for 'Wiki Text Index'.
+
 
   Rendering/Views
 
@@ -30,7 +40,6 @@
 
     - Check in Traverser that found subobj has self.context as parent.
 
-    - Implement events, so that we can have E-mail subscriptions to Wiki
-      changes.
-
-    - Implements E-mail subscriptions.
+    - Activating diff support for edited Wiki Pages. The main issue right now
+      is to get to the old version of the text. so that we can execute the
+      Differ. 
\ No newline at end of file


=== zopeproducts/zwiki/configure.zcml 1.19 => 1.20 ===
--- zopeproducts/zwiki/configure.zcml:1.19	Thu Apr 10 08:51:56 2003
+++ zopeproducts/zwiki/configure.zcml	Thu Apr 10 21:37:45 2003
@@ -126,6 +126,12 @@
 
   </content>
 
+  <!-- Mail Subscriptions support -->
+  <adapter
+      factory=".wikipage.MailSubscriptions"
+      provides=".interfaces.IMailSubscriptions"
+      for=".interfaces.IWiki" />
+
 
   <content class=".wikipage.WikiPage">
 
@@ -151,6 +157,12 @@
       provides=".interfaces.IWikiPageHierarchy"
       for=".interfaces.IWikiPage" />
 
+  <!-- Mail Subscriptions support -->
+  <adapter
+      factory=".wikipage.MailSubscriptions"
+      provides=".interfaces.IMailSubscriptions"
+      for=".interfaces.IWikiPage" />
+
   <adapter 
       factory=".traversal.WikiPageTraversable"
       provides="zope.app.interfaces.traversing.ITraversable"
@@ -208,6 +220,15 @@
         />
 
   </content>
+
+  <!-- Register event listener for change mails -->
+  <event:subscribe
+      subscriber=".wikipage.mailer"
+      event_types="zope.app.interfaces.event.IObjectAddedEvent
+                   zope.app.interfaces.event.IObjectModifiedEvent
+                   zope.app.interfaces.event.IObjectRemovedEvent
+                   zope.app.interfaces.event.IObjectMovedEvent" />
+
 
   <!-- Register the various renderers, like plain text, stx, and rest -->  
   <include package=".renderer" />


=== zopeproducts/zwiki/interfaces.py 1.6 => 1.7 ===
--- zopeproducts/zwiki/interfaces.py:1.6	Wed Apr  9 11:16:39 2003
+++ zopeproducts/zwiki/interfaces.py	Thu Apr 10 21:37:45 2003
@@ -66,10 +66,10 @@
     Pages."""
 
     parents = List(
-        title=_(u"Wiki Page Parents"),
-        description=_(u"Parents of a a Wiki"),
-        value_types=(TextLine(title=_(u"Parent Name"),
-                            description=_(u"Name of the parent wiki page.")),),
+        title = _(u"Wiki Page Parents"),
+        description = _(u"Parents of a a Wiki"),
+        value_types = (TextLine(title=_(u"Parent Name"),
+                         description=_(u"Name of the parent wiki page.")),),
         required=False)
 
     def reparent(parents):
@@ -79,6 +79,19 @@
            names of the parent wiki pages.
         """
 
+class IMailSubscriptions(Interface):
+    """This interface allows you to retrieve a list of E-mails for
+    mailings. In our context """
+
+    def getSubscriptions():
+        """Return a list of E-mails."""
+
+    def addSubscriptions(emails):
+        """Add a bunch of subscriptions, but one would be okay as well."""
+
+    def removeSubscriptions(emails):
+        """Remove a set of subscriptions."""
+        
 
 class IWikiSourceTypeService(Interface):
     """ """
@@ -93,7 +106,7 @@
     def getAllTitles():
         """Return a list of all titles."""
 
-    def createObject(self, title):
+    def createObject(title):
         """Creates an object that implements the interface (note these are
         just marker interfaces, so the object is minimal) that is registered
         with the title passed."""


=== zopeproducts/zwiki/wiki.py 1.3 => 1.4 ===
--- zopeproducts/zwiki/wiki.py:1.3	Thu Apr 10 08:50:41 2003
+++ zopeproducts/zwiki/wiki.py	Thu Apr 10 21:37:45 2003
@@ -17,7 +17,6 @@
 """
 from zope.app.content.folder import Folder
 from zopeproducts.zwiki.interfaces import IWiki
-from zopeproducts.zwiki.wikipage import WikiPage
 
 
 class Wiki(Folder):


=== zopeproducts/zwiki/wikipage.py 1.2 => 1.3 ===
--- zopeproducts/zwiki/wikipage.py:1.2	Thu Apr 10 07:31:38 2003
+++ zopeproducts/zwiki/wikipage.py	Thu Apr 10 21:37:45 2003
@@ -15,18 +15,28 @@
 
 $Id$
 """
+import smtplib
 from persistence import Persistent
 
 from zope.component import getAdapter
 from zope.proxy.context import ContextWrapper
 from zope.app.traversing import getParent, objectName
+
+from zope.app.interfaces.index.text import ISearchableText
+from zope.app.interfaces.file import IReadFile, IWriteFile
 from zope.app.interfaces.annotation import IAnnotations
+from zope.app.interfaces.event import ISubscriber
+from zope.app.interfaces.event import IObjectAddedEvent, IObjectModifiedEvent
+from zope.app.interfaces.event import IObjectRemovedEvent, IObjectMovedEvent
+
 
-from zopeproducts.zwiki.interfaces import IWikiPage, IWikiPageHierarchy
+from zopeproducts.zwiki.interfaces import \
+     IWiki, IWikiPage, IWikiPageHierarchy, IMailSubscriptions
 
 __metaclass__ = type
 
 HierarchyKey = 'http://www.zope.org/zwiki#1.0/PageHierarchy/parents'
+SubscriberKey = 'http://www.zope.org/zwiki#1.0/MailSubscriptions/emails'
 
 
 class WikiPage(Persistent):
@@ -75,7 +85,6 @@
         self.setParents(parents)
 
     def setParents(self, parents):
-        data = self._annotations.get(HierarchyKey)
         self._annotations[HierarchyKey] = tuple(parents)
 
     def getParents(self):
@@ -116,30 +125,42 @@
 # Adapters for file-system style access
 
 class WikiPageReadFile:
+    """Adapter for letting a Wiki Page look like a regular readable file."""
+
+    __implements__ = IReadFile
+    __used_for__ = IWikiPage
 
     def __init__(self, context):
         self.context = context
 
     def read(self):
+        """See zope.app.interfaces.file.IReadFile"""
         return self.context.source
 
     def size(self):
+        """See zope.app.interfaces.file.IReadFile"""
         return len(self.context.source)
 
+
 class WikiPageWriteFile:
+    """Adapter for letting a Wiki Page look like a regular writable file."""
 
+    __implements__ = IWriteFile
+    __used_for__ = IWikiPage
+    
     def __init__(self, context):
         self.context = context
 
     def write(self, data):
+        """See zope.app.interfaces.file.IWriteFile"""
         self.context.source = unicode(data)
 
 
 # Adapter for ISearchableText
 
-from zope.app.interfaces.index.text import ISearchableText
-
 class SearchableText:
+    """This adapter provides an API that allows the Wiki Pages to be indexed
+    by the Text Index.""" 
 
     __implements__ = ISearchableText
     __used_for__ = IWikiPage
@@ -149,3 +170,105 @@
 
     def getSearchableText(self):
         return [unicode(self.page.source)]
+
+
+# Component to fullfill mail subscriptions
+
+class MailSubscriptions:
+    """An adapter for WikiPages to provide an interface for collecting E-mails
+    for sending out change notices."""
+
+    __implements__ = IMailSubscriptions
+    __used_for__ = IWikiPage, IWiki
+
+    def __init__(self, context):
+        self.context = context
+        self._annotations = getAdapter(context, IAnnotations)
+        if not self._annotations.get(SubscriberKey):
+            self._annotations[SubscriberKey] = ()
+
+    def getSubscriptions(self):
+        "See zopeproducts.zwiki.interfaces.IMailSubscriptions"
+        return self._annotations[SubscriberKey]
+        
+    def addSubscriptions(self, emails):
+        "See zopeproducts.zwiki.interfaces.IMailSubscriptions"
+        subscribers = list(self._annotations[SubscriberKey])
+        for email in emails:
+            if email not in subscribers:
+                subscribers.append(email.strip())
+        self._annotations[SubscriberKey] = tuple(subscribers)
+                
+    def removeSubscriptions(self, emails):
+        "See zopeproducts.zwiki.interfaces.IMailSubscriptions"
+        subscribers = list(self._annotations[SubscriberKey])
+        for email in emails:
+            if email in subscribers:
+                subscribers.remove(email)
+        self._annotations[SubscriberKey] = tuple(subscribers)
+                
+
+
+class WikiMailer:
+    """Class to handle all outgoing mail."""
+
+    __implements__ = ISubscriber
+
+    def __init__(self, host="localhost", port="25"):
+        """Initialize the the object.""" 
+        self.host = host
+        self.port = port
+
+    def notify(self, event):
+        """See zope.app.interfaces.event.ISubscriber"""
+        if IWikiPage.isImplementedBy(event.object):
+            if IObjectAddedEvent.isImplementedBy(event):
+                self.handleAdded(event.object)
+
+            elif IObjectModifiedEvent.isImplementedBy(event):
+                self.handleModified(event.object)
+
+            elif IObjectRemovedEvent.isImplementedBy(event):
+                self.handleRemoved(event.object)
+
+    def handleAdded(self, object):
+        subject = 'Added: '+objectName(object)
+        emails = self.getAllSubscribers(object)
+        body = object.source
+        self.mail(emails, subject, body)        
+
+    def handleModified(self, object):
+        # XXX: Should have some nice diff code here.
+        # from diff import textdiff
+        subject = 'Modified: '+objectName(object)
+        emails = self.getAllSubscribers(object)
+        body = object.source
+        self.mail(emails, subject, body)
+
+    def handleRemoved(self, object):
+        subject = 'Removed: '+objectName(object)
+        emails = self.getAllSubscribers(object)
+        body = subject
+        self.mail(emails, subject, body)
+
+    def getAllSubscribers(self, object):
+        """Retrieves all email subscribers by looking into the local Wiki Page
+           and into the Wiki for the global subscriptions."""
+        emails = tuple(getAdapter(object,
+                                  IMailSubscriptions).getSubscriptions())
+        emails += tuple(getAdapter(getParent(object),
+                                   IMailSubscriptions).getSubscriptions())
+        return emails
+
+    def mail(self, emails, subject, body):
+        """Mail out the Wiki change message."""
+        if not emails:
+            return
+        msg = 'Subject: %s\n\n\n%s' %(subject, body)
+        server = smtplib.SMTP(self.host, self.port)
+        server.set_debuglevel(0)
+        server.sendmail('wiki@zope3.org', emails, msg)
+        server.quit()
+
+# Create a global mailer object.
+mailer = WikiMailer()