source: announcerplugin/trunk/announcer/opt/fullblog/announce.py

Last change on this file was 16893, checked in by Ryan J Ollos, 6 years ago

TracAnnouncer 1.2.0dev: Use Trac 1.2 API

The plugin is loading and basic functionality is working
when the Trac notification system is disabled. Some of the
optional components may not yet be functional.

This is incremental progress towards removing the components
that have been added to Trac in release 1.2.

Refs #12120.

File size: 14.4 KB
RevLine 
[7663]1# -*- coding: utf-8 -*-
[7637]2#
[7663]3# Copyright (c) 2010, Robert Corsaro
[8083]4# Copyright (c) 2010, Steffen Hoffmann
[9207]5#
[12189]6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution.
[9207]8#
[12189]9
[9207]10import re
11
[16128]12from genshi.template import NewTextTemplate, TemplateLoader
13from tracfullblog.api import IBlogChangeListener
14from tracfullblog.model import BlogPost, BlogComment
[7637]15from trac.config import BoolOption, Option
[16128]16from trac.core import Component, implements
17from trac.util.html import html
[9207]18from trac.web.api import IRequestFilter, IRequestHandler
19from trac.web.chrome import Chrome, add_notice, add_ctxtnav
[7637]20
[16128]21from announcer.api import _, AnnouncementSystem, AnnouncementEvent,\
22                          IAnnouncementFormatter, IAnnouncementSubscriber, \
23                          IAnnouncementPreferenceProvider
[7637]24from announcer.distributors.mail import IAnnouncementEmailDecorator
[9207]25from announcer.model import Subscription, SubscriptionAttribute
[7637]26from announcer.util.mail import set_header, next_decorator
27
28
29class BlogChangeEvent(AnnouncementEvent):
[16128]30
[7637]31    def __init__(self, blog_post, category, url, blog_comment=None):
32        AnnouncementEvent.__init__(self, 'blog', category, blog_post)
33        if blog_comment:
34            if 'comment deleted' == category:
35                self.comment = blog_comment['comment']
36                self.author = blog_comment['author']
37                self.timestamp = blog_comment['time']
38            else:
39                self.comment = blog_comment.comment
40                self.author = blog_comment.author
41                self.timestamp = blog_comment.time
42        else:
43            self.comment = blog_post.version_comment
44            self.author = blog_post.version_author
45            self.timestamp = blog_post.version_time
[9207]46        self.remote_addr = url
[7637]47        self.version = blog_post.version
48        self.blog_post = blog_post
49        self.blog_comment = blog_comment
50
[16128]51
[9207]52class FullBlogAllSubscriber(Component):
53    """Subscriber for any blog changes."""
[7637]54
[9207]55    implements(IAnnouncementSubscriber)
[7637]56
[9207]57    def matches(self, event):
58        if event.realm != 'blog':
59            return
[16128]60        if event.category not in ('post created', 'post changed',
61                                  'post deleted', 'comment created',
62                                  'comment changed', 'comment deleted'):
[9207]63            return
[7637]64
[9207]65        klass = self.__class__.__name__
66        for i in Subscription.find_by_class(self.env, klass):
67            yield i.subscription_tuple()
68
69    def description(self):
[12350]70        return _("notify me when any blog is modified, "
[16128]71                 "changed, deleted or commented on.")
[9207]72
73
74class FullBlogNewSubscriber(Component):
75    """Subscriber for any blog post creation."""
76
77    implements(IAnnouncementSubscriber)
78
79    def matches(self, event):
80        if event.realm != 'blog':
81            return
82        if event.category != 'post created':
83            return
84
85        klass = self.__class__.__name__
86        for i in Subscription.find_by_class(self.env, klass):
87            yield i.subscription_tuple()
88
89    def description(self):
90        return "notify me when any blog post is created."
91
92
93class FullBlogMyPostSubscriber(Component):
94    """Subscriber for any blog changes to my posts."""
95
96    implements(IAnnouncementSubscriber)
97
98    always_notify_author = BoolOption('fullblog-announcement',
[16128]99        'always_notify_author', True,
100        """Notify the blog author of any changes to her blogs,
101        including changes to comments.
102        """)
[7637]103
[9207]104    def matches(self, event):
105        if event.realm != 'blog':
106            return
[16128]107        if event.category not in ('post changed', 'post deleted',
108                                  'comment created', 'comment changed',
[9207]109                                  'comment deleted'):
110            return
111
[16128]112        sids = ((event.blog_post.author, 1),)
[9207]113        klass = self.__class__.__name__
114        for i in Subscription.find_by_sids_and_class(self.env, sids, klass):
115            yield i.subscription_tuple()
116
117    def description(self):
[12350]118        return _("notify me when any blog that I posted "
[16128]119                 "is modified or commented on.")
[9207]120
[16128]121
[9207]122class FullBlogWatchSubscriber(Component):
123    """Subscriber to watch individual blogs."""
124
[16128]125    implements(IAnnouncementSubscriber, IRequestFilter, IRequestHandler)
[9207]126
[16128]127    # IAnnouncementSubscriber methods
128
[9207]129    def matches(self, event):
130        if event.realm != 'blog':
131            return
[16128]132        if event.category not in ('post created', 'post changed',
133                                  'post deleted', 'comment created',
134                                  'comment changed', 'comment deleted'):
[9207]135            return
136
137        klass = self.__class__.__name__
138
[16128]139        attrs = SubscriptionAttribute.\
140            find_by_class_realm_and_target(self.env, klass, 'blog',
141                                           event.blog_post.name)
142        sids = set(map(lambda x: (x['sid'], x['authenticated']), attrs))
[9207]143
144        for i in Subscription.find_by_sids_and_class(self.env, sids, klass):
145            yield i.subscription_tuple()
146
147    def description(self):
[16128]148        return _("notify me when a blog that I'm watching changes.")
[9207]149
[16128]150    # IRequestFilter methods
151
[9207]152    def pre_process_request(self, req, handler):
153        return handler
154
155    def post_process_request(self, req, template, data, content_type):
156        if 'BLOG_VIEW' not in req.perm:
[16128]157            return template, data, content_type
[9207]158
159        if '_blog_watch_message_' in req.session:
160            add_notice(req, req.session['_blog_watch_message_'])
161            del req.session['_blog_watch_message_']
162
[16128]163        if req.authname == 'anonymous':
164            return template, data, content_type
[9207]165
166        # FullBlogPlugin sets the blog_path arg in pre_process_request
167        name = req.args.get('blog_path')
168        if not name:
[16128]169            return template, data, content_type
[9207]170
171        klass = self.__class__.__name__
172
173        attrs = SubscriptionAttribute.find_by_sid_class_and_target(
[9235]174            self.env, req.session.sid, req.session.authenticated, klass, name)
[9207]175        if attrs:
[16128]176            add_ctxtnav(req, html.a(_("Unwatch This"),
177                                    href=req.href.blog_watch(name)))
[9207]178        else:
[16128]179            add_ctxtnav(req, html.a(_("Watch This"),
180                                    href=req.href.blog_watch(name)))
[9207]181
[16128]182        return template, data, content_type
[9207]183
[16128]184    # IRequestHandler methods
185
[9207]186    def match_request(self, req):
187        return re.match(r'^/blog_watch/(.*)', req.path_info)
188
189    def process_request(self, req):
190        klass = self.__class__.__name__
191
192        m = re.match(r'^/blog_watch/(.*)', req.path_info)
193        (name,) = m.groups()
194
[16893]195        with self.env.db_transaction:
[9207]196            attrs = SubscriptionAttribute.find_by_sid_class_and_target(
[9235]197                self.env, req.session.sid, req.session.authenticated,
198                klass, name)
[9207]199            if attrs:
200                SubscriptionAttribute.delete_by_sid_class_and_target(
[9235]201                    self.env, req.session.sid, req.session.authenticated,
202                    klass, name)
[9207]203                req.session['_blog_watch_message_'] = \
[16128]204                    _("You are no longer watching this blog post.")
[9207]205            else:
206                SubscriptionAttribute.add(
[9235]207                    self.env, req.session.sid, req.session.authenticated,
208                    klass, 'blog', (name,))
[9207]209                req.session['_blog_watch_message_'] = \
[16128]210                    _("You are now watching this blog post.")
211
[9207]212        req.redirect(req.href.blog(name))
213
214
215class FullBlogBloggerSubscriber(Component):
216    """Subscriber for any blog changes to bloggers that I follow."""
217
[16128]218    implements(IAnnouncementPreferenceProvider, IAnnouncementSubscriber)
[9207]219
220    def matches(self, event):
221        if event.realm != 'blog':
222            return
[16128]223        if event.category not in ('post created', 'post changed',
224                                  'post deleted', 'comment created',
225                                  'comment changed', 'comment deleted'):
[9207]226            return
227
228        klass = self.__class__.__name__
229
[9235]230        sids = set(map(lambda x: (x['sid'], x['authenticated']),
[16128]231                       SubscriptionAttribute.find_by_class_realm_and_target(
232                           self.env, klass, 'blog', event.blog_post.author)))
[9207]233
234        for i in Subscription.find_by_sids_and_class(self.env, sids, klass):
235            yield i.subscription_tuple()
236
237    def description(self):
[16128]238        return _("notify me when any blogger that I follow has a blog "
239                 "update.")
[9207]240
[16128]241    # IAnnouncementPreferenceProvider methods
242
[9207]243    def get_announcement_preference_boxes(self, req):
[16128]244        if req.authname == 'anonymous' and 'email' not in req.session:
[9207]245            return
[16128]246        yield 'bloggers', _("Followed Bloggers")
[9207]247
248    def render_announcement_preference_box(self, req, panel):
249        klass = self.__class__.__name__
250
251        if req.method == "POST":
[16893]252            with self.env.db_transaction:
[9207]253                SubscriptionAttribute.delete_by_sid_and_class(
[16128]254                    self.env, req.session.sid, req.session.authenticated,
255                    klass)
[9207]256                blogs = set(map(lambda x: x.strip(),
[16128]257                                req.args.get(
258                                    'announcer_watch_bloggers').split(',')))
[9207]259                SubscriptionAttribute.add(self.env, req.session.sid,
[16128]260                                          req.session.authenticated, klass,
261                                          'blog', blogs)
[9207]262
[16128]263        attrs = SubscriptionAttribute.\
264            find_by_sid_and_class(self.env, req.session.sid,
265                                  req.session.authenticated, klass)
[9235]266        data = {'sids': ','.join(set(map(lambda x: x['target'], attrs)))}
[16128]267        return 'prefs_announcer_watch_bloggers.html', dict(data=data)
[9207]268
269
270class FullBlogAnnouncement(Component):
271    """Send announcements on blog events."""
272
[16128]273    implements(IAnnouncementEmailDecorator, IAnnouncementFormatter,
274               IBlogChangeListener)
[9207]275
[7637]276    blog_email_subject = Option('fullblog-announcement', 'blog_email_subject',
[16128]277        _("Blog: ${blog.name} ${action}"),
278        """Format string for the blog email subject.
[9207]279
[16128]280        This is a mini genshi template and it is passed the blog_post and
281        action objects.
282        """)
[7637]283
[16128]284    # IBlogChangeListener methods
[7637]285    def blog_post_changed(self, postname, version):
[7661]286        """Called when a new blog post 'postname' with 'version' is added.
287
[9207]288        version==1 denotes a new post, version>1 is a new version on existing
[7661]289        post.
290        """
[7637]291        blog_post = BlogPost(self.env, postname, version)
292        action = 'post created'
293        if version > 1:
[9207]294            action = 'post changed'
[7637]295        announcer = AnnouncementSystem(self.env)
296        announcer.send(
297            BlogChangeEvent(
[9207]298                blog_post,
299                action,
[7637]300                self.env.abs_href.blog(blog_post.name)
301            )
302        )
303
304    def blog_post_deleted(self, postname, version, fields):
305        """Called when a blog post is deleted:
[7661]306
[7637]307        version==0 means all versions (or last remaining) version is deleted.
308        Any version>0 denotes a specific version only.
309        Fields is a dict with the pre-existing values of the blog post.
[9207]310        If all (or last) the dict will contain the 'current' version
[7661]311        contents.
312        """
[7637]313        blog_post = BlogPost(self.env, postname, version)
314        announcer = AnnouncementSystem(self.env)
315        announcer.send(
316            BlogChangeEvent(
[9207]317                blog_post,
318                'post deleted',
[7637]319                self.env.abs_href.blog(blog_post.name)
320            )
321        )
322
323    def blog_comment_added(self, postname, number):
324        """Called when Blog comment number N on post 'postname' is added."""
325        blog_post = BlogPost(self.env, postname, 0)
326        blog_comment = BlogComment(self.env, postname, number)
327        announcer = AnnouncementSystem(self.env)
328        announcer.send(
329            BlogChangeEvent(
[9207]330                blog_post,
331                'comment created',
[7637]332                self.env.abs_href.blog(blog_post.name),
333                blog_comment
334            )
335        )
336
337    def blog_comment_deleted(self, postname, number, fields):
338        """Called when blog post comment 'number' is deleted.
[7661]339
[7637]340        number==0 denotes all comments is deleted and fields will be empty.
[9207]341        (usually follows a delete of the blog post).
342
[16128]343        number>0 denotes a specific comment is deleted, and fields will
344        contain the values of the fields as they existed pre-delete.
[7661]345        """
[7637]346        blog_post = BlogPost(self.env, postname, 0)
347        announcer = AnnouncementSystem(self.env)
348        announcer.send(
349            BlogChangeEvent(
[9207]350                blog_post,
351                'comment deleted',
[7637]352                self.env.abs_href.blog(blog_post.name),
353                fields
354            )
355        )
356
[16128]357    # IAnnouncementEmailDecorator methods
[7637]358
359    def decorate_message(self, event, message, decorates=None):
360        if event.realm == "blog":
[8960]361            template = NewTextTemplate(self.blog_email_subject.encode('utf8'))
[7637]362            subject = template.generate(
[9207]363                blog=event.blog_post,
[7637]364                action=event.category
[8960]365            ).render('text', encoding=None)
[9207]366            set_header(message, 'Subject', subject)
[7637]367        return next_decorator(event, message, decorates)
368
[16128]369    # IAnnouncementFormatter methods
370
[7637]371    def styles(self, transport, realm):
372        if realm == 'blog':
373            yield 'text/plain'
374
375    def alternative_style_for(self, transport, realm, style):
376        if realm == 'blog' and style != 'text/plain':
377            return 'text/plain'
378
379    def format(self, transport, realm, style, event):
380        if realm == 'blog' and style == 'text/plain':
381            return self._format_plaintext(event)
382
383    def _format_plaintext(self, event):
384        blog_post = event.blog_post
385        data = dict(
[16128]386            name=blog_post.name,
387            author=event.author,
388            time=event.timestamp,
389            category=event.category,
390            version=event.version,
391            link=event.remote_addr,
392            title=blog_post.title,
393            body=blog_post.body,
394            comment=event.comment,
[7637]395        )
396        chrome = Chrome(self.env)
397        dirs = []
398        for provider in chrome.template_providers:
399            dirs += provider.get_templates_dirs()
400        templates = TemplateLoader(dirs, variable_lookup='lenient')
401        template = templates.load(
402            'fullblog_plaintext.txt',
403            cls=NewTextTemplate
404        )
405        if template:
406            stream = template.generate(**data)
[16128]407            return stream.render('text')
Note: See TracBrowser for help on using the repository browser.