[Checkins] SVN: zc.notification/trunk/src/zc/notification/ Initial checkin. Simple user notification framework. Currently includes email notification. Somewhat unstable API (need to review for scalability).

Gary Poster gary at zope.com
Tue Aug 15 17:08:39 EDT 2006


Log message for revision 69540:
  Initial checkin.  Simple user notification framework.  Currently includes email notification.  Somewhat unstable API (need to review for scalability).
  

Changed:
  A   zc.notification/trunk/src/zc/notification/README.txt
  A   zc.notification/trunk/src/zc/notification/__init__.py
  A   zc.notification/trunk/src/zc/notification/browser/
  A   zc.notification/trunk/src/zc/notification/browser/__init__.py
  A   zc.notification/trunk/src/zc/notification/browser/configure.zcml
  A   zc.notification/trunk/src/zc/notification/browser/views.py
  A   zc.notification/trunk/src/zc/notification/configure.zcml
  A   zc.notification/trunk/src/zc/notification/email/
  A   zc.notification/trunk/src/zc/notification/email/README.txt
  A   zc.notification/trunk/src/zc/notification/email/TODO.txt
  A   zc.notification/trunk/src/zc/notification/email/__init__.py
  A   zc.notification/trunk/src/zc/notification/email/browser/
  A   zc.notification/trunk/src/zc/notification/email/browser/__init__.py
  A   zc.notification/trunk/src/zc/notification/email/browser/configure.zcml
  A   zc.notification/trunk/src/zc/notification/email/browser/views.py
  A   zc.notification/trunk/src/zc/notification/email/configure.zcml
  A   zc.notification/trunk/src/zc/notification/email/interfaces.py
  A   zc.notification/trunk/src/zc/notification/email/notifier.py
  A   zc.notification/trunk/src/zc/notification/email/tests.py
  A   zc.notification/trunk/src/zc/notification/email/view.py
  A   zc.notification/trunk/src/zc/notification/email/view.txt
  A   zc.notification/trunk/src/zc/notification/email/view_test.pt
  A   zc.notification/trunk/src/zc/notification/i18n.py
  A   zc.notification/trunk/src/zc/notification/interfaces.py
  A   zc.notification/trunk/src/zc/notification/notification.py
  A   zc.notification/trunk/src/zc/notification/requestless.pt
  A   zc.notification/trunk/src/zc/notification/requestless.py
  A   zc.notification/trunk/src/zc/notification/requestless.txt
  A   zc.notification/trunk/src/zc/notification/tests.py

-=-
Added: zc.notification/trunk/src/zc/notification/README.txt
===================================================================
--- zc.notification/trunk/src/zc/notification/README.txt	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/README.txt	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,297 @@
+====================
+Notification utility
+====================
+
+
+  >>> import zc.notification.interfaces
+  >>> import zc.notification.notification
+
+  >>> import zope.i18nmessageid
+  >>> _ = zope.i18nmessageid.MessageFactory("zc.notification.tests")
+
+The notification facility provides a single, very simple notification
+implementation.  This should be specifialized to provide custom
+behaviors as needed, but may be used directly.
+
+The constructor for the notification requires the name of the
+notification and a message::
+
+  >>> n = zc.notification.notification.Notification(
+  ...     name="my-notification-name",
+  ...     message=_(u"my-notification-message",
+  ...               default=u"This is a test notification."))
+
+Notification objects conform to the `INotification` interface::
+
+  >>> import zope.interface.verify
+
+  >>> zope.interface.verify.verifyObject(
+  ...     zc.notification.interfaces.INotification, n)
+  True
+
+Notification objects provide the information passed to the constructor
+as attributes::
+
+  >>> n.name
+  'my-notification-name'
+
+  >>> n.message
+  u'my-notification-message'
+
+There is also a `timestamp` attribute::
+
+  >>> type(n.timestamp)
+  <type 'datetime.datetime'>
+  >>> n.timestamp.tzinfo
+  <UTC>
+
+Notifications have an `applicablePrincipals()` method that takes a set
+of principal ids as an argument and returns a new set of principal ids
+that should be sent the notification.  The default implementation is
+"noisy": the argument set is returned::
+
+  >>> principal_ids = set(("user1", "user2"))
+  >>> sorted(n.applicablePrincipals(principal_ids))
+  ['user1', 'user2']
+
+Other implementations are available in the package: see the discussion near the
+end of this document.
+
+Notifications also have a `mapping` attribute and a `summary` attribute.  They
+are both optional, and our notification example has neither.
+
+  >>> n.summary # None
+  >>> n.mapping # None
+
+Summaries are intended to be single-line versions of the message--a headline.
+Delivering notifications must be able to accomodate empty summaries.  
+
+Mappings, if provided, are a dictionary.  Keys are strings.  Values are
+strings that can be substituted in the message when translated; message ids
+that should be translated and then substituted in the message when in it is
+translated; or other values that custom notification views can use to render
+the notification.
+
+The notification utility
+------------------------
+
+Let's create a fresh notification utility with no registrations::
+
+  >>> utility = zc.notification.notification.NotificationUtility()
+
+Sending a notification at this point should work just fine, but nobody
+will be notified::
+
+  >>> utility.notify(n)
+
+The utility implementation provided here implements an additional
+interface that allows configuring what notifications should be
+registered for a principal, as well as the principal's preferred
+notifier.  There are four methods (`getRegistrations()`,
+`setRegistrations()`, `getNotifierMethod()`, and
+`setNotifierMethod()`) which allow manipulation of the set of
+notifications each principal is registered to receive, and the means
+by which each principal will be notified..
+
+Let's register for a few notifications and check that we get the
+registrations we expect back.  When starting, there should be no
+registrations for a principal who has registered anything yet::
+
+  >>> utility.getRegistrations("user1")
+  set([])
+  >>> utility.getNotifierMethod("user1")
+  ''
+
+  >>> utility.setRegistrations("user1", [])
+  >>> utility.getRegistrations("user1")
+  set([])
+  >>> utility.setNotifierMethod("user1", "email")
+  >>> utility.getNotifierMethod("user1")
+  'email'
+
+  >>> utility.setRegistrations("user1",
+  ...     ["my-notification-name", "another-notification-name"])
+  >>> sorted(utility.getRegistrations("user1"))
+  ['another-notification-name', 'my-notification-name']
+  >>> sorted(utility.getNotificationSubscriptions("another-notification-name"))
+  ['user1']
+  >>> sorted(utility.getNotificationSubscriptions("my-notification-name"))
+  ['user1']
+
+  >>> utility.setRegistrations("user1", ["another-notification-name"])
+  >>> sorted(utility.getRegistrations("user1"))
+  ['another-notification-name']
+  >>> sorted(utility.getNotificationSubscriptions("another-notification-name"))
+  ['user1']
+  >>> sorted(utility.getNotificationSubscriptions("my-notification-name"))
+  []
+
+  >>> utility.getRegistrations("user2")
+  set([])
+  >>> utility.getNotifierMethod("user2")
+  ''
+
+  >>> utility.setRegistrations("user1", [])
+  >>> utility.getRegistrations("user2")
+  set([])
+
+  >>> utility.setNotifierMethod("user2", "smoke signals")
+  >>> utility.getNotifierMethod("user2")
+  'smoke signals'
+
+Let's add one of those registrations back so we can test the utility
+with registrations in place::
+
+  >>> utility.setRegistrations("user1", ["my-notification-name"])
+
+Sending the notification is a little more effective now::
+
+  >>> utility.notify(n)
+  my-notification-name
+  my-notification-message
+  user1 by 'email'
+
+Note that the delivery method was "email": This was determined by
+looking for a principal annotation specifying the preferred delivery
+method.  If the preferred method does not exist, a default delivery
+mechanism is used.
+
+The "user2" user has configured a delivery method that doesn't exist
+(presumably it used to), so let's configure the notification utility
+to send the notification to him::
+
+  >>> utility.setRegistrations("user1", [])
+  >>> utility.setRegistrations("user2", ["my-notification-name"])
+
+Since the "smoke signals" notifier isn't available, the default
+notifier is used instead (the default meaning name == '')::
+
+  >>> utility.notify(n)
+  my-notification-name
+  my-notification-message
+  user2 by ''
+
+If there is no annotation specifying the delivery method, as for
+"user3", the default mechanism is used::
+
+  >>> utility.setRegistrations("user2", [])
+  >>> utility.setRegistrations("user3", ["my-notification-name"])
+
+  >>> utility.notify(n)
+  my-notification-name
+  my-notification-message
+  user3 by ''
+
+
+Sending notifications
+---------------------
+
+Application code that needs to send a notification needs to create a
+notification object and pass it to the `zc.notification.notify()`
+function.  This function takes care of locating the notification
+utility and passing it to the utility's `notify()` method.
+
+  >>> import zope.component
+  >>> zope.component.provideUtility(
+  ...     utility, zc.notification.interfaces.INotificationUtility)
+
+  >>> zc.notification.notify(n)
+  my-notification-name
+  my-notification-message
+  user3 by ''
+
+Registering notifications
+-------------------------
+
+Notice that the implementation-specific notification utility interfaces define
+a source for the notifier methods and for the available notification
+subscriptions.  These are populated by default with registered utilities. 
+Register INotifier objects as utilities, and INotificationDefinition objects as
+utilities.  It is worth noting that the INotificationDefinition interface can
+be fulfilled with a class that directly provides the interface.
+
+Other `applicablePrincipals` implementations
+--------------------------------------------
+
+The notification module includes two other notification implementations.  One
+simply accepts an iterable of principal ids and intersects the principal ids
+given to the `applicablePrincipals` method with the original ids given.
+
+    >>> n = zc.notification.notification.PrincipalNotification(
+    ...     name="my-notification-name",
+    ...     message=_(u"my-notification-message",
+    ...               default=u"This is a test notification."),
+    ...     principal_ids=('user0', 'user1', 'user3'))
+    >>> sorted(
+    ...     n.applicablePrincipals(set(('user1', 'user2', 'user3', 'user4'))))
+    ['user1', 'user3']
+
+The other does a similar job, but it also checks group membership.  We need to
+set up a demo authentication utility to show this.
+
+    >>> import zope.app.security.interfaces
+    >>> import zope.security.interfaces
+    >>> class DemoPrincipal(object):
+    ...     def __init__(self, groups=(), is_group=False):
+    ...         self.groups = groups
+    ...         if is_group:
+    ...             zope.interface.directlyProvides(
+    ...                 self, zope.security.interfaces.IGroup)
+    ...
+    >>> principals = {
+    ... 'user1': DemoPrincipal(),
+    ... 'user2': DemoPrincipal(('group1', 'group3')),
+    ... 'user3': DemoPrincipal(('group2',)),
+    ... 'group1': DemoPrincipal(is_group=True),
+    ... 'group2': DemoPrincipal(('group3',), is_group=True),
+    ... 'group3': DemoPrincipal(is_group=True)}
+    >>> class DemoAuth(object):
+    ...     zope.interface.implements(
+    ...         zope.app.security.interfaces.IAuthentication)
+    ...     def getPrincipal(self, pid):
+    ...         return principals[pid]
+    ...
+    >>> auth = DemoAuth()
+    >>> zope.component.provideUtility(auth)
+
+    >>> n = zc.notification.notification.GroupAwarePrincipalNotification(
+    ...     name="my-notification-name",
+    ...     message=_(u"my-notification-message",
+    ...               default=u"This is a test notification."),
+    ...     principal_ids=('user1', 'group3'))
+    >>> sorted(
+    ...     n.applicablePrincipals(set(('user1', 'user2', 'user3'))))
+    ['user1', 'user2', 'user3']
+
+    >>> n = zc.notification.notification.GroupAwarePrincipalNotification(
+    ...     name="my-notification-name",
+    ...     message=_(u"my-notification-message",
+    ...               default=u"This is a test notification."),
+    ...     principal_ids=('group1',))
+    >>> sorted(
+    ...     n.applicablePrincipals(set(('user1', 'user2', 'user3'))))
+    ['user2']
+
+    >>> n = zc.notification.notification.GroupAwarePrincipalNotification(
+    ...     name="my-notification-name",
+    ...     message=_(u"my-notification-message",
+    ...               default=u"This is a test notification."),
+    ...     principal_ids=('user1',))
+    >>> sorted(
+    ...     n.applicablePrincipals(set(('user1', 'user2', 'user3'))))
+    ['user1']
+
+It also allows you to specify users who should not be included, even if they
+match a group.
+
+    >>> n = zc.notification.notification.GroupAwarePrincipalNotification(
+    ...     name="my-notification-name",
+    ...     message=_(u"my-notification-message",
+    ...               default=u"This is a test notification."),
+    ...     principal_ids=('user1', 'group3'),
+    ...     exclude_ids=('user2',))
+    >>> sorted(
+    ...     n.applicablePrincipals(set(('user1', 'user2', 'user3'))))
+    ['user1', 'user3']
+
+    


