Zine

open source content publishing system


source: zine/notifications.py @ 1279:088d2f519391

Revision 1279:088d2f519391, 11.3 KB checked in by Georg Brandl <georg@…>, 2 years ago (diff)

Update copyright notices.

Line 
1# -*- coding: utf-8 -*-
2"""
3    zine.notifications
4    ~~~~~~~~~~~~~~~~~~
5
6    This module implements an extensible notification system.  Plugins can
7    provide different kinds of notification systems (like email, jabber etc.)
8
9    Each user can subscribe to different kinds of events.  The general design
10    is inspired by Growl.
11
12    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
13    :license: BSD, see LICENSE for more details.
14"""
15from datetime import datetime
16from urlparse import urlsplit
17
18from werkzeug import url_unquote
19
20from zine.models import NotificationSubscription
21from zine.application import get_application, get_request, render_template
22from zine.privileges import BLOG_ADMIN, ENTER_ACCOUNT_PANEL, MODERATE_COMMENTS,\
23     MODERATE_OWN_PAGES, MODERATE_OWN_ENTRIES
24from zine.utils.zeml import parse_zeml
25from zine.utils.mail import send_email
26from zine.i18n import lazy_gettext
27
28
29__all__ = ['DEFAULT_NOTIFICATION_TYPES', 'NotificationType']
30
31DEFAULT_NOTIFICATION_TYPES = {}
32
33
34def send_notification(type, message, user=Ellipsis):
35    """Convenience function.  Get the application object and deliver the
36    notification to it's NotificationManager.
37
38    The message must be a valid ZEML formatted message.  The following
39    top-level elements are available for marking up the message:
40
41    title
42        The title of the notification.  Some systems may only transmit this
43        part of the message.
44
45    summary
46        An optional quick summary.  If the text is short enough it can be
47        omitted and the system will try to transmit the longtext in that
48        case.  The upper limit for the summary should be around 100 chars.
49
50    details
51        If given this may either contain a paragraph with textual information
52        or an ordered or unordered list of text or links.  The general markup
53        rules apply.
54
55    longtext
56        The full text of this notification.  May contain some formattings.
57
58    actions
59        If given this may contain an unordered list of action links.  These
60        links may be transmitted together with the notification.
61
62    Additionally if there is an associated page with the notification,
63    somewhere should be a link element with a "selflink" class.  This can be
64    embedded in the longtext or actions (but any other element too).
65
66    Example markup::
67
68        <title>New comment on "Foo bar baz"</title>
69        <summary>Mr. Miracle wrote a new comment: "This is awesome."</summary>
70        <details>
71          <ul>
72            <li><a href="http://miracle.invalid/">Mr. Miracle</a>
73            <li><a href="mailto:mr@miracle.invalid">E-Mail</a>
74          </ul>
75        </details>
76        <longtext>
77          <p>This is awesome.  Keep it up!
78          <p>Love your work
79        </longtext>
80        <actions>
81          <ul>
82            <li><a href="http://.../link" class="selflink">all comments</a>
83            <li><a href="http://.../?action=delete">delete it</a>
84            <li><a href="http://.../?action=approve">approve it</a>
85          </ul>
86        </actions>
87
88    Example plaintext rendering (e-mail)::
89
90        Subject: New comment on "Foo bar baz"
91
92        Mr. Miracle             http://miracle.invalid/
93        E-Mail                  mr@mircale.invalid
94
95        > This is awesome.   Keep it up!
96        > Love your work.
97
98        Actions:
99          - delete it           http://.../?action=delete
100          - approve it          http://.../?action=approve
101
102    Example IM notification rendering (jabber)::
103
104        New comment on "Foo bar baz."  Mr. Miracle wrote anew comment:
105        "This is awesome".  http://.../link
106    """
107    get_application().notification_manager.send(
108        Notification(type, message, user)
109    )
110
111
112def send_notification_template(type, template_name, user=Ellipsis, **context):
113    """Like `send_notification` but renders a template instead."""
114    notification = render_template(template_name, **context)
115    send_notification(type, notification, user)
116
117
118class NotificationType(object):
119    """There are different kinds of notifications. E.g. you want to
120    send a special type of notification after a comment is saved.
121    """
122
123    def __init__(self, name, description, privileges):
124        self.name = name
125        self.description = description
126        self.privileges = privileges
127
128    def __repr__(self):
129        return '<%s %r>' % (self.__class__.__name__, self.name)
130
131
132class Notification(object):
133    """A notification that can be sent to a user. It contains a message.
134    The message is a zeml construct.
135    """
136
137    def __init__(self, id, message, user=Ellipsis):
138        self.message = parse_zeml(message, 'system')
139        self.id = id
140        self.sent_date = datetime.utcnow()
141        if user is Ellipsis:
142            self.user = get_request().user
143        else:
144            self.user = user
145
146    @property
147    def self_link(self):
148        link = self.message.query('a[class~=selflink]').first
149        if link is not None:
150            return link.attributes.get('href')
151
152    title = property(lambda x: x.message.query('/title').first)
153    details = property(lambda x: x.message.query('/details').first)
154    actions = property(lambda x: x.message.query('/actions').first)
155    summary = property(lambda x: x.message.query('/summary').first)
156    longtext = property(lambda x: x.message.query('/longtext').first)
157
158
159class NotificationSystem(object):
160    """Use this as a base class for specific notification systems such as
161    `JabberNotificationSystem` or `EmailNotificationSystem`.
162
163    The class must implement a method `send` that receives a notification
164    object and a user object as parameter and then sends the message via
165    the specific system.  The plugin is itself responsible for extracting the
166    information necessary to send the message from the user object.  (Like
167    extracting the email address).
168    """
169
170    def __init__(self, app):
171        self.app = app
172
173    #: subclasses have to overrides this as class attributes.
174    name = None
175    key = None
176
177    def send(self, user, notification):
178        raise NotImplementedError()
179
180
181class EMailNotificationSystem(NotificationSystem):
182    """Sends notifications to user via E-Mail."""
183
184    key = 'email'
185    name = lazy_gettext(u'E-Mail')
186
187    def send(self, user, notification):
188        title = u'[%s] %s' % (
189            self.app.cfg['blog_title'],
190            notification.title.to_text()
191        )
192        text = self.mail_from_notification(notification)
193        send_email(title, text, [user.email])
194
195    def unquote_link(self, link):
196        """Unquotes some kinds of links.  For example mailto:foo links are
197        stripped and properly unquoted because the mails we write are in
198        plain text and nobody is interested in URLs there.
199        """
200        scheme, netloc, path = urlsplit(link)[:3]
201        if scheme == 'mailto':
202            return url_unquote(path)
203        return link
204
205    def collect_list_details(self, container):
206        """Returns the information collected from a single detail list item."""
207        for item in container.children:
208            if len(item.children) == 1 and item.children[0].name == 'a':
209                link = item.children[0]
210                href = link.attributes.get('href')
211                yield dict(text=link.to_text(simple=True),
212                           link=self.unquote_link(href), is_textual=False)
213            else:
214                yield dict(text=item.to_text(multiline=False),
215                           link=None, is_textual=True)
216
217
218    def find_details(self, container):
219        # no container given, nothing can be found
220        if container is None or not container.children:
221            return []
222
223        result = []
224        for child in container.children:
225            if child.name in ('ul', 'ol'):
226                result.extend(self.collect_list_details(child))
227            elif child.name == 'p':
228                result.extend(dict(text=child.to_text(),
229                                   link=None, is_textual=True))
230        return result
231
232    def find_actions(self, container):
233        if not container:
234            return []
235        ul = container.query('/ul').first
236        if not ul:
237            return []
238        return list(self.collect_list_details(ul))
239
240    def mail_from_notification(self, message):
241        title = message.title.to_text()
242        details = self.find_details(message.details)
243        longtext = message.longtext.to_text(collect_urls=True,
244                                            initial_indent=2)
245        actions = self.find_actions(message.actions)
246        return render_template('notifications/email.txt', title=title,
247                               details=details, longtext=longtext,
248                               actions=actions)
249
250
251class NotificationManager(object):
252    """The NotificationManager is informed about new notifications by the
253    send_notification function. It then decides to which notification
254    plugins the notification is handed over by looking up a database table
255    in the form:
256
257        user_id  | notification_system | notification id
258        ---------+---------------------+--------------------------
259        1        | jabber              | NEW_COMMENT
260        1        | email               | ZINE_UPGRADE_AVAILABLE
261        1        | sms                 | SERVER_EXPLODED
262
263    The NotificationManager also assures that only users interested in
264    a particular type of notifications receive a message.
265    """
266
267    def __init__(self):
268        self.systems = {}
269        self.notification_types = DEFAULT_NOTIFICATION_TYPES.copy()
270
271    def send(self, notification):
272        # given the type of the notification, check what users want that
273        # notification; via what system and call the according
274        # notification system in order to finally deliver the message
275        subscriptions = NotificationSubscription.query.filter_by(
276            notification_id=notification.id.name
277        )
278        if notification.user:
279            subscriptions = subscriptions.filter(
280                NotificationSubscription.user!=notification.user
281            )
282
283        for subscription in subscriptions.all():
284            system = self.systems.get(subscription.notification_system)
285            if system is not None:
286                system.send(subscription.user, notification)
287
288    def types(self, user=None):
289        if not user:
290            user = get_request().user
291        for notification in self.notification_types.itervalues():
292            if user.has_privilege(notification.privileges):
293                yield notification
294
295    def add_notification_type(self, notification):
296        self.notification_types[type.name] = type
297
298
299def _register(name, description, privileges=ENTER_ACCOUNT_PANEL):
300    """Register a new builtin type of notifications."""
301    nottype = NotificationType(name, description, privileges)
302    DEFAULT_NOTIFICATION_TYPES[name] = nottype
303    globals()[name] = nottype
304    __all__.append(name)
305
306
307_register('NEW_COMMENT',
308          lazy_gettext(u'When a new comment is received.'))
309_register('COMMENT_REQUIRES_MODERATION',
310          lazy_gettext(u'When a comment requires moderation.'),
311          (MODERATE_OWN_PAGES | MODERATE_OWN_ENTRIES | MODERATE_COMMENTS))
312_register('SECURITY_ALERT',
313          lazy_gettext(u'When Zine found an urgent security alarm.'),
314          BLOG_ADMIN)
315_register('ZINE_ERROR', lazy_gettext(u'When Zine throws errors.'), BLOG_ADMIN)
316
317
318DEFAULT_NOTIFICATION_SYSTEMS = [EMailNotificationSystem]
319del _register
Note: See TracBrowser for help on using the repository browser.