changeset 2194:1eaf51f6eb90

create and send notification mails
author Ana Balica <ana.balica@gmail.com>
date Sun, 15 Sep 2013 11:33:30 +0200
parents b3059f21645b
children 6484a3d24db6
files MoinMoin/util/_tests/test_notifications.py MoinMoin/util/notifications.py
diffstat 2 files changed, 314 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/util/_tests/test_notifications.py	Sun Sep 15 11:33:30 2013 +0200
@@ -0,0 +1,102 @@
+# Copyright: 2013 MoinMoin:AnaBalica
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - MoinMoin.util.notifications Tests
+"""
+
+from io import StringIO
+
+from flask import g as flaskg
+from flask import current_app as app
+from flask import url_for
+
+from MoinMoin.constants.keys import ACTION_SAVE, ACTION_TRASH
+from MoinMoin.items import Item
+from MoinMoin.util.diff_datastruct import diff as dict_diff
+from MoinMoin.util.notifications import Notification, get_item_last_revisions, DESTROY_REV, DESTROY_ALL
+
+
+class TestNotifications(object):
+    reinit_storage = True
+
+    def setup_method(self, method):
+        self.imw = flaskg.unprotected_storage
+        self.item_name = u"foo"
+
+    def test_get_last_item_revisions(self):
+        assert get_item_last_revisions(app, self.item_name) == []
+        item = self.imw[self.item_name]
+        rev1 = item.store_revision(dict(name=[self.item_name, ]),
+                                   StringIO(u'x'), trusted=True, return_rev=True)
+        assert get_item_last_revisions(app, self.item_name) == [rev1]
+        rev2 = item.store_revision(dict(name=[self.item_name, ]),
+                                   StringIO(u'xx'), trusted=True, return_rev=True)
+        assert get_item_last_revisions(app, self.item_name) == [rev2, rev1]
+        rev3 = item.store_revision(dict(name=[self.item_name, ]),
+                                   StringIO(u'xxx'), trusted=True, return_rev=True)
+        assert get_item_last_revisions(app, self.item_name) == [rev3, rev2]
+
+    def test_get_content_diff(self):
+        item = self.imw[self.item_name]
+        rev1 = item.store_revision(dict(name=[self.item_name, ], contenttype='text/plain'),
+                                   StringIO(u'x'), trusted=True, return_rev=True)
+        notification = Notification(app, self.item_name, [rev1], action=ACTION_SAVE)
+        assert notification.get_content_diff() == ["+ x"]
+        rev1.data.seek(0, 0)
+
+        rev2 = item.store_revision(dict(name=[self.item_name, ], contenttype='text/plain'),
+                                   StringIO(u'xx'), trusted=True, return_rev=True)
+        notification = Notification(app, self.item_name, [rev2, rev1], action=ACTION_SAVE)
+        assert notification.get_content_diff() == ['- x', '+ xx']
+        rev2.data.seek(0, 0)
+
+        notification = Notification(app, self.item_name, [rev2, rev1], action=ACTION_TRASH)
+        assert notification.get_content_diff() == ['- xx']
+        rev2.data.seek(0, 0)
+
+        item = Item.create(self.item_name)
+        notification = Notification(app, self.item_name, [], content=item.rev.data,
+                                    meta=rev2.meta, action=DESTROY_REV)
+        assert notification.get_content_diff() == ['- xx']
+        rev2.data.seek(0, 0)
+
+        item = Item.create(self.item_name)
+        notification = Notification(app, self.item_name, [], content=item.rev.data,
+                                    meta=rev2.meta, action=DESTROY_ALL)
+        assert notification.get_content_diff() == ['- xx']
+
+    def test_get_meta_diff(self):
+        item = self.imw[self.item_name]
+        rev1 = item.store_revision(dict(name=[self.item_name, ]), StringIO(u'x'),
+                                   trusted=True, return_rev=True)
+        notification = Notification(app, self.item_name, [rev1], action=ACTION_SAVE)
+        assert notification.get_meta_diff() == dict_diff(dict(), rev1.meta._meta)
+
+        rev2 = item.store_revision(dict(name=[self.item_name, ]), StringIO(u'xx'),
+                                   trusted=True, return_rev=True)
+        notification = Notification(app, self.item_name, [rev2, rev1], action=ACTION_SAVE)
+        assert notification.get_meta_diff() == dict_diff(rev1.meta._meta, rev2.meta._meta)
+
+        actions = [DESTROY_REV, DESTROY_ALL, ACTION_TRASH, ]
+        for action in actions:
+            notification = Notification(app, self.item_name, [rev2, rev1], meta=rev2.meta, action=action)
+            assert notification.get_meta_diff() == dict_diff(rev2.meta._meta, dict())
+
+    def test_generate_diff_url(self):
+        domain = "http://test.com"
+        notification = Notification(app, self.item_name, [], action=DESTROY_REV)
+        assert notification.generate_diff_url(domain) == u""
+
+        item = self.imw[self.item_name]
+        rev1 = item.store_revision(dict(name=[self.item_name, ]), StringIO(u'x'),
+                                   trusted=True, return_rev=True)
+        notification.revs = [rev1]
+        assert notification.generate_diff_url(domain) == u""
+
+        rev2 = item.store_revision(dict(name=[self.item_name, ]), StringIO(u'xx'),
+                                   trusted=True, return_rev=True)
+        notification.revs = [rev2, rev1]
+        assert notification.generate_diff_url(domain) == u"{0}{1}".format(
+            domain, url_for('frontend.diff', item_name=self.item_name,
+                            rev1=rev1.revid, rev2=rev2.revid))
--- a/MoinMoin/util/notifications.py	Sun Sep 15 11:29:58 2013 +0200
+++ b/MoinMoin/util/notifications.py	Sun Sep 15 11:33:30 2013 +0200
@@ -5,6 +5,218 @@
     MoinMoin - Notifications
 """
 
