How to Write a Notification

Or, more correctly, how Michael writes notifications.

I write notifications in three steps: 1. Write an HTML Page, 2. Write a Text Page, and 3. Write a Notifier.

1. Write an HTML Page

A MessageSender takes both a HTML and a plain-text version of a message. However, I find it easier to start with the HTML form, and then work on the text-version (see 2. Write a Text Page below). I write the HTML form of the message as a normal page-view.

If the message is about a group, or a group member, I will make the view a subclass of the gs.content.email.base.GroupEmail class [1], so it is in the group context [2].

class InvitationMessage(GroupEmail):

    def __init__(self, context, request):
        super(InvitationMessage, self).__init__(context, request)

Messages about group members are placed in the group-context so the permissions are correct.

<browser:page
  name="invitationmessage.html"
  for="gs.group.base.interfaces.IGSGroupMarker"
  class=".notifymessages.InvitationMessage"
  template="browser/templates/new-invitationmessage.pt"
  permission="zope2.View" />

If the message is just about an individual, removed from the context of the group, I will make the page a subclass of the gs.content.email.base.SiteEmail class [1].

from gs.content.email.base import SiteEmail, TextMixin

class ProfileStatus(SiteEmail):
    'The profile-status notification'

The page is rendered in the context of a user it is in the context of a user [4].

<browser:page
  name="gs-profile-status.html"
  for="Products.CustomUserFolder.interfaces.ICustomUser"
  class=".notification.ProfileStatus"
  template="browser/templates/notification-html.pt"
  permission="zope2.ManageProperties" />

The view sometimes has code specific to the message, just like other page-views often have page-specific code in them.

At the top of the page-template I set up all the arguments to the page. Later these will be passed in as options (see 3. Write a Notifier below). However, for prototyping it is easier to use hard-coded defaults if the arguments are not supplied:

<html
  tal:define="userInfo options/userInfo | view/userInfo;
              emailAddress options/emailAddress | string:placeholder@email.address;
              verifyLink options/verifyLink | string:${view/siteInfo/url}/r/verify/placeholder">

The above code is in the context of a user, so there is always a view/userInfo available for the userInfo option. In the group-context I use the logged-in user information (self.loggedInUserInfo) to fill in the user-specific details. The two other options use place-holder strings: one completely hard-coded, and one with some site-specific information.

2. Write a Text Page

The text-page is normally a cut-down version of the HTML-page. It hangs off the same marker interfaces, and I will give the page the same name, except with a .txt extension.

I normally make the view a subclass of the HTML view, and make use of the from gs.content.email.base.TextMixin class.

class SomeNotificationText(SomeNotificationHTML, TextMixin):

    def __init__(self, context, request):
        super(SomeNotificationText, self).__init__(context, request)
        filename = 'some-notification-{0}.txt'.format(self.groupInfo.id)
        self.set_header(filename)

    def format_message_no_indent(self, m):
        tw = TextWrapper()
        retval = tw.fill(m)
        return retval

The page-template itself normally follows the HTML closely, but with all styling removed and the remaining elements replaced with <tal:block /> elements.

3. Write a Notifier

A notifier is what is called by the UI to send the message. It creates the HTML and text forms of the message, and sends it to the correct people. Normally they notifier is a sub-class of the gs.content.email.base.NotifierABC abstract base-class, or one of its subclasses [1]:

class DigestOnNotifier(GroupNotifierABC):
    htmlTemplateName = 'gs-group-member-email-settings-digest-on.html'
    textTemplateName = 'gs-group-member-email-settings-digest-on.txt'

    def notify(self, userInfo):
        subject = _('digest-on-subject',
                    'Topic digests from ${groupName}',
                    mapping={'groupName': self.groupInfo.name})
        translatedSubject = translate(subject)
        text = self.textTemplate()
        html = self.htmlTemplate()

        sender = MessageSender(self.context, userInfo)
        sender.send_message(translatedSubject, text, html)
        self.reset_content_type()
Initialisation:
The notifier needs access to both the request and context. Because of this it is the responsibility of the user-interfaces (normally forms) to send the notifications. It is not the responsibility of the low-level code that actually does the work.
htmlTemplate:

This property acquires the HTML view of the message. To do this it calls:

getMultiAdapter((self.context, self.request),
                name=self.htmlTemplateName)

This is the same way that the normal publishing system acquires the view for display. The view is not rendered until the notify method is called (see below).

textTemplate:
This property works much the same way as the HTML Template property, but the name of the text-view is passed in.
notify:

This method does three things.

  1. Renders the HTML and text versions of the message. It does this by passing in any options that are needed by the page. For example:

    text = self.textTemplate(userInfo=userInfo)
    html = self.htmlTemplate(userInfo=userInfo)
    
  2. Instantiating the MessageSender class.

  3. Calling the MessageSender.send_message() method.

The main difference between the different Notifier classes are different views are created (the names passed to the named-adaptor calls are different), and the notify method takes different arguments. These arguments are normally blindly passed on to the two views.

[1](1, 2, 3) See gs.content.email.base <https://github.com/groupserver/gs.content.email.base>
[2]A page in the group-context will hang off the gs.group.base.interfaces.IGSGroupMarker marker interface
[3]See gs.profile.base <https://github.com/groupserver/gs.profile.base>
[4]A page in the context of a user will hang off the marker interface Products.CustomUserFolder.interfaces.ICustomUser.