|
| 1 | +from trac.attachment import Attachment |
| 2 | +from trac.config import BoolOption |
| 3 | +from trac.core import Component, implements |
| 4 | +from trac.notification import NotifyEmail |
| 5 | +from trac.resource import ResourceNotFound |
| 6 | +from trac.versioncontrol import RepositoryManager, NoSuchChangeset |
| 7 | +from code_comments.api import ICodeCommentChangeListener |
| 8 | +from code_comments.comments import Comments |
| 9 | + |
| 10 | + |
| 11 | +class CodeCommentChangeListener(Component): |
| 12 | + """ |
| 13 | + Sends email notifications when comments have been created. |
| 14 | + """ |
| 15 | + implements(ICodeCommentChangeListener) |
| 16 | + |
| 17 | + # ICodeCommentChangeListener methods |
| 18 | + |
| 19 | + def comment_created(self, comment): |
| 20 | + notifier = CodeCommentNotifyEmail(self.env) |
| 21 | + notifier.notify(comment) |
| 22 | + |
| 23 | + |
| 24 | +class CodeCommentNotifyEmail(NotifyEmail): |
| 25 | + """ |
| 26 | + Sends code comment notifications by email. |
| 27 | + """ |
| 28 | + |
| 29 | + notify_self = BoolOption('code_comments', 'notify_self', False, |
| 30 | + doc="Send comment notifications to the author of " |
| 31 | + "the comment.") |
| 32 | + |
| 33 | + template_name = "code_comment_notify_email.txt" |
| 34 | + from_email = "trac+comments@localhost" |
| 35 | + |
| 36 | + def _get_attachment_author(self, parent, parent_id, filename): |
| 37 | + """ |
| 38 | + Returns the author of a given attachment. |
| 39 | + """ |
| 40 | + try: |
| 41 | + attachment = Attachment(self.env, parent, parent_id, filename) |
| 42 | + return attachment.author |
| 43 | + except ResourceNotFound: |
| 44 | + self.env.log.debug("Invalid attachment, unable to determine " |
| 45 | + "author.") |
| 46 | + |
| 47 | + def _get_changeset_author(self, revision, reponame=None): |
| 48 | + """ |
| 49 | + Returns the author of a changeset for a given revision. |
| 50 | + """ |
| 51 | + try: |
| 52 | + repos = RepositoryManager(self.env).get_repository(reponame) |
| 53 | + changeset = repos.get_changeset(revision) |
| 54 | + return changeset.author |
| 55 | + except NoSuchChangeset: |
| 56 | + self.env.log.debug("Invalid changeset, unable to determine author") |
| 57 | + |
| 58 | + def _get_original_author(self, comment): |
| 59 | + """ |
| 60 | + Returns the author for the target of a given comment. |
| 61 | + """ |
| 62 | + if comment.type == 'attachment': |
| 63 | + parent, parent_id, filename = comment.path.split("/")[1:] |
| 64 | + return self._get_attachment_author(parent, parent_id, |
| 65 | + filename) |
| 66 | + elif (comment.type == 'changeset' or comment.type == "browser"): |
| 67 | + # TODO: When support is added for multiple repositories, this |
| 68 | + # will need updated |
| 69 | + return self._get_changeset_author(comment.revision) |
| 70 | + |
| 71 | + def _get_comment_thread(self, comment): |
| 72 | + """ |
| 73 | + Returns all comments in the same location as a given comment, sorted |
| 74 | + in order of id. |
| 75 | + """ |
| 76 | + comments = Comments(None, self.env) |
| 77 | + args = {'type': comment.type, |
| 78 | + 'revision': comment.revision, |
| 79 | + 'path': comment.path, |
| 80 | + 'line': comment.line} |
| 81 | + return comments.search(args, order_by='id') |
| 82 | + |
| 83 | + def _get_commenters(self, comment): |
| 84 | + """ |
| 85 | + Returns a list of all commenters for the same thing. |
| 86 | + """ |
| 87 | + comments = Comments(None, self.env) |
| 88 | + args = {'type': comment.type, |
| 89 | + 'revision': comment.revision, |
| 90 | + 'path': comment.path} |
| 91 | + return comments.get_all_comment_authors(comments.search(args)) |
| 92 | + |
| 93 | + def get_recipients(self, comment): |
| 94 | + """ |
| 95 | + Determine who should receive the notification. |
| 96 | +
|
| 97 | + Required by NotifyEmail. |
| 98 | +
|
| 99 | + Current scheme is as follows: |
| 100 | +
|
| 101 | + * For the first comment in a given location, the notification is sent |
| 102 | + 'to' the original author of the thing being commented on, and 'copied' |
| 103 | + to the authors of any other comments on that thing |
| 104 | + * For any further comments in a given location, the notification is |
| 105 | + sent 'to' the author of the last comment in that location, and |
| 106 | + 'copied' to both the original author of the thing and the authors of |
| 107 | + any other comments on that thing |
| 108 | + """ |
| 109 | + torcpts = set() |
| 110 | + |
| 111 | + # Get the original author |
| 112 | + original_author = self._get_original_author(comment) |
| 113 | + |
| 114 | + # Get other commenters |
| 115 | + ccrcpts = set(self._get_commenters(comment)) |
| 116 | + |
| 117 | + # Is this a reply, or a new comment? |
| 118 | + thread = self._get_comment_thread(comment) |
| 119 | + if len(thread) > 1: |
| 120 | + # The author of the comment before this one |
| 121 | + torcpts.add(thread[-2].author) |
| 122 | + # Copy to the original author |
| 123 | + ccrcpts.add(original_author) |
| 124 | + else: |
| 125 | + # This is the first comment in this thread |
| 126 | + torcpts.add(original_author) |
| 127 | + |
| 128 | + # Should we notify the comment author? |
| 129 | + if not self.notify_self: |
| 130 | + torcpts = torcpts.difference([comment.author]) |
| 131 | + ccrcpts = ccrcpts.difference([comment.author]) |
| 132 | + |
| 133 | + # Remove duplicates |
| 134 | + ccrcpts = ccrcpts.difference(torcpts) |
| 135 | + |
| 136 | + self.env.log.debug("Sending notification to: %s" % torcpts) |
| 137 | + self.env.log.debug("Copying notification to: %s" % ccrcpts) |
| 138 | + |
| 139 | + return (torcpts, ccrcpts) |
| 140 | + |
| 141 | + def notify(self, comment): |
| 142 | + from_name = comment.author |
| 143 | + |
| 144 | + # See if we can get a real name for the comment author |
| 145 | + for username, name, email in self.env.get_known_users(): |
| 146 | + if username == comment.author and name: |
| 147 | + from_name = name |
| 148 | + |
| 149 | + self.data.update({ |
| 150 | + "comment": comment, |
| 151 | + }) |
| 152 | + |
| 153 | + projname = self.config.get("project", "name") |
| 154 | + subject = "Re: [%s] %s" % (projname, comment.link_text()) |
| 155 | + |
| 156 | + # Temporarily switch the smtp_from_name setting so we can pretend |
| 157 | + # the mail came from the author of the comment |
| 158 | + try: |
| 159 | + self.env.log.debug("Changing smtp_from_name to %s" % from_name) |
| 160 | + old_setting = self.config['notification'].get('smtp_from_name') |
| 161 | + self.config.set('notification', 'smtp_from_name', from_name) |
| 162 | + try: |
| 163 | + NotifyEmail.notify(self, comment, subject) |
| 164 | + except: |
| 165 | + pass |
| 166 | + finally: |
| 167 | + self.env.log.debug("Changing smtp_from_name back to %s" % |
| 168 | + old_setting) |
| 169 | + self.config.set('notification', 'smtp_from_name', old_setting) |
0 commit comments