+from io import BytesIO
+
+from blinker import ANY
+from urlparse import urljoin
+from whoosh.query import Term, And
+
+from flask import url_for, g as flaskg
+
+from MoinMoin.constants.keys import (ACTION_COPY, ACTION_RENAME, ACTION_REVERT,
+                                     ACTION_SAVE, ACTION_TRASH, ALL_REVS, CONTENTTYPE,
+                                     MTIME, NAME_EXACT, WIKINAME)
+from MoinMoin.i18n import _, L_, N_
+from MoinMoin.i18n import force_locale
+from MoinMoin.items.content import Content
+from MoinMoin.mail.sendmail import sendmail
+from MoinMoin.themes import render_template
+from MoinMoin.signalling.signals import item_modified
+from MoinMoin.util.subscriptions import get_subscribers
+from MoinMoin.util.diff_datastruct import make_text_diff, diff as dict_diff
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+# additional action values
+ACTION_CREATE = u"CREATE"
+ACTION_MODIFY = u"MODIFY"
+
 # destroy types
 DESTROY_REV = u"DESTROY_REV"
 DESTROY_ALL = u"DESTROY_ALL"
+
+
+def msgs():
+    """ Encapsulates the main notification messages
+
+    :return: a dictionary of notification messages
+    """
+    _ = lambda x: x
+    messages = {
+        ACTION_CREATE: _("The '%(item_name)s' item on '%(wiki_name)s' has been created by %(user_name)s:"),
+        ACTION_MODIFY: _("The '%(item_name)s' item on '%(wiki_name)s' has been modified by %(user_name)s:"),
+        ACTION_RENAME: _("The '%(item_name)s' item on '%(wiki_name)s' has been renamed by %(user_name)s:"),
+        ACTION_COPY: _("The '%(item_name)s' item on '%(wiki_name)s' has been copied by %(user_name)s:"),
+        ACTION_REVERT: _("The '%(item_name)s' item on '%(wiki_name)s' has been reverted by %(user_name)s:"),
+        ACTION_TRASH: _("The '%(item_name)s' item on '%(wiki_name)s' has been deleted by %(user_name)s:"),
+        DESTROY_REV: _("The '%(item_name)s' item on '%(wiki_name)s' has one revision destroyed by %(user_name)s:"),
+        DESTROY_ALL: _("The '%(item_name)s' item on '%(wiki_name)s' has been destroyed by %(user_name)s:"),
+    }
+    return messages
+
+MESSAGES = msgs()
+
+
+class Notification(object):
+    """
+    Represents a mail notification about an item change
+    """
+    txt_template = "mail/notification.txt"
+    html_template = "mail/notification_main.html"
+
+    def __init__(self, app, item_name, revs, **kwargs):
+        self.app = app
+        self.item_name = item_name
+        self.revs = revs
+        self.action = kwargs.get('action', None)
+        self.content = kwargs.get('content', None)
+        self.meta = kwargs.get('meta', None)
+        self.comment = kwargs.get('comment', None)
+        self.wiki_name = self.app.cfg.interwikiname
+
+        if self.action == ACTION_SAVE:
+            self.action = ACTION_CREATE if len(self.revs) == 1 else ACTION_MODIFY
+
+        if self.action == ACTION_TRASH:
+            self.meta = self.revs[0].meta
+
+        kw = dict(item_name=self.item_name, wiki_name=self.wiki_name, user_name=flaskg.user.name0)
+        self.notification_sentence = L_(MESSAGES[self.action], **kw)
+
+    def get_content_diff(self):
+        """ Create a content diff for the last item change
+
+        :return: list of diff lines
+        """
+        if self.action in [DESTROY_REV, DESTROY_ALL, ]:
+            contenttype = self.meta[CONTENTTYPE]
+            oldfile, newfile = self.content, BytesIO("")
+        elif self.action == ACTION_TRASH:
+            contenttype = self.meta[CONTENTTYPE]
+            oldfile, newfile = self.revs[0].data, BytesIO("")
+        else:
+            newfile = self.revs[0].data
+            if len(self.revs) == 1:
+                contenttype = self.revs[0].meta[CONTENTTYPE]
+                oldfile = BytesIO("")
+            else:
+                from MoinMoin.apps.frontend.views import _common_type
+                contenttype = _common_type(self.revs[0].meta[CONTENTTYPE], self.revs[1].meta[CONTENTTYPE])
+                oldfile = self.revs[1].data
+        content = Content.create(contenttype)
+        return content._get_data_diff_text(oldfile, newfile)
+
+    def get_meta_diff(self):
+        """ Create a meta diff for the last item change
+
+        :return: a list of tuples of the format (<change type>, <basekeys>, <value>)
+                 that can be used to format a diff
+        """
+        if self.action in [ACTION_TRASH, DESTROY_REV, DESTROY_ALL, ]:
+            old_meta, new_meta = dict(self.meta), dict()
+        else:
+            new_meta = dict(self.revs[0].meta)
+            if len(self.revs) == 1:
+                old_meta = dict()
+            else:
+                old_meta = dict(self.revs[1].meta)
+        meta_diff = dict_diff(old_meta, new_meta)
+        return meta_diff
+
+    def generate_diff_url(self, domain):
+        """ Generate the URL that leads to diff page of the last 2 revisions
+
+        :param domain: domain name
+        :return: the absolute URL to the diff page
+        """
+        if len(self.revs) < 2:
+            return u""
+        else:
+            revid1 = self.revs[1].revid
+            revid2 = self.revs[0].revid
+        diff_rel_url = url_for('frontend.diff', item_name=self.item_name, rev1=revid1, rev2=revid2)
+        return urljoin(domain, diff_rel_url)
+
+    def render_templates(self, content_diff, meta_diff):
+        """ Render both plain text and HTML templates by providing all the
+        necessary arguments
+
+        :return: tuple consisting of plain text and HTML notification message
+         """
+        meta_diff_txt = list(make_text_diff(meta_diff))
+        domain = self.app.cfg.interwiki_map[self.app.cfg.interwikiname]
+        unsubscribe_url = urljoin(domain, url_for('frontend.subscribe_item',
+                                                  item_name=self.item_name))
+        diff_url = self.generate_diff_url(domain)
+        item_url = urljoin(domain, url_for('frontend.show_item', item_name=self.item_name))
+        if self.comment:
+            comment = self.meta["comment"]
+        else:
+            comment = self.revs[0].meta["comment"]
+        txt_template = render_template(Notification.txt_template,
+                                       wiki_name=self.wiki_name,
+                                       notification_sentence=self.notification_sentence,
+                                       diff_url=diff_url,
+                                       item_url=item_url,
+                                       comment=comment,
+                                       content_diff_=content_diff,
+                                       meta_diff_=meta_diff_txt,
+                                       unsubscribe_url=unsubscribe_url,
+                                       )
+        html_template = render_template(Notification.html_template,
+                                        wiki_name=self.wiki_name,
+                                        notification_sentence=self.notification_sentence,
+                                        diff_url=diff_url,
+                                        item_url=item_url,
+                                        comment=comment,
+                                        content_diff_=content_diff,
+                                        meta_diff_=meta_diff,
+                                        unsubscribe_url=unsubscribe_url,
+        )
+        return txt_template, html_template
+
+
+def get_item_last_revisions(app, item_name):
+    """ Get 2 or less most recent item revisions from the index
+
+    :param app: local proxy app
+    :param item_name: the name of the item
+    :return: a list of revisions
+    """
+    terms = [Term(WIKINAME, app.cfg.interwikiname), Term(NAME_EXACT, item_name), ]
+    query = And(terms)
+    return list(
+        flaskg.storage.search(query, idx_name=ALL_REVS, sortedby=[MTIME],
+                              reverse=True, limit=2))
+
+
+@item_modified.connect_via(ANY)
+def send_notifications(app, item_name, **kwargs):
+    """ Send mail notifications to subscribers on item change
+
+    :param app: local proxy app
+    :param item_name: name of the changed item
+    :param kwargs: key/value pairs that contain extra information about the item
+                   required in order to create a notification
+    """
+    action = kwargs.get('action')
+    revs = get_item_last_revisions(app, item_name) if action not in [
+        DESTROY_REV, DESTROY_ALL, ] else []
+    notification = Notification(app, item_name, revs, **kwargs)
+    content_diff = notification.get_content_diff()
+    meta_diff = notification.get_meta_diff()
+
+    u = flaskg.user
+    meta = kwargs.get('meta') if action in [DESTROY_REV, DESTROY_ALL, ] else revs[0].meta._meta
+    subscribers = {subscriber for subscriber in get_subscribers(**meta) if
+                   subscriber.itemid != u.itemid}
+    subscribers_locale = {subscriber.locale for subscriber in subscribers}
+    for locale in subscribers_locale:
+        with force_locale(locale):
+            txt_msg, html_msg = notification.render_templates(content_diff, meta_diff)
+            subject = L_('[%(moin_name)s] Update of "%(item_name)s" by %(user_name)s',
+                         moin_name=app.cfg.interwikiname, item_name=item_name, user_name=u.name0)
+            subscribers_emails = [subscriber.email for subscriber in subscribers
+                                  if subscriber.locale == locale]
+            sendmail(subject, txt_msg, to=subscribers_emails, html=html_msg)