Property changes on: zc.notification/trunk/src/zc/notification/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.notification/trunk/src/zc/notification/__init__.py
===================================================================
--- zc.notification/trunk/src/zc/notification/__init__.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/__init__.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,32 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""Notification utilities.
+
+"""
+
+import zope.component
+
+import interfaces
+
+
+def notify(notification):
+    """Dispatch a notification.
+
+    This takes care of the dance to get the notification utility and
+    send the notification.
+
+    """
+    utility = zope.component.getUtility(interfaces.INotificationUtility)
+    utility.notify(notification)

Added: zc.notification/trunk/src/zc/notification/browser/__init__.py
===================================================================
--- zc.notification/trunk/src/zc/notification/browser/__init__.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/browser/__init__.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1 @@
+#

Added: zc.notification/trunk/src/zc/notification/browser/configure.zcml
===================================================================
--- zc.notification/trunk/src/zc/notification/browser/configure.zcml	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/browser/configure.zcml	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:browser="http://namespaces.zope.org/browser"
+   i18n_domain="zc.notification.browser"
+   >
+
+  <browser:page
+     for="zc.notification.interfaces.INotificationUtility"
+     name="preferences.html"
+     class=".views.PreferencesForm"
+     permission="zope.View"
+     />
+
+  <browser:menuItem
+     for="zc.notification.interfaces.INotificationUtility"
+     menu="zmi_views"
+     title="Preferences"
+     action="preferences.html"
+     permission="zope.View"
+     />
+
+</configure>

Added: zc.notification/trunk/src/zc/notification/browser/views.py
===================================================================
--- zc.notification/trunk/src/zc/notification/browser/views.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/browser/views.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,52 @@
+import zope.interface
+import zope.component
+import zope.schema
+import zope.formlib.form
+
+from zope.app.principalannotation.interfaces import IPrincipalAnnotationUtility
+
+import zc.notification.interfaces
+
+from zc.notification.i18n import _
+
+class PreferencesForm(zope.formlib.form.PageForm):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+        self.utility = context
+
+    form_fields = zope.formlib.form.FormFields(
+        zc.notification.interfaces.INotificationSubscriptions,
+        zc.notification.interfaces.IPreferredNotifierMethod)
+
+    # subclasses can override/extend these two methods
+
+    def collect_data(self):
+        principal_id = self.request.principal.id
+        data = {}
+        data[u'notifications'] = self.utility.getRegistrations(principal_id)
+        data[u'method'] = self.utility.getNotifierMethod(principal_id)
+        return data
+
+    def apply_data(self, data):
+        principal_id = self.request.principal.id
+        notifications = data.get(u'notifications', [])
+        method = data.get(u'method', "")
+        self.utility.setRegistrations(principal_id, notifications)
+        self.utility.setNotifierMethod(principal_id, method)
+
+    # but shouldn't need to override these
+
+    def setUpWidgets(self, ignore_request=False):
+        self.adapters = {}
+        self.widgets = zope.formlib.form.setUpWidgets(
+            self.form_fields, self.prefix, self.context, self.request,
+            form=self, adapters=self.adapters,
+            ignore_request=ignore_request, data=self.collect_data())
+
+    @zope.formlib.form.action(
+        _("Apply"), condition=zope.formlib.form.haveInputWidgets)
+    def handle_apply(self, action, data):
+        self.apply_data(data)
+        self.status = _(u'Preferences Applied')

Added: zc.notification/trunk/src/zc/notification/configure.zcml
===================================================================
--- zc.notification/trunk/src/zc/notification/configure.zcml	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/configure.zcml	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:browser="http://namespaces.zope.org/browser"
+   i18n_domain="zc.notification"
+   >
+
+  <localUtility class=".notification.NotificationUtility">
+    <require
+        permission="zope.View"
+        interface="zc.notification.interfaces.INotificationUtility"
+        />
+    <require
+        permission="zope.ManageContent"
+        set_schema="zc.notification.interfaces.INotificationUtility" />
+    <require
+        permission="zope.View"
+        interface="zc.notification.interfaces.INotificationUtilityConfiguration"
+        />
+ </localUtility>
+
+  <browser:addMenuItem
+      title="Notification Utility"
+      description="A Simple Notification Utility"
+      class=".notification.NotificationUtility"
+      permission="zope.ManageContent"
+      />
+
+  <utility
+     provides="zope.schema.interfaces.IVocabularyFactory"
+     name="zc.notification.notifications"
+     component=".notification.getNotificationNames"
+     />
+
+  <utility
+     provides="zope.schema.interfaces.IVocabularyFactory"
+     name="zc.notification.notifiers"
+     component=".notification.getNotifierMethods"
+     />
+
+  <include package=".email" />
+
+  <include package=".browser" />
+
+</configure>

Added: zc.notification/trunk/src/zc/notification/email/README.txt
===================================================================
--- zc.notification/trunk/src/zc/notification/email/README.txt	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/README.txt	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,135 @@
+==============================
+Sending notifications by email
+==============================
+
+The email notifier sends email for each notification it handles.
+Email is sent using an `IMailDelivery` object (as defined in the
+`zope.app.mail` package).  We'll need to register one::
+
+  >>> import zope.component
+  >>> import zope.interface
+  >>> import zope.sendmail.interfaces
+
+  >>> class MailDelivery(object):
+  ...
+  ...     zope.interface.implements(
+  ...         zope.sendmail.interfaces.IMailDelivery)
+  ...
+  ...     messages = []
+  ...
+  ...     def send(self, fromaddr, toaddrs, message):
+  ...         self.messages.append((fromaddr, toaddrs, message))
+
+  >>> mailer = MailDelivery()
+
+  >>> zope.component.provideUtility(mailer)
+
+We're also going to need to use notification interfaces::
+
+  >>> import zc.notification.interfaces
+  >>> import zc.notification.email.interfaces
+
+The email notifier will need a way to look up email addresses for
+users.  There is an implementation that looks in the principal
+annotations, but let's use something even simpler to show that the
+notifier itself is working::
+
+  >>> class AddressLookup(object):
+  ...
+  ...    zope.interface.implements(
+  ...        zc.notification.email.interfaces.IEmailLookupUtility)
+  ...
+  ...    addresses = {
+  ...        "user1": "user1 at example.net",
+  ...        }
+  ...
+  ...    def getAddress(self, principal_id, annotations):
+  ...        return self.addresses.get(principal_id)
+  ...
+
+  >>> lookup = AddressLookup()
+
+  >>> zope.component.provideUtility(lookup)
+
+When the notifier generates an email, it will adapt the notification
+and the principal to the `IEmailView` interface.  This will produce an
+adapter that is used to generate the email itself, except for the
+"To:" and "From:" headers (which are generated by the notifier
+itself).
+
+Let's create a simple email view and register that as an adapter::
+
+  >>> class SampleView(object):
+  ...
+  ...     zope.interface.implements(
+  ...         zc.notification.email.interfaces.IEmailView)
+  ...
+  ...     zope.component.adapts(
+  ...         zc.notification.interfaces.INotification,
+  ...         zope.app.security.interfaces.IPrincipal)
+  ...
+  ...     def __init__(self, notification, principal):
+  ...         self.notification = notification
+  ...         self.principal = principal
+  ...
+  ...     def render(self):
+  ...         return ("Subject: notification email\r\n"
+  ...                 "\r\n"
+  ...                 + self.notification.message.encode("ascii")
+  ...                 + "\r\n")
+
+  >>> zope.component.provideAdapter(SampleView)
+
+Now that the operating environment has been prepared, we can create
+and use the notifier::
+
+  >>> import zc.notification.email.notifier
+
+  >>> notifier = zc.notification.email.notifier.EmailNotifier()
+
+The `fromAddress` attribute must be initialized before the utility can
+be used::
+
+  >>> notifier.fromAddress = "email-notifier at example.net"
+
+We can now synthesize a notification to send::
+
+  >>> import zope.i18nmessageid
+  >>> _ = zope.i18nmessageid.MessageFactory("zc.notification.tests")
+
+  >>> import zc.notification.notification
+
+  >>> n = zc.notification.notification.Notification(
+  ...     name="test-notification",
+  ...     message=_(u"test-notification-message",
+  ...               default=u"This is a test notification."))
+
+To use the notifier, we'll need the annotations for the target user::
+
+  >>> annotations = zope.component.getUtility(
+  ...     zope.app.principalannotation.interfaces.IPrincipalAnnotationUtility)
+  >>> user1 = annotations.getAnnotationsById("user1")
+
+As for all notifiers, we can just use the `send()` method::
+
+  >>> notifier.send(n, "user1", user1)
+
+Since our test mailer collects information from the calls to its
+`send()` method, we can examine what was done::
+
+  >>> len(mailer.messages)
+  1
+  >>> sent = mailer.messages[0]
+  >>> sent[0]
+  'email-notifier at example.net'
+  >>> sent[1]
+  ['user1 at example.net']
+
+  >>> text = sent[2].replace("\r\n", "\n")
+  >>> print text
+  From: email-notifier at example.net
+  To: user1 at example.net
+  Subject: notification email
+  <BLANKLINE>
+  test-notification-message
+  <BLANKLINE>


Property changes on: zc.notification/trunk/src/zc/notification/email/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.notification/trunk/src/zc/notification/email/TODO.txt
===================================================================
--- zc.notification/trunk/src/zc/notification/email/TODO.txt	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/TODO.txt	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,9 @@
+Things that should be done
+--------------------------
+
+* Add and edit forms to allow management of the notifier.
+
+* Asynchronous version of the notifier (or just rely on the mail
+  delivery agent?).
+
+* More tests.


Property changes on: zc.notification/trunk/src/zc/notification/email/TODO.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.notification/trunk/src/zc/notification/email/__init__.py
===================================================================
--- zc.notification/trunk/src/zc/notification/email/__init__.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/__init__.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1 @@
+# This directory is a Python package.

Added: zc.notification/trunk/src/zc/notification/email/browser/__init__.py
===================================================================
--- zc.notification/trunk/src/zc/notification/email/browser/__init__.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/browser/__init__.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1 @@
+#

Added: zc.notification/trunk/src/zc/notification/email/browser/configure.zcml
===================================================================
--- zc.notification/trunk/src/zc/notification/email/browser/configure.zcml	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/browser/configure.zcml	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:browser="http://namespaces.zope.org/browser"
+   i18n_domain="zc.notification.email.browser"
+   >
+
+  <browser:page
+     for="zc.notification.email.interfaces.IEmailNotifier"
+     name="edit.html"
+     class=".views.EditNotifierForm"
+     permission="zope.ManageSite"
+     />
+
+  <browser:menuItem
+     for="zc.notification.email.interfaces.IEmailNotifier"
+     menu="zmi_views"
+     title="Edit"
+     action="edit.html"
+     permission="zope.ManageSite"
+     />
+
+</configure>

Added: zc.notification/trunk/src/zc/notification/email/browser/views.py
===================================================================
--- zc.notification/trunk/src/zc/notification/email/browser/views.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/browser/views.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,8 @@
+import zope.formlib.form
+
+import zc.notification.email.interfaces
+
+class EditNotifierForm(zope.formlib.form.EditForm):
+
+    form_fields = zope.formlib.form.FormFields(
+        zc.notification.email.interfaces.IEmailNotifier)

Added: zc.notification/trunk/src/zc/notification/email/configure.zcml
===================================================================
--- zc.notification/trunk/src/zc/notification/email/configure.zcml	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/configure.zcml	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure
+   xmlns="http://namespaces.zope.org/zope"
+   xmlns:browser="http://namespaces.zope.org/browser"
+   i18n_domain="zc.notification.email"
+   >
+
+  <localUtility class=".notifier.EmailNotifier">
+    <require
+        permission="zope.View"
+        interface="zc.notification.email.interfaces.IEmailNotifier"
+        />
+    <require
+        permission="zope.ManageContent"
+        set_schema="zc.notification.email.interfaces.IEmailNotifier" />
+  </localUtility>
+
+  <browser:addMenuItem
+      title="Email Notifier"
+      description="An email notifier for the notification utility."
+      class=".notifier.EmailNotifier"
+      permission="zope.ManageContent"
+      />
+
+  <adapter factory=".view.EmailView" />
+
+  <include package=".browser" />
+
+</configure>

Added: zc.notification/trunk/src/zc/notification/email/interfaces.py
===================================================================
--- zc.notification/trunk/src/zc/notification/email/interfaces.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/interfaces.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,82 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""Interfaces for the email support for the notification utility.
+
+"""
+__docformat__ = "reStructuredText"
+
+import zope.interface
+import zope.schema
+
+import zc.notification.interfaces
+
+from zc.notification.i18n import _
+
+
+class IEmailNotifier(zc.notification.interfaces.INotifier):
+    """Notifier that sends email.
+
+    """
+
+    fromAddress = zope.schema.ASCIILine(
+        title=_(u"From address"),
+        description=_(u"Email address used for the From: header."),
+        required=True,
+        )
+
+    fromName = zope.schema.ASCIILine(
+        title=_(u"From name"),
+        description=_(u"Name to use in the From: header."),
+        required=False,
+        )
+
+
+class IEmailLookupUtility(zope.interface.Interface):
+    """Utility that can retrieve an email address for a principal.
+
+    """
+
+    def getAddress(principal_id, annotations):
+        """Return an email address as a string, or None.
+
+        `principal_id` is a principal id.
+
+        `annotations` is the principal annotations corresponding to
+        `principal_id`.
+
+        """
+
+
+class IEmailView(zope.interface.Interface):
+    """View that generates an email.
+
+    Email views are adaptations of a notification and a principal.
+    The principal is the recipient of the email, not necessarily the
+    principal who caused the notification to be sent.
+
+    """
+
+    def render():
+        """Return the rendered email.
+
+        The generated email should include the RFC-2822 headers
+        (except the To: and From: headers), and the blank line that
+        follows them, and the email payload.
+
+        The return value should be an 8-bit string; it will not be
+        further encoded before being passed to the `IMailDelivery`
+        utility.
+
+        """

