[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