Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the new Notification system and avoid using NotifyEmail #88

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 214 additions & 64 deletions code_comments/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,167 @@

from trac.config import BoolOption
from trac.core import Component, implements
from trac.notification import NotifyEmail
from trac.notification.api import NotificationEvent, NotificationSystem, INotificationSubscriber, INotificationFormatter, IEmailDecorator
from trac.notification.mail import RecipientMatcher, set_header
from trac.notification.model import Subscription
from trac.web.chrome import Chrome
from trac.util.datefmt import datetime_now, utc

from code_comments.api import ICodeCommentChangeListener
from code_comments.comments import Comments
from code_comments.subscription import Subscription
from code_comments.subscription import Subscription as CcSubscription

CODE_COMMENT_REALM = 'code-comment'


class CodeCommentChangeEvent(NotificationEvent):
"""
This class represents a notification event for a code-comment.
"""

def __init__(self, category, comment):
super(CodeCommentChangeEvent, self).__init__(
CODE_COMMENT_REALM,
category,
comment,
time=datetime_now(utc),
author=comment.author,
)


class CodeCommentChangeListener(Component):
"""
Sends email notifications when comments have been created.
Publishes notifications when comments have been created.
"""
implements(ICodeCommentChangeListener)

# ICodeCommentChangeListener methods

def comment_created(self, comment):
notifier = CodeCommentNotifyEmail(self.env)
notifier.notify(comment)
event = CodeCommentChangeEvent('created', comment)
NotificationSystem(self.env).notify(event)


class _CodeCommentNotificationSubscriberMixin(object):
"""
This mixin class defines useful methods for all classes implementing INotificationSubscriber.
"""

def _get_default_subscriptions(self, recipient):
sid, auth, addr = recipient
return [
(s[0], s[1], sid, auth, addr, s[2], s[3], s[4])
for s in self.default_subscriptions()
]

def _get_existing_subscriptions(self, sids):
klass = self.__class__.__name__
return [
s.subscription_tuple()
for s in Subscription.find_by_sids_and_class(self.env, sids, klass)
]


class CodeCommentNotifyEmail(NotifyEmail):
class CodeCommentNotificationSubscriberSelf(_CodeCommentNotificationSubscriberMixin, Component):
"""
Sends code comment notifications by email.
Allows to block notifications for the users own code-comments.
"""

implements(INotificationSubscriber)

notify_self = BoolOption('code_comments', 'notify_self', False,
doc="Send comment notifications to the author of "
"the comment.")

template_name = "code_comment_notify_email.txt"
from_email = "trac+comments@localhost"
def matches(self, event):
if event.realm != CODE_COMMENT_REALM:
return []

comment = event.target
recipient = RecipientMatcher(self.env).match_recipient(comment.author)
if not recipient:
return []

result = self._get_default_subscriptions(recipient)

sid, auth, _ = recipient
if sid:
result += self._get_existing_subscriptions([(sid, auth)])

return result

def description(self):
return "I make a code-comment"

def requires_authentication(self):
return True

def default_subscriptions(self):
if not self.notify_self:
klass = self.__class__.__name__
return [
(klass, 'email', 'text/plain', 99, 'never'),
]
return []


class CodeCommentNotificationSubscriberSubscribed(_CodeCommentNotificationSubscriberMixin, Component):
"""
Allows to receive notifications for subscribed revisions/files/attachments.
"""

implements(INotificationSubscriber)

def _get_recipients(self, comment):
recipients = set()
for subscription in CcSubscription.for_comment(self.env, comment,
notify=True):
recipients.add(subscription.user)
return recipients

def matches(self, event):
if event.realm != CODE_COMMENT_REALM:
return []

comment = event.target
candidates = self._get_recipients(comment)

result = []
matcher = RecipientMatcher(self.env)
sids = set()
for candidate in candidates:
recipient = matcher.match_recipient(candidate)
if not recipient:
continue

result += self._get_default_subscriptions(recipient)
sid, auth, _ = recipient
if sid:
sids.add((sid, auth))

result += self._get_existing_subscriptions(sids)

return result

def description(self):
return "A code-comment is made on a revision/file/attachment I'm subscribed to"

def requires_authentication(self):
return True

def default_subscriptions(self):
klass = self.__class__.__name__
return [
(klass, 'email', 'text/plain', 100, 'always'),
]