Added: zc.notification/trunk/src/zc/notification/email/notifier.py
===================================================================
--- zc.notification/trunk/src/zc/notification/email/notifier.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/notifier.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,102 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""Basic utility to convert notifications into email.
+
+"""
+__docformat__ = "reStructuredText"
+
+import logging
+
+import persistent
+
+import zope.component
+
+import zope.app.container.interfaces
+import zope.app.container.contained
+import zope.sendmail.interfaces
+
+from zope.app import zapi
+
+import zc.notification.email.interfaces
+
+
+_log = logging.getLogger(__name__)
+
+
+class EmailNotifier(zope.app.container.contained.Contained,
+                    persistent.Persistent):
+    """Send emails for notifications.
+
+    """
+
+    zope.interface.implements(
+        zope.app.container.interfaces.IContained,
+        zc.notification.email.interfaces.IEmailNotifier)
+
+    fromAddress = None
+    fromName = None
+
+    def send(self, notification, principal_id, annotations):
+        address = self.email_lookup.getAddress(principal_id, annotations)
+        if address:
+            # send some email
+            principal = zapi.principals().getPrincipal(principal_id)
+            view = zope.component.getMultiAdapter(
+                (notification, principal),
+                zc.notification.email.interfaces.IEmailView)
+            if self.fromName:
+                response = ("From: %s <%s>\r\n"
+                            % (self.fromName, self.fromAddress))
+            else:
+                response = "From: %s\r\n" % self.fromAddress
+            response += "To: %s\r\n" % address
+            response += view.render()
+            self.mailer.send(self.fromAddress, [address], response)
+        else:
+            _log.info("No email address for principal id %r." % principal_id)
+
+    _v_email_lookup_utility = None
+    _v_mailer = None
+
+    @property
+    def email_lookup(self):
+        if self._v_email_lookup_utility is None:
+            utility = zope.component.getUtility(
+                zc.notification.email.interfaces.IEmailLookupUtility)
+            self._v_email_lookup_utility = utility
+        return self._v_email_lookup_utility
+
+    @property
+    def mailer(self):
+        if self._v_mailer is None:
+            utility = zope.component.getUtility(
+                zope.sendmail.interfaces.IMailDelivery)
+            self._v_mailer = utility
+        return self._v_mailer
+
+
+EMAIL_ADDRESS_ANNOTATION_KEY = "zc.notification.email.email_address"
+
+class EmailLookupUtility(object):
+    """Look up email address for principals.
+
+    The email address is stored as a principal annotation.
+
+    """
+    zope.interface.implements(
+        zc.notification.email.interfaces.IEmailLookupUtility)
+
+    def getAddress(self, principal_id, annotations):
+        return annotations.get(EMAIL_ADDRESS_ANNOTATION_KEY)

Added: zc.notification/trunk/src/zc/notification/email/tests.py
===================================================================
--- zc.notification/trunk/src/zc/notification/email/tests.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/tests.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,68 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""Test harness for zc.notification.email.
+
+"""
+__docformat__ = "reStructuredText"
+
+import unittest
+from zope.testing import doctest
+
+import zope.component
+import zope.interface
+
+import zope.app.security.interfaces
+import zope.app.testing.placelesssetup
+
+import zc.notification.interfaces
+import zc.notification.tests
+
+
+class Authentication(object):
+
+    zope.interface.implements(
+        zope.app.security.interfaces.IAuthentication)
+
+    def getPrincipal(self, id):
+        return Principal(id)
+
+
+class Principal(object):
+
+    zope.interface.implements(
+        zope.app.security.interfaces.IPrincipal)
+
+    def __init__(self, id):
+        self.id = id
+
+
+def setUp(test):
+    zope.app.testing.placelesssetup.setUp(test)
+    zope.component.provideUtility(Authentication())
+    zope.component.provideUtility(
+        zc.notification.tests.PrincipalAnnotationUtility())
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite(
+            "README.txt",
+            setUp=setUp,
+            tearDown=zope.app.testing.placelesssetup.tearDown,
+            optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE),
+        doctest.DocFileSuite(
+            "view.txt",
+            setUp=zope.app.testing.placelesssetup.setUp,
+            tearDown=zope.app.testing.placelesssetup.tearDown),
+        ))

