##// END OF EJS Templates
part2 of pull-request notification improvements
marcink -
r2802:0a623ec2 beta
parent child Browse files
Show More
@@ -0,0 +1,15 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="main.html"/>
3
4 User <b>${pr_comment_user}</b> commented on pull request #${pr_id} for
5 repository ${pr_target_repo}
6
7 <p>
8 ${body}
9
10 %if status_change:
11 <span>New status -> ${status_change}</span>
12 %endif
13 </p>
14
15 View this comment here: ${pr_comment_url}
@@ -1,225 +1,238 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.comment
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 comments model for RhodeCode
7 7
8 8 :created_on: Nov 11, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons.i18n.translation import _
30 30 from sqlalchemy.util.compat import defaultdict
31 31
32 32 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import ChangesetComment, User, Repository, \
36 36 Notification, PullRequest
37 37 from rhodecode.model.notification import NotificationModel
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class ChangesetCommentsModel(BaseModel):
43 43
44 44 cls = ChangesetComment
45 45
46 46 def __get_changeset_comment(self, changeset_comment):
47 47 return self._get_instance(ChangesetComment, changeset_comment)
48 48
49 49 def __get_pull_request(self, pull_request):
50 50 return self._get_instance(PullRequest, pull_request)
51 51
52 52 def _extract_mentions(self, s):
53 53 user_objects = []
54 54 for username in extract_mentioned_users(s):
55 55 user_obj = User.get_by_username(username, case_insensitive=True)
56 56 if user_obj:
57 57 user_objects.append(user_obj)
58 58 return user_objects
59 59
60 60 def create(self, text, repo, user, revision=None, pull_request=None,
61 61 f_path=None, line_no=None, status_change=None):
62 62 """
63 63 Creates new comment for changeset or pull request.
64 64 IF status_change is not none this comment is associated with a
65 65 status change of changeset or changesets associated with pull request
66 66
67 67 :param text:
68 68 :param repo:
69 69 :param user:
70 70 :param revision:
71 71 :param pull_request:
72 72 :param f_path:
73 73 :param line_no:
74 74 :param status_change:
75 75 """
76 76 if not text:
77 77 return
78 78
79 79 repo = self._get_repo(repo)
80 80 user = self._get_user(user)
81 81 comment = ChangesetComment()
82 82 comment.repo = repo
83 83 comment.author = user
84 84 comment.text = text
85 85 comment.f_path = f_path
86 86 comment.line_no = line_no
87 87
88 88 if revision:
89 89 cs = repo.scm_instance.get_changeset(revision)
90 90 desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
91 91 author_email = cs.author_email
92 92 comment.revision = revision
93 93 elif pull_request:
94 94 pull_request = self.__get_pull_request(pull_request)
95 95 comment.pull_request = pull_request
96 96 desc = pull_request.pull_request_id
97 97 else:
98 98 raise Exception('Please specify revision or pull_request_id')
99 99
100 100 self.sa.add(comment)
101 101 self.sa.flush()
102 102
103 103 # make notification
104 104 line = ''
105 105 body = text
106 106
107 107 #changeset
108 108 if revision:
109 109 if line_no:
110 110 line = _('on line %s') % line_no
111 111 subj = safe_unicode(
112 112 h.link_to('Re commit: %(desc)s %(line)s' % \
113 113 {'desc': desc, 'line': line},
114 114 h.url('changeset_home', repo_name=repo.repo_name,
115 115 revision=revision,
116 116 anchor='comment-%s' % comment.comment_id,
117 117 qualified=True,
118 118 )
119 119 )
120 120 )
121 121 notification_type = Notification.TYPE_CHANGESET_COMMENT
122 122 # get the current participants of this changeset
123 123 recipients = ChangesetComment.get_users(revision=revision)
124 124 # add changeset author if it's in rhodecode system
125 125 recipients += [User.get_by_email(author_email)]
126 126 #pull request
127 127 elif pull_request:
128 _url = h.url('pullrequest_show',
129 repo_name=pull_request.other_repo.repo_name,
130 pull_request_id=pull_request.pull_request_id,
131 anchor='comment-%s' % comment.comment_id,
132 qualified=True,
133 )
128 134 subj = safe_unicode(
129 135 h.link_to('Re pull request: %(desc)s %(line)s' % \
130 {'desc': desc, 'line': line},
131 h.url('pullrequest_show',
132 repo_name=pull_request.other_repo.repo_name,
133 pull_request_id=pull_request.pull_request_id,
134 anchor='comment-%s' % comment.comment_id,
135 qualified=True,
136 )
137 )
136 {'desc': desc, 'line': line}, _url)
138 137 )
139 138
140 139 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
141 140 # get the current participants of this pull request
142 141 recipients = ChangesetComment.get_users(pull_request_id=
143 142 pull_request.pull_request_id)
144 143 # add pull request author
145 144 recipients += [pull_request.author]
146 145
146 # add the reviewers to notification
147 recipients += [x.user for x in pull_request.reviewers]
148
149 #set some variables for email notification
150 kwargs = {
151 'pr_id': pull_request.pull_request_id,
152 'status_change': status_change,
153 'pr_comment_url': _url,
154 'pr_comment_user': h.person(user.email),
155 'pr_target_repo': h.url('summary_home',
156 repo_name=pull_request.other_repo.repo_name,
157 qualified=True)
158 }
147 159 # create notification objects, and emails
148 160 NotificationModel().create(
149 created_by=user, subject=subj, body=body,
150 recipients=recipients, type_=notification_type,
151 email_kwargs={'status_change': status_change}
161 created_by=user, subject=subj, body=body,
162 recipients=recipients, type_=notification_type,
163 email_kwargs=kwargs
152 164 )
153 165
154 166 mention_recipients = set(self._extract_mentions(body))\
155 167 .difference(recipients)
156 168 if mention_recipients:
169 kwargs.update({'pr_mention': True})
157 170 subj = _('[Mention]') + ' ' + subj
158 171 NotificationModel().create(
159 172 created_by=user, subject=subj, body=body,
160 173 recipients=mention_recipients,
161 174 type_=notification_type,
162 email_kwargs={'status_change': status_change}
175 email_kwargs=kwargs
163 176 )
164 177
165 178 return comment
166 179
167 180 def delete(self, comment):
168 181 """
169 182 Deletes given comment
170 183
171 184 :param comment_id:
172 185 """
173 186 comment = self.__get_changeset_comment(comment)
174 187 self.sa.delete(comment)
175 188
176 189 return comment
177 190
178 191 def get_comments(self, repo_id, revision=None, pull_request=None):
179 192 """
180 193 Get's main comments based on revision or pull_request_id
181 194
182 195 :param repo_id:
183 196 :type repo_id:
184 197 :param revision:
185 198 :type revision:
186 199 :param pull_request:
187 200 :type pull_request:
188 201 """
189 202
190 203 q = ChangesetComment.query()\
191 204 .filter(ChangesetComment.repo_id == repo_id)\
192 205 .filter(ChangesetComment.line_no == None)\
193 206 .filter(ChangesetComment.f_path == None)
194 207 if revision:
195 208 q = q.filter(ChangesetComment.revision == revision)
196 209 elif pull_request:
197 210 pull_request = self.__get_pull_request(pull_request)
198 211 q = q.filter(ChangesetComment.pull_request == pull_request)
199 212 else:
200 213 raise Exception('Please specify revision or pull_request')
201 214 q = q.order_by(ChangesetComment.created_on)
202 215 return q.all()
203 216
204 217 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
205 218 q = self.sa.query(ChangesetComment)\
206 219 .filter(ChangesetComment.repo_id == repo_id)\
207 220 .filter(ChangesetComment.line_no != None)\
208 221 .filter(ChangesetComment.f_path != None)\
209 222 .order_by(ChangesetComment.comment_id.asc())\
210 223
211 224 if revision:
212 225 q = q.filter(ChangesetComment.revision == revision)
213 226 elif pull_request:
214 227 pull_request = self.__get_pull_request(pull_request)
215 228 q = q.filter(ChangesetComment.pull_request == pull_request)
216 229 else:
217 230 raise Exception('Please specify revision or pull_request_id')
218 231
219 232 comments = q.all()
220 233
221 234 paths = defaultdict(lambda: defaultdict(list))
222 235
223 236 for co in comments:
224 237 paths[co.f_path][co.line_no].append(co)
225 238 return paths.items()
@@ -1,275 +1,277 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.notification
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Model for notifications
7 7
8 8
9 9 :created_on: Nov 20, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30
31 31 from pylons.i18n.translation import _
32 32
33 33 import rhodecode
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import Notification, User, UserNotification
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class NotificationModel(BaseModel):
42 42
43 43 cls = Notification
44 44
45 45 def __get_notification(self, notification):
46 46 if isinstance(notification, Notification):
47 47 return notification
48 48 elif isinstance(notification, (int, long)):
49 49 return Notification.get(notification)
50 50 else:
51 51 if notification:
52 52 raise Exception('notification must be int, long or Instance'
53 53 ' of Notification got %s' % type(notification))
54 54
55 55 def create(self, created_by, subject, body, recipients=None,
56 56 type_=Notification.TYPE_MESSAGE, with_email=True,
57 57 email_kwargs={}):
58 58 """
59 59
60 60 Creates notification of given type
61 61
62 62 :param created_by: int, str or User instance. User who created this
63 63 notification
64 64 :param subject:
65 65 :param body:
66 66 :param recipients: list of int, str or User objects, when None
67 67 is given send to all admins
68 68 :param type_: type of notification
69 69 :param with_email: send email with this notification
70 70 :param email_kwargs: additional dict to pass as args to email template
71 71 """
72 72 from rhodecode.lib.celerylib import tasks, run_task
73 73
74 74 if recipients and not getattr(recipients, '__iter__', False):
75 75 raise Exception('recipients must be a list of iterable')
76 76
77 77 created_by_obj = self._get_user(created_by)
78 78
79 79 if recipients:
80 80 recipients_objs = []
81 81 for u in recipients:
82 82 obj = self._get_user(u)
83 83 if obj:
84 84 recipients_objs.append(obj)
85 85 recipients_objs = set(recipients_objs)
86 86 log.debug('sending notifications %s to %s' % (
87 87 type_, recipients_objs)
88 88 )
89 89 else:
90 90 # empty recipients means to all admins
91 91 recipients_objs = User.query().filter(User.admin == True).all()
92 92 log.debug('sending notifications %s to admins: %s' % (
93 93 type_, recipients_objs)
94 94 )
95 95 notif = Notification.create(
96 96 created_by=created_by_obj, subject=subject,
97 97 body=body, recipients=recipients_objs, type_=type_
98 98 )
99 99
100 100 if with_email is False:
101 101 return notif
102 102
103 103 #don't send email to person who created this comment
104 104 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
105 105
106 106 # send email with notification to all other participants
107 107 for rec in rec_objs:
108 108 email_subject = NotificationModel().make_description(notif, False)
109 109 type_ = type_
110 110 email_body = body
111 111 ## this is passed into template
112 112 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
113 113 kwargs.update(email_kwargs)
114 114 email_body_html = EmailNotificationModel()\
115 115 .get_email_tmpl(type_, **kwargs)
116 116
117 117 run_task(tasks.send_email, rec.email, email_subject, email_body,
118 118 email_body_html)
119 119
120 120 return notif
121 121
122 122 def delete(self, user, notification):
123 123 # we don't want to remove actual notification just the assignment
124 124 try:
125 125 notification = self.__get_notification(notification)
126 126 user = self._get_user(user)
127 127 if notification and user:
128 128 obj = UserNotification.query()\
129 129 .filter(UserNotification.user == user)\
130 130 .filter(UserNotification.notification
131 131 == notification)\
132 132 .one()
133 133 self.sa.delete(obj)
134 134 return True
135 135 except Exception:
136 136 log.error(traceback.format_exc())
137 137 raise
138 138
139 139 def get_for_user(self, user, filter_=None):
140 140 """
141 141 Get mentions for given user, filter them if filter dict is given
142 142
143 143 :param user:
144 144 :type user:
145 145 :param filter:
146 146 """
147 147 user = self._get_user(user)
148 148
149 149 q = UserNotification.query()\
150 150 .filter(UserNotification.user == user)\
151 151 .join((Notification, UserNotification.notification_id ==
152 152 Notification.notification_id))
153 153
154 154 if filter_:
155 155 q = q.filter(Notification.type_.in_(filter_))
156 156
157 157 return q.all()
158 158
159 159 def mark_read(self, user, notification):
160 160 try:
161 161 notification = self.__get_notification(notification)
162 162 user = self._get_user(user)
163 163 if notification and user:
164 164 obj = UserNotification.query()\
165 165 .filter(UserNotification.user == user)\
166 166 .filter(UserNotification.notification
167 167 == notification)\
168 168 .one()
169 169 obj.read = True
170 170 self.sa.add(obj)
171 171 return True
172 172 except Exception:
173 173 log.error(traceback.format_exc())
174 174 raise
175 175
176 176 def mark_all_read_for_user(self, user, filter_=None):
177 177 user = self._get_user(user)
178 178 q = UserNotification.query()\
179 179 .filter(UserNotification.user == user)\
180 180 .filter(UserNotification.read == False)\
181 181 .join((Notification, UserNotification.notification_id ==
182 182 Notification.notification_id))
183 183 if filter_:
184 184 q = q.filter(Notification.type_.in_(filter_))
185 185
186 186 # this is a little inefficient but sqlalchemy doesn't support
187 187 # update on joined tables :(
188 188 for obj in q.all():
189 189 obj.read = True
190 190 self.sa.add(obj)
191 191
192 192 def get_unread_cnt_for_user(self, user):
193 193 user = self._get_user(user)
194 194 return UserNotification.query()\
195 195 .filter(UserNotification.read == False)\
196 196 .filter(UserNotification.user == user).count()
197 197
198 198 def get_unread_for_user(self, user):
199 199 user = self._get_user(user)
200 200 return [x.notification for x in UserNotification.query()\
201 201 .filter(UserNotification.read == False)\
202 202 .filter(UserNotification.user == user).all()]
203 203
204 204 def get_user_notification(self, user, notification):
205 205 user = self._get_user(user)
206 206 notification = self.__get_notification(notification)
207 207
208 208 return UserNotification.query()\
209 209 .filter(UserNotification.notification == notification)\
210 210 .filter(UserNotification.user == user).scalar()
211 211
212 212 def make_description(self, notification, show_age=True):
213 213 """
214 214 Creates a human readable description based on properties
215 215 of notification object
216 216 """
217 217 #alias
218 218 _n = notification
219 219 _map = {
220 220 _n.TYPE_CHANGESET_COMMENT: _('commented on commit'),
221 221 _n.TYPE_MESSAGE: _('sent message'),
222 222 _n.TYPE_MENTION: _('mentioned you'),
223 223 _n.TYPE_REGISTRATION: _('registered in RhodeCode'),
224 224 _n.TYPE_PULL_REQUEST: _('opened new pull request'),
225 225 _n.TYPE_PULL_REQUEST_COMMENT: _('commented on pull request')
226 226 }
227 227
228 228 # action == _map string
229 229 tmpl = "%(user)s %(action)s at %(when)s"
230 230 if show_age:
231 231 when = h.age(notification.created_on)
232 232 else:
233 233 when = h.fmt_date(notification.created_on)
234 234
235 235 data = dict(
236 236 user=notification.created_by_user.username,
237 237 action=_map[notification.type_], when=when,
238 238 )
239 239 return tmpl % data
240 240
241 241
242 242 class EmailNotificationModel(BaseModel):
243 243
244 244 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
245 245 TYPE_PASSWORD_RESET = 'passoword_link'
246 246 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
247 247 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
248 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
248 249 TYPE_DEFAULT = 'default'
249 250
250 251 def __init__(self):
251 252 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
252 253 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
253 254
254 255 self.email_types = {
255 256 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
256 257 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
257 258 self.TYPE_REGISTRATION: 'email_templates/registration.html',
258 259 self.TYPE_DEFAULT: 'email_templates/default.html',
259 260 self.TYPE_PULL_REQUEST: 'email_templates/pull_request.html',
261 self.TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.html',
260 262 }
261 263
262 264 def get_email_tmpl(self, type_, **kwargs):
263 265 """
264 266 return generated template for email based on given type
265 267
266 268 :param type_:
267 269 """
268 270
269 271 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
270 272 email_template = self._tmpl_lookup.get_template(base)
271 273 # translator inject
272 274 _kwargs = {'_': _}
273 275 _kwargs.update(kwargs)
274 276 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
275 277 return email_template.render(**_kwargs)
General Comments 0
You need to be logged in to leave comments. Login now