class CodeCommentNotificationSubscriberReply(_CodeCommentNotificationSubscriberMixin, Component):
"""
Allows to receive notifications when a comment is being replied to.
"""

implements(INotificationSubscriber)

def _get_comment_thread(self, comment):
"""
Expand All @@ -46,75 +176,95 @@ def _get_comment_thread(self, comment):
'line': comment.line}
return comments.search(args, order_by='id')

def get_recipients(self, comment):
"""
Determine who should receive the notification.
def matches(self, event):
if event.realm != CODE_COMMENT_REALM:
return []

Required by NotifyEmail.
comment = event.target
thread = self._get_comment_thread(comment)
is_reply = len(thread) > 1
if not is_reply:
return []

Current scheme is as follows:
previous_author = thread[-2].author
recipient = RecipientMatcher(self.env).match_recipient(previous_author)
if not recipient:
return []

* For the first comment in a given location, the notification is sent
to any subscribers to that resource
* For any further comments in a given location, the notification is
sent to the author of the last comment in that location, and any other
subscribers for that resource
"""
torcpts = set()
ccrcpts = set()
result = self._get_default_subscriptions(recipient)

for subscription in Subscription.for_comment(self.env, comment,
notify=True):
torcpts.add(subscription.user)
sid, auth, _ = recipient
if sid:
result += self._get_existing_subscriptions([(sid, auth)])

# Is this a reply, or a new comment?
thread = self._get_comment_thread(comment)
if len(thread) > 1:
# The author of the comment before this one
torcpts.add(thread[-2].author)
return result

# Should we notify the comment author?
if not self.notify_self:
torcpts = torcpts.difference([comment.author])
ccrcpts = ccrcpts.difference([comment.author])
def description(self):
return "A code-comment is made as a reply to (directly following) one of my own code-comments"

# Remove duplicates
ccrcpts = ccrcpts.difference(torcpts)
def requires_authentication(self):
return True

return (torcpts, ccrcpts)
def default_subscriptions(self):
klass = self.__class__.__name__
return [
(klass, 'email', 'text/plain', 100, 'always'),
]

def _get_author_name(self, comment):
"""
Get the real name of the user who made the comment. If it cannot be
determined, return their username.
"""
for username, name, email in self.env.get_known_users():
if username == comment.author and name:
return name

return comment.author
class CodeCommentNotificationFormatter(Component):
"""
Provides body and email headers for code-comment notifications.
"""

def notify(self, comment):
self.comment_author = self._get_author_name(comment)
implements(INotificationFormatter, IEmailDecorator)

self.data.update({
"comment": comment,
"comment_url": self.env.abs_href() + comment.href(),
"project_url": self.env.project_url or self.env.abs_href(),
})
template_name = "code_comment_notify_email.txt"

# IEmailDecorator methods

def decorate_message(self, event, message, charset):
if event.realm != CODE_COMMENT_REALM:
return

comment = event.target

reply_to = RecipientMatcher(self.env).match_from_author(comment.author)
if reply_to:
set_header(message, 'Reply-To', reply_to, charset)

sender_address = NotificationSystem(self.env).smtp_from
if sender_address:
set_header(message, 'From', (reply_to[0], sender_address), charset)

projname = self.config.get("project", "name")
subject = "Re: [%s] %s" % (projname, comment.link_text())
set_header(message, 'Subject', subject, charset)

try:
NotifyEmail.notify(self, comment, subject)
except Exception, e:
self.env.log.error("Failure sending notification on creation of "
"comment #%d: %s", comment.id, e)
# INotificationFormatter methods

def send(self, torcpts, ccrcpts):
"""
Override NotifyEmail.send() so we can provide from_name.
"""
self.from_name = self.comment_author
NotifyEmail.send(self, torcpts, ccrcpts)
def get_supported_styles(self, transport):
yield 'text/plain', CODE_COMMENT_REALM

def format(self, transport, style, event):
if event.realm != CODE_COMMENT_REALM:
return

comment = event.target
chrome = Chrome(self.env)

template, data = chrome.prepare_template(
req=None,
filename=self.template_name,
data=None,
text=True,
)

data.update({
"comment": comment,
"comment_url": self.env.abs_href() + comment.href(),
"project_url": self.env.project_url or self.env.abs_href(),
})

body = chrome.render_template_string(template, data, text=True)
return body.encode('utf-8')