Added: zc.notification/trunk/src/zc/notification/email/view.py
===================================================================
--- zc.notification/trunk/src/zc/notification/email/view.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/view.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,83 @@
+from email.MIMENonMultipart import MIMENonMultipart
+from email.MIMEMultipart import MIMEMultipart
+from email import Charset
+from email.Header import Header
+
+from zope import i18n, component, interface
+import zope.app.security.interfaces
+
+import zc.notification.interfaces
+import zc.notification.email.notifier
+
+def translate(msgid, domain=None, mapping=None, context=None,
+              target_language=None, default=None):
+    if mapping is not None:
+        msgid = zope.i18nmessageid.Message(msgid, mapping=mapping)
+    return i18n.translate(
+        msgid, domain, mapping, context, target_language, default)
+
+UTF8 = Charset.Charset('utf-8')
+UTF8.body_encoding = Charset.QP
+
+class UTF8MIMEText(MIMENonMultipart):
+    def __init__(self, _text, _subtype='plain'):
+        MIMENonMultipart.__init__(self, 'text', _subtype, charset='utf-8')
+        self.set_payload(_text, UTF8)
+
+class EmailView(object):
+
+    interface.implements(
+        zc.notification.email.interfaces.IEmailView)
+
+    component.adapts(
+        zc.notification.interfaces.INotification,
+        zope.app.security.interfaces.IPrincipal)
+
+    renderHTML = None
+
+    def __init__(self, context, principal):
+        self.context = context
+        self.principal = principal
+
+    def render(self):
+        self.mapping = self.context.mapping
+        if self.mapping is not None:
+            res = {}
+            for k, v in self.mapping.items():
+                if isinstance(v, basestring):
+                    if isinstance(v, zope.i18nmessageid.Message):
+                        v = i18n.translate(v, context=self.principal)
+                    res[k] = v
+            self.mapping = res
+        msg = translate(
+            self.context.message, mapping=self.mapping,
+            context=self.principal)
+        if self.context.summary is not None:
+            summary = translate(
+                self.context.summary, mapping=self.mapping,
+                context=self.principal)
+        else:
+            parts = msg.split('\n', 1)
+            if len(parts) == 1:
+                summary = msg
+                rest = ''
+            else:
+                summary, rest = parts
+            if len(summary) > 53:
+                summary = summary[:50] + "..."
+            else:
+                msg = rest.strip()
+
+        body = UTF8MIMEText(msg.encode("utf8"))
+        if self.renderHTML is None:
+            body['Subject'] = Header(summary.encode("utf8"), 'utf-8')
+            return body.as_string()
+        else:
+            self.message = msg
+            self.summary = summary
+            html = UTF8MIMEText(self.renderHTML(), 'html')
+            multi = MIMEMultipart('alternative', None, (body, html))
+            multi['Subject'] = Header(summary, UTF8)
+            multi.epilogue = ''
+            return multi.as_string()
+

Added: zc.notification/trunk/src/zc/notification/email/view.txt
===================================================================
--- zc.notification/trunk/src/zc/notification/email/view.txt	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/view.txt	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,206 @@
+The email view module's EmailView class is a reasonable default notification
+email, as well as a reasonable start for custom notification emails.
+
+It simply takes a notification and a principal, translates the mapping if any;
+translates the summary, if any, with the optional mapping; and translates the
+message, with the optional mapping.  It then generates either a simple text
+email, or an html/text alternative email if a `html` attribute is not None.
+The html attribute is called without arguments to render the html version
+of the email.  It is expected that the requestless template found in the
+zc.notification package will be a good implementation for the html, though
+other approaches can be used.
+
+We need to set up the translation framework. We'll set up the standard
+negotiator, and we'll set up the fallback-domain factory, which provides the test
+language for all domains::
+
+    >>> from zope import interface, component
+    >>> import zope.i18n.interfaces
+    >>> import zope.i18n.negotiator
+
+    >>> component.provideUtility(zope.i18n.negotiator.Negotiator())
+
+    >>> from zope.i18n.testmessagecatalog import TestMessageFallbackDomain
+    >>> component.provideUtility(TestMessageFallbackDomain)
+
+Now we'll set up an adapter from IPrincipal to IUserPreferredLanguages that
+returns 'test' as its language.
+
+    >>> import zope.security.interfaces
+    >>> class DemoLanguagePrefAdapter(object):
+    ...     interface.implements(zope.i18n.interfaces.IUserPreferredLanguages)
+    ...     component.adapts(zope.security.interfaces.IPrincipal)
+    ...     def __init__(self, context):
+    ...         self.context = context
+    ...     def getPreferredLanguages(self):
+    ...         return ('test',)
+    ...
+    >>> component.provideAdapter(DemoLanguagePrefAdapter)
+
+Now we'll make a principal, make a notification, instantiate the view, and
+render it.
+
+    >>> class DemoPrincipal(object):
+    ...     interface.implements(zope.security.interfaces.IPrincipal)
+    ...
+    >>> principal = DemoPrincipal()
+    >>> import zc.notification.notification
+    >>> import zope.i18nmessageid
+    >>> _ = zope.i18nmessageid.MessageFactory("view.tests")
+    >>> n = zc.notification.notification.Notification(
+    ...     name="my-notification-name",
+    ...     message=_("my-message", default=u"test notification."))
+    >>> import zc.notification.email.view
+    >>> print zc.notification.email.view.EmailView(n, principal).render()
+    Content-Type: text/plain; charset="utf-8"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: quoted-printable
+    Subject: =?utf-8?b?W1t2aWV3LnRlc3RzXVtteS1tZXNzYWdlICh0ZXN0IG5vdGlmaWNhdGlvbi4p?=
+     =?utf-8?b?XV0=?=
+    <BLANKLINE>
+    <BLANKLINE>
+
+Notice that the email only has a subject: the message is short enough to fit on
+one line, and there was no summary.  Let's look at a richer example, with
+a slightly longer single-line message.
+
+    >>> n = zc.notification.notification.Notification(
+    ...     name="my-notification-name",
+    ...     message=_("my-message-2",
+    ...               default=u"test notification number two."))
+    >>> print zc.notification.email.view.EmailView(n, principal).render()
+    Content-Type: text/plain; charset="utf-8"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: quoted-printable
+    Subject: =?utf-8?q?=5B=5Bview=2Etests=5D=5Bmy-message-2_=28test_notification_numb?=
+     =?utf-8?b?Li4u?=
+    <BLANKLINE>
+    [[view.tests][my-message-2 (test notification number two.)]]
+
+Notice that the subject is truncated, so the body contains the full message.
+Here's a multiline one with a short first line.
+
+    >>> n = zc.notification.notification.Notification(
+    ...     name="my-notification-name",
+    ...     message=_("my-message-3",
+    ...               default=u"test three.\n"
+    ...                        "It spans lines.\nIt is cool."))
+    >>> print zc.notification.email.view.EmailView(n, principal).render()
+    Content-Type: text/plain; charset="utf-8"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: quoted-printable
+    Subject: =?utf-8?b?W1t2aWV3LnRlc3RzXVtteS1tZXNzYWdlLTMgKHRlc3QgdGhyZWUu?=
+    <BLANKLINE>
+    It spans lines.
+    It is cool.)]]
+
+The first line is not repeated in the body.
+
+Now we'll turn to a rich notification: one with a summary, a mapping, and a
+multiline message.
+
+    >>> n = zc.notification.notification.Notification(
+    ...     name="my-notification-name",
+    ...     summary=_("summary-4", "A summary with ${foo} interpolation"),
+    ...     message=_("my-message-4",
+    ...               default=u"test four.\n"
+    ...                        "It spans ${number} lines.\n"
+    ...                        "It is ${adjective}."),
+    ...     mapping={'foo': _('foo-summary', 'bar'),
+    ...              'number': _('number-three', 'three'),
+    ...              'adjective': _('adjective-super', 'super')})
+    >>> print zc.notification.email.view.EmailView(n, principal).render()
+    Content-Type: text/plain; charset="utf-8"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: quoted-printable
+    Subject: =?utf-8?q?=5B=5Bview=2Etests=5D=5Bsummary-4_=28A_summary_with_=5B=5Bview?=
+     =?utf-8?b?LnRlc3RzXVtmb28tc3VtbWFyeSAoYmFyKV1dIGludGVycG9sYXRpb24pXV0=?=
+    <BLANKLINE>
+    [[view.tests][my-message-4 (test four.
+    It spans [[view.tests][number-three (three)]] lines.
+    It is [[view.tests][adjective-super (super)]].)]]
+
+Notice (as best you can) that both the summary and the message can be
+interpolated with mapping translations.
+
+The view also supports html mail as an alternate rendering.  To use this, you
+need to provide a "renderHTML" callable; the requestless template is perfect
+for this use, but the view doesn't care as long as it is callable without
+arguments.
+
+Notification mappings can include non-string values for rich uses like this.
+We give a useless example below.
+
+    >>> from zope.traversing.adapters import DefaultTraversable
+    >>> component.provideAdapter(
+    ...     DefaultTraversable, adapts=(None,))
+    >>> import zc.notification.requestless
+    >>> class RichEmailView(zc.notification.email.view.EmailView):
+    ...     renderHTML = zc.notification.requestless.PageTemplateFile(
+    ...         'view_test.pt')
+    ...
+    >>> class DemoObject(object):
+    ...     pass
+    ...
+    >>> o = DemoObject()
+    >>> o.attr = 'you could adapt my object!'
+    >>> n = zc.notification.notification.Notification(
+    ...     name="my-notification-name",
+    ...     summary=_("summary-5", "alternate plain and html renderings"),
+    ...     message=_("my-message-5",
+    ...               default=u"Important: ${message}\n"
+    ...                        "See ${url}"),
+    ...     mapping={'message': _('mapping-message', 'Fix this!'),
+    ...              'object': o,
+    ...              'url': 'http://example.com/foo.html'})
+    >>> view = RichEmailView(n, principal) 
+    >>> print view.render() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Content-Type: multipart/alternative;
+        boundary="..."
+    MIME-Version: 1.0
+    Subject: [[view.tests][summary-5 (alternate plain and html renderings)]]
+    <BLANKLINE>
+    --...
+    Content-Type: text/plain; charset="utf-8"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: quoted-printable
+    <BLANKLINE>
+    [[view.tests][my-message-5 (Important: [[view.tests][mapping-message (Fix t=
+    his!)]]
+    See http://example.com/foo.html)]]
+    --...
+    Content-Type: text/html; charset="utf-8"
+    MIME-Version: 1.0
+    Content-Transfer-Encoding: quoted-printable
+    <BLANKLINE>
+    <html>
+      <head></head>
+      <body>
+        <h1>[[view.tests][mapping-message (Fix this!)]]</h1>
+        <p> See <a href=3D"http://example.com/foo.html">you could adapt my obje=
+    ct!</a>
+        </p>
+      </body>
+    </html>
+    <BLANKLINE>
+    <BLANKLINE>
+    --...--
+    <BLANKLINE>
+
+In addition to the `context` and `principal` attributes on the view, the
+callable has access to three other attributes on the view: `mapping`, which
+contains translated unicode message ids and untranslated normal strings;
+`summary`, the unicode interpolated summary; and `message`, the unicode
+interpolated message.  These are set during render, so they are set now.
+
+    >>> view.mapping # doctest: +NORMALIZE_WHITESPACE
+    {'url': 'http://example.com/foo.html',
+     'message': u'[[view.tests][mapping-message (Fix this!)]]'}
+    >>> view.summary
+    u'[[view.tests][summary-5 (alternate plain and html renderings)]]'
+    >>> view.message # doctest: +NORMALIZE_WHITESPACE
+    u'[[view.tests][my-message-5
+                    (Important: [[view.tests][mapping-message
+                                              (Fix this!)]]\nSee
+                    http://example.com/foo.html)]]'
+    


Property changes on: zc.notification/trunk/src/zc/notification/email/view.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.notification/trunk/src/zc/notification/email/view_test.pt
===================================================================
--- zc.notification/trunk/src/zc/notification/email/view_test.pt	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/email/view_test.pt	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,10 @@
+<html>
+  <head></head>
+  <body>
+    <h1 tal:content="view/mapping/message">Message</h1>
+    <p> See <a href="" tal:attributes="href context/mapping/url"
+      tal:content="context/mapping/object/attr">Attr</a>
+    </p>
+  </body>
+</html>
+

Added: zc.notification/trunk/src/zc/notification/i18n.py
===================================================================
--- zc.notification/trunk/src/zc/notification/i18n.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/i18n.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,32 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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
+#
+##############################################################################
+"""I18N support for zc.notification.
+
+This defines a `MessageFactory` for the I18N domain for the
+zc.notification package.  This is normally used with the import::
+
+  from zc.notification.i18n import MessageFactory as _
+
+The factory is then used normally.  Two examples::
+
+  text = _('some internationalized text')
+  text = _('helpful-descriptive-message-id', 'default text')
+"""
+__docformat__ = "reStructuredText"
+
+
+from zope import i18nmessageid
+
+MessageFactory = _ = i18nmessageid.MessageFactory("zc.notification")

Added: zc.notification/trunk/src/zc/notification/interfaces.py
===================================================================
--- zc.notification/trunk/src/zc/notification/interfaces.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/interfaces.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,180 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""Interfaces for the notification utility.
+
+"""
+__docformat__ = "reStructuredText"
+
+import zope.interface
+import zope.schema
+
+from zc.notification.i18n import _
+
+
+class INotificationUtility(zope.interface.Interface):
+    """Notification utility.
+
+    """
+
+    def notify(notification):
+        """Process a notification.
+
+        `notification` must implement `INotification`.
+
+        """
+
+
+class INotification(zope.interface.Interface):
+    """Individual notification.
+
+    """
+
+    name = zope.schema.TextLine(
+        title=_("Name"),
+        description=_(u"Name of the notification"),
+        required=True,
+        )
+
+    # This should be a zope.i18nmessageid.Message.
+    summary = zope.schema.TextLine(
+        title=_("Summary"),
+        description=_("Optional one-line message summary."),
+        required=False)
+
+    # This should be a zope.i18nmessageid.Message.
+    message = zope.interface.Attribute(
+        "Message associated with this notification."
+        )
+
+    mapping = zope.interface.Attribute(
+        """A dictionary of name: i18nmessageid.Message to be translated and 
+        then included in message translation, or None""")
+
+    timestamp = zope.schema.Datetime(
+        title=_(u"Time"),
+        description=_(u"Time that the notification was generated."
+                      u" This is given in UTC with the tzinfo set."),
+        required=True,
+        )
+
+    def applicablePrincipals(principal_ids):
+        """Return the set of principal ids this notification should be sent to.
+
+        `principal_ids` is a set of principal ids.
+
+        """
+
+
+# User interfaces will want some way of describing notifications; each
+# should be described using an `INotificationDefinition`.  These can
+# be utilities looked up by name, where the name matches that of the
+# notifications.
+
+class INotificationDefinition(zope.interface.Interface):
+    """Information about a type of notification.
+
+    This should be used to generate user-interfaces, which may include
+    representations of the individual notifications.
+
+    """
+
+    name = zope.schema.TextLine(
+        title=_("Name"),
+        description=_(u"Name of the notification"),
+        required=True,
+        )
+
+    # This should be a zope.i18nmessageid.Message.
+    title = zope.interface.Attribute(
+        "Short human-consumable name of the notification."
+        )
+
+    # This should be a zope.i18nmessageid.Message.
+    description = zope.interface.Attribute(
+        "Human-consumable description of the notification."
+        " This should include what triggers the notification."
+        )
+
+
+# The following interfaces are defined for use by the reference
+# implementation; these may not be used by alternate implementations.
+
+class INotifier(zope.interface.Interface):
+    """Object responsible for sending a notification to principals.
+
+    """
+
+    def send(notification, principal_id, annotations):
+        """Send one notification to one principal.
+
+        `notification` must implement `INotification`.
+
+        `principal_id` is a principal id.
+
+        `annotations` is the annotations object for the principal.
+
+        """
+
+
+class INotificationUtilityConfiguration(zope.interface.Interface):
+    """Configuration interface for the notification utility.
+
+    """
+
+    def setNotifierMethod(principal_id, method):
+        """Set the preferred notifier method for `principal_id`.
+
+        """
+
+    def getNotifierMethod(principal_id):
+        """Return the preferred notifier method for `principal_id`.
+
+        """
+
+    def setRegistrations(principal_id, names):
+        """Replace the existing registrations for `principal_id`.
+
+        Existing registrations are removed if not included in `names`,
+        and all registrations from `names` are added if not already
+        present.
+
+        """
+
+    def getRegistrations(principal_id):
+        """Return the current set of registrations for `principal_id`.
+
+        """
+
+    def getNotificationSubscriptions(notification_name):
+        """Return the current set of subscribers for `notification_name`.
+
+        """
+
+class INotificationSubscriptions(zope.interface.Interface):
+
+    notifications = zope.schema.Set(
+        title=_(u'Notifications'),
+        description=_(u'Available Notifications'),
+        required=True,
+        value_type=zope.schema.Choice(
+            vocabulary='zc.notification.notifications'))
+
+
+class IPreferredNotifierMethod(zope.interface.Interface):
+
+    method = zope.schema.Choice(
+        title=_(u'Notifier'),
+        description=_(u'Preferred means of being notified'),
+        vocabulary='zc.notification.notifiers')

Added: zc.notification/trunk/src/zc/notification/notification.py
===================================================================
--- zc.notification/trunk/src/zc/notification/notification.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/notification.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,223 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""Simple reference implementation of the notification tools.
+
+"""
+__docformat__ = "reStructuredText"
+
+import datetime
+
+import BTrees.OOBTree
+import pytz
+import persistent
+
+import zope.component
+import zope.interface
+import zope.schema.vocabulary
+import zope.schema.interfaces
+import zope.app.container.interfaces
+import zope.app.container.contained
+
+from zope.app.principalannotation.interfaces import IPrincipalAnnotationUtility
+
+import zc.notification.interfaces
+
+
+def getNotificationNames(context):
+    N = zope.component.getUtilitiesFor(
+        zc.notification.interfaces.INotificationDefinition,
+        context=context)
+    return zope.schema.vocabulary.SimpleVocabulary.fromValues(
+        [name for (name, utility) in N])
+zope.interface.directlyProvides(
+    getNotificationNames, zope.schema.interfaces.IVocabularyFactory)
+
+def getNotifierMethods(context):
+    N = zope.component.getUtilitiesFor(
+        zc.notification.interfaces.INotifier,
+        context=context)
+    return zope.schema.vocabulary.SimpleVocabulary.fromValues(
+        [name for (name, utility) in N])
+zope.interface.directlyProvides(
+    getNotifierMethods, zope.schema.interfaces.IVocabularyFactory)
+
+
+class Notification(object):
+    """Really basic notification object.
+
+    """
+
+    zope.interface.implements(
+        zc.notification.interfaces.INotification)
+
+    name = message = mapping= event = timestamp = summary = None
+
+    def __init__(self, name, message, mapping=None, summary=None):
+        self.name = name
+        self.message = message
+        self.mapping = mapping
+        self.summary = summary
+        self.timestamp = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
+
+    def applicablePrincipals(self, principal_ids):
+        """Please(!) override in subclass!
+
+        Really.  This is very noisy.
+
+        """
+
+        return principal_ids
+
+class PrincipalNotification(Notification):
+
+    def __init__(self,
+                 name, message, principal_ids, mapping=None, summary=None):
+        super(PrincipalNotification, self).__init__(
+            name, message, mapping, summary)
+        self.principal_ids = frozenset(principal_ids)
+
+    def applicablePrincipals(self, principal_ids):
+        return self.principal_ids.intersection(principal_ids)
+
+class GroupAwarePrincipalNotification(PrincipalNotification):
+
+    def __init__(self,
+                 name, message, principal_ids, mapping=None, summary=None,
+                 exclude_ids=frozenset()):
+        super(GroupAwarePrincipalNotification, self).__init__(
+            name, message, principal_ids, mapping, summary)
+        self.principals = zope.component.getUtility(
+            zope.app.security.interfaces.IAuthentication)
+        self.group_ids = frozenset(
+            pid for pid in self.principal_ids if
+            zope.security.interfaces.IGroup.providedBy(
+                self.principals.getPrincipal(pid)))
+        self.exclude_ids = exclude_ids
+
+    def applicablePrincipals(self, principal_ids):
+        res = set()
+        for pid in principal_ids:
+            if pid in self.exclude_ids:
+                continue
+            if pid not in self.principal_ids:
+                if not self.group_ids:
+                    continue
+                # go through all groups of pid and see if they are in
+                # performers.  if not, continue
+                seen = set()
+                p = self.principals.getPrincipal(pid)
+                groups = getattr(p, 'groups', ())
+                if groups:
+                    stack = [iter(groups)]
+                    while stack:
+                        try:
+                            gid = stack[-1].next()
+                        except StopIteration:
+                            stack.pop()
+                        else:
+                            if gid not in seen:
+                                seen.add(gid)
+                                if gid in self.group_ids:
+                                    break
+                                p = self.principals.getPrincipal(gid)
+                                groups = getattr(p, 'groups', ())
+                                if groups:
+                                    stack.append(iter(groups))
+                    else:
+                        continue
+                else:
+                    continue
+            res.add(pid)
+        return res
+
+
+PREFERRED_METHOD_ANNOTATION_KEY = "zc.notification.preferred_method"
+
+
+class NotificationUtility(zope.app.container.contained.Contained,
+                          persistent.Persistent):
+    """Utility implementation.
+
+    """
+
+    zope.interface.implements(
+        zope.app.container.interfaces.IContained,
+        zc.notification.interfaces.INotificationUtility,
+        zc.notification.interfaces.INotificationUtilityConfiguration)
+
+    def __init__(self):
+        # notification name --> Set([principal_ids])
+        self._notifications = BTrees.OOBTree.OOBTree()
+        # principal_id --> Set([notification names])
+        self._registrations = BTrees.OOBTree.OOBTree()
+
+    def get_annotations(self, principal_id):
+        utility = zope.component.getUtility(IPrincipalAnnotationUtility)
+        return utility.getAnnotationsById(principal_id)
+
+    def setNotifierMethod(self, principal_id, method):
+        annotations = self.get_annotations(principal_id)
+        annotations[PREFERRED_METHOD_ANNOTATION_KEY] = method
+
+    def getNotifierMethod(self, principal_id):
+        annotations = self.get_annotations(principal_id)
+        return annotations.get(PREFERRED_METHOD_ANNOTATION_KEY, "")
+
+    def setRegistrations(self, principal_id, names):
+        names = set(names)
+        current = self.getRegistrations(principal_id)
+        added = names - current
+        removed = current - names
+        notifications = self._notifications
+
+        # We would use .setdefault() here, but Zope 3.1 doesn't have that.
+
+        for name in added:
+            principals = notifications.get(name)
+            if principals is None:
+                principals = set()
+            principals.add(principal_id)
+            notifications[name] = principals
+
+        for name in removed:
+            principals = notifications.get(name)
+            if principals is None:
+                principals = set()
+            principals.discard(principal_id)
+            notifications[name] = principals
+
+        self._registrations[principal_id] = names
+
+    def getRegistrations(self, principal_id):
+        return self._registrations.get(principal_id, set())
+
+    def getNotificationSubscriptions(self, notification_name):
+        return self._notifications.get(notification_name, set())
+
+    def notify(self, notification):
+        ids = notification.applicablePrincipals(
+            set(self._notifications.get(notification.name, ())))
+
+        for id in ids:
+            method = self.getNotifierMethod(id)
+            notifier = None
+            if method:
+                notifier = zope.component.queryUtility(
+                    zc.notification.interfaces.INotifier,
+                    name=method)
+            if notifier is None:
+                notifier = zope.component.getUtility(
+                    zc.notification.interfaces.INotifier)
+            notifier.send(notification, id, self.get_annotations(id))

Added: zc.notification/trunk/src/zc/notification/requestless.pt
===================================================================
--- zc.notification/trunk/src/zc/notification/requestless.pt	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/requestless.pt	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1 @@
+<html><body>Hello <span tal:replace="context/foo">you</span></body></html>

Added: zc.notification/trunk/src/zc/notification/requestless.py
===================================================================
--- zc.notification/trunk/src/zc/notification/requestless.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/requestless.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,60 @@
+from zope.pagetemplate import pagetemplatefile
+from zope.app.pagetemplate import engine, viewpagetemplatefile
+
+class Context(engine.ZopeContextBase):
+    def translate(self, msgid, domain=None, mapping=None, default=None):
+        return i18n.translate(
+            msgid, domain, mapping, context=self.principal, default=default)
+
+class ZopeEngine(engine.ZopeEngine):
+    _create_context = Context
+    def getContext(self, __namespace=None, **namespace):
+        if __namespace:
+            if namespace:
+                namespace.update(__namespace)
+            else:
+                namespace = __namespace
+
+        context = self._create_context(self, namespace)
+
+        # Put principal into context so path traversal can find it
+        if 'principal' in namespace:
+            context.principal = namespace['principal']
+
+        # Put context into context so path traversal can find it
+        if 'context' in namespace:
+            context.context = namespace['context']
+
+        return context
+
+Engine = engine._TrustedEngine(ZopeEngine())
+
+class AppPT(object):
+    def pt_getEngine(self):
+        return Engine
+
+class PageTemplateFile(AppPT, pagetemplatefile.PageTemplateFile):
+
+    def __init__(self, filename, _prefix=None):
+        _prefix = self.get_path_from_prefix(_prefix)
+        super(PageTemplateFile, self).__init__(filename, _prefix)
+
+    def pt_getContext(self, instance, **_kw):
+        # instance is object with 'context' and 'principal' atttributes.
+        namespace = super(PageTemplateFile, self).pt_getContext(**_kw)
+        namespace['view'] = instance
+        namespace['context'] = context = instance.context
+        return namespace
+
+    def __call__(self, instance, *args, **keywords):
+        namespace = self.pt_getContext(
+            instance=instance, args=args, options=keywords)
+        s = self.pt_render(
+            namespace,
+            showtal=getattr(instance, 'showTAL', 0),
+            sourceAnnotations=getattr(instance, 'sourceAnnotations', 0),
+            )
+        return s
+
+    def __get__(self, instance, type):
+        return viewpagetemplatefile.BoundPageTemplate(self, instance)

Added: zc.notification/trunk/src/zc/notification/requestless.txt
===================================================================
--- zc.notification/trunk/src/zc/notification/requestless.txt	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/requestless.txt	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,41 @@
+The requestless module provides a PageTemplateFile implementation that offers
+many of the typical Zope 3 template features but does so without a request. 
+It is useful for rendering a template for a user who is not in the current
+request, or perhaps when there is no request.
+
+Instead of a typical view-class page template, this expects to find a
+`principal` attribute on the instance to which the template has been bound,
+rather than a `request` attribute.  The principal must be adaptable to
+zope.i18n.interfaces.IUserPreferredLanguages in order for translation to
+work.
+
+Like a typical view-class page template, this expects to find a `context`
+attribute, which is often the object to be rendered.
+
+Let's have a quick demo.
+
+    >>> import zope.i18n.interfaces
+    >>> import zope.security.interfaces
+    >>> from zope import interface, component
+    >>> class DemoPrincipal(object):
+    ...     interface.implements(zope.security.interfaces.IPrincipal)
+    ...
+    >>> class DemoLanguagePrefAdapter(object):
+    ...     interface.implements(zope.i18n.interfaces.IUserPreferredLanguages)
+    ...     component.adapts(zope.security.interfaces.IPrincipal)
+    ...     def __init__(self, context):
+    ...         self.context = context
+    ...     def getPreferredLanguages(self):
+    ...         return ('en_us',)
+    ...
+    >>> component.provideAdapter(DemoLanguagePrefAdapter)
+    >>> context = {'foo': 'bar'}
+    >>> import requestless
+    >>> class Renderer(object):
+    ...     def __init__(self, context, principal):
+    ...         self.context = context
+    ...         self.principal = principal
+    ...     template = requestless.PageTemplateFile('requestless.pt')
+    ...
+    >>> Renderer(context, DemoPrincipal()).template()
+    u'<html><body>Hello bar</body></html>\n'


Property changes on: zc.notification/trunk/src/zc/notification/requestless.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.notification/trunk/src/zc/notification/tests.py
===================================================================
--- zc.notification/trunk/src/zc/notification/tests.py	2006-08-15 21:03:00 UTC (rev 69539)
+++ zc.notification/trunk/src/zc/notification/tests.py	2006-08-15 21:08:39 UTC (rev 69540)
@@ -0,0 +1,81 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""Test harness for zc.notification.
+"""
+__docformat__ = "reStructuredText"
+
+import unittest
+
+from zope.testing import doctest
+import zope.testing.module
+import zope.component
+import zope.interface
+
+import zope.app.principalannotation.interfaces
+import zope.app.testing.placelesssetup
+
+import zc.notification.interfaces
+
+class PrincipalAnnotationUtility(object):
+
+    zope.interface.implements(
+        zope.app.principalannotation.interfaces.IPrincipalAnnotationUtility)
+
+    def __init__(self):
+        self._data = {}
+
+    def getAnnotationsById(self, id):
+        return self._data.setdefault(id, {})
+
+
+class PrintNotifier(object):
+
+    zope.interface.implements(
+        zc.notification.interfaces.INotifier)
+
+    def __init__(self, method=""):
+        self.method = method
+
+    def send(self, notification, principal_id, annotations):
+        print notification.name
+        print notification.message
+        print principal_id, "by", repr(self.method)
+
+def setUp(test):
+    zope.app.testing.placelesssetup.setUp(test)
+    util = PrincipalAnnotationUtility()
+    zope.component.provideUtility(util)
+    zope.component.provideUtility(PrintNotifier("email"), name="email")
+    zope.component.provideUtility(PrintNotifier())
+
+def requestlessSetUp(test):
+    zope.app.testing.placelesssetup.setUp(test)
+    zope.testing.module.setUp(test, 'zc.notification.requestless_txt')
+
+def requestlessTearDown(test):
+    zope.testing.module.tearDown(test)
+    zope.app.testing.placelesssetup.tearDown(test)
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite(
+            "README.txt",
+            setUp=setUp,
+            tearDown=zope.app.testing.placelesssetup.tearDown),
+        doctest.DocFileSuite(
+            "requestless.txt",
+            setUp=requestlessSetUp,
+            tearDown=requestlessTearDown),
+        ))



More information about the Checkins mailing list