##// END OF EJS Templates
notifications: don't rely on template context variable in notification model.
marcink -
r1319:6ba85770 default
parent child Browse files
Show More
@@ -1,367 +1,376 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Model for notifications
23 Model for notifications
24 """
24 """
25
25
26
26
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from pylons import tmpl_context as c
31 from pylons.i18n.translation import _, ungettext
30 from pylons.i18n.translation import _, ungettext
32 from sqlalchemy.sql.expression import false, true
31 from sqlalchemy.sql.expression import false, true
33 from mako import exceptions
32 from mako import exceptions
34
33
35 import rhodecode
34 import rhodecode
36 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
37 from rhodecode.lib.utils import PartialRenderer
36 from rhodecode.lib.utils import PartialRenderer
38 from rhodecode.model import BaseModel
37 from rhodecode.model import BaseModel
39 from rhodecode.model.db import Notification, User, UserNotification
38 from rhodecode.model.db import Notification, User, UserNotification
40 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
41 from rhodecode.model.settings import SettingsModel
40 from rhodecode.model.settings import SettingsModel
42
41
43 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
44
43
45
44
46 class NotificationModel(BaseModel):
45 class NotificationModel(BaseModel):
47
46
48 cls = Notification
47 cls = Notification
49
48
50 def __get_notification(self, notification):
49 def __get_notification(self, notification):
51 if isinstance(notification, Notification):
50 if isinstance(notification, Notification):
52 return notification
51 return notification
53 elif isinstance(notification, (int, long)):
52 elif isinstance(notification, (int, long)):
54 return Notification.get(notification)
53 return Notification.get(notification)
55 else:
54 else:
56 if notification:
55 if notification:
57 raise Exception('notification must be int, long or Instance'
56 raise Exception('notification must be int, long or Instance'
58 ' of Notification got %s' % type(notification))
57 ' of Notification got %s' % type(notification))
59
58
60 def create(
59 def create(
61 self, created_by, notification_subject, notification_body,
60 self, created_by, notification_subject, notification_body,
62 notification_type=Notification.TYPE_MESSAGE, recipients=None,
61 notification_type=Notification.TYPE_MESSAGE, recipients=None,
63 mention_recipients=None, with_email=True, email_kwargs=None):
62 mention_recipients=None, with_email=True, email_kwargs=None):
64 """
63 """
65
64
66 Creates notification of given type
65 Creates notification of given type
67
66
68 :param created_by: int, str or User instance. User who created this
67 :param created_by: int, str or User instance. User who created this
69 notification
68 notification
70 :param notification_subject: subject of notification itself
69 :param notification_subject: subject of notification itself
71 :param notification_body: body of notification text
70 :param notification_body: body of notification text
72 :param notification_type: type of notification, based on that we
71 :param notification_type: type of notification, based on that we
73 pick templates
72 pick templates
74
73
75 :param recipients: list of int, str or User objects, when None
74 :param recipients: list of int, str or User objects, when None
76 is given send to all admins
75 is given send to all admins
77 :param mention_recipients: list of int, str or User objects,
76 :param mention_recipients: list of int, str or User objects,
78 that were mentioned
77 that were mentioned
79 :param with_email: send email with this notification
78 :param with_email: send email with this notification
80 :param email_kwargs: dict with arguments to generate email
79 :param email_kwargs: dict with arguments to generate email
81 """
80 """
82
81
83 from rhodecode.lib.celerylib import tasks, run_task
82 from rhodecode.lib.celerylib import tasks, run_task
84
83
85 if recipients and not getattr(recipients, '__iter__', False):
84 if recipients and not getattr(recipients, '__iter__', False):
86 raise Exception('recipients must be an iterable object')
85 raise Exception('recipients must be an iterable object')
87
86
88 created_by_obj = self._get_user(created_by)
87 created_by_obj = self._get_user(created_by)
89 # default MAIN body if not given
88 # default MAIN body if not given
90 email_kwargs = email_kwargs or {'body': notification_body}
89 email_kwargs = email_kwargs or {'body': notification_body}
91 mention_recipients = mention_recipients or set()
90 mention_recipients = mention_recipients or set()
92
91
93 if not created_by_obj:
92 if not created_by_obj:
94 raise Exception('unknown user %s' % created_by)
93 raise Exception('unknown user %s' % created_by)
95
94
96 if recipients is None:
95 if recipients is None:
97 # recipients is None means to all admins
96 # recipients is None means to all admins
98 recipients_objs = User.query().filter(User.admin == true()).all()
97 recipients_objs = User.query().filter(User.admin == true()).all()
99 log.debug('sending notifications %s to admins: %s',
98 log.debug('sending notifications %s to admins: %s',
100 notification_type, recipients_objs)
99 notification_type, recipients_objs)
101 else:
100 else:
102 recipients_objs = []
101 recipients_objs = []
103 for u in recipients:
102 for u in recipients:
104 obj = self._get_user(u)
103 obj = self._get_user(u)
105 if obj:
104 if obj:
106 recipients_objs.append(obj)
105 recipients_objs.append(obj)
107 else: # we didn't find this user, log the error and carry on
106 else: # we didn't find this user, log the error and carry on
108 log.error('cannot notify unknown user %r', u)
107 log.error('cannot notify unknown user %r', u)
109
108
110 recipients_objs = set(recipients_objs)
109 recipients_objs = set(recipients_objs)
111 if not recipients_objs:
110 if not recipients_objs:
112 raise Exception('no valid recipients specified')
111 raise Exception('no valid recipients specified')
113
112
114 log.debug('sending notifications %s to %s',
113 log.debug('sending notifications %s to %s',
115 notification_type, recipients_objs)
114 notification_type, recipients_objs)
116
115
117 # add mentioned users into recipients
116 # add mentioned users into recipients
118 final_recipients = set(recipients_objs).union(mention_recipients)
117 final_recipients = set(recipients_objs).union(mention_recipients)
119 notification = Notification.create(
118 notification = Notification.create(
120 created_by=created_by_obj, subject=notification_subject,
119 created_by=created_by_obj, subject=notification_subject,
121 body=notification_body, recipients=final_recipients,
120 body=notification_body, recipients=final_recipients,
122 type_=notification_type
121 type_=notification_type
123 )
122 )
124
123
125 if not with_email: # skip sending email, and just create notification
124 if not with_email: # skip sending email, and just create notification
126 return notification
125 return notification
127
126
128 # don't send email to person who created this comment
127 # don't send email to person who created this comment
129 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
128 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
130
129
131 # now notify all recipients in question
130 # now notify all recipients in question
132
131
133 for recipient in rec_objs.union(mention_recipients):
132 for recipient in rec_objs.union(mention_recipients):
134 # inject current recipient
133 # inject current recipient
135 email_kwargs['recipient'] = recipient
134 email_kwargs['recipient'] = recipient
136 email_kwargs['mention'] = recipient in mention_recipients
135 email_kwargs['mention'] = recipient in mention_recipients
137 (subject, headers, email_body,
136 (subject, headers, email_body,
138 email_body_plaintext) = EmailNotificationModel().render_email(
137 email_body_plaintext) = EmailNotificationModel().render_email(
139 notification_type, **email_kwargs)
138 notification_type, **email_kwargs)
140
139
141 log.debug(
140 log.debug(
142 'Creating notification email task for user:`%s`', recipient)
141 'Creating notification email task for user:`%s`', recipient)
143 task = run_task(
142 task = run_task(
144 tasks.send_email, recipient.email, subject,
143 tasks.send_email, recipient.email, subject,
145 email_body_plaintext, email_body)
144 email_body_plaintext, email_body)
146 log.debug('Created email task: %s', task)
145 log.debug('Created email task: %s', task)
147
146
148 return notification
147 return notification
149
148
150 def delete(self, user, notification):
149 def delete(self, user, notification):
151 # we don't want to remove actual notification just the assignment
150 # we don't want to remove actual notification just the assignment
152 try:
151 try:
153 notification = self.__get_notification(notification)
152 notification = self.__get_notification(notification)
154 user = self._get_user(user)
153 user = self._get_user(user)
155 if notification and user:
154 if notification and user:
156 obj = UserNotification.query()\
155 obj = UserNotification.query()\
157 .filter(UserNotification.user == user)\
156 .filter(UserNotification.user == user)\
158 .filter(UserNotification.notification == notification)\
157 .filter(UserNotification.notification == notification)\
159 .one()
158 .one()
160 Session().delete(obj)
159 Session().delete(obj)
161 return True
160 return True
162 except Exception:
161 except Exception:
163 log.error(traceback.format_exc())
162 log.error(traceback.format_exc())
164 raise
163 raise
165
164
166 def get_for_user(self, user, filter_=None):
165 def get_for_user(self, user, filter_=None):
167 """
166 """
168 Get mentions for given user, filter them if filter dict is given
167 Get mentions for given user, filter them if filter dict is given
169
168
170 :param user:
169 :param user:
171 :param filter:
170 :param filter:
172 """
171 """
173 user = self._get_user(user)
172 user = self._get_user(user)
174
173
175 q = UserNotification.query()\
174 q = UserNotification.query()\
176 .filter(UserNotification.user == user)\
175 .filter(UserNotification.user == user)\
177 .join((
176 .join((
178 Notification, UserNotification.notification_id ==
177 Notification, UserNotification.notification_id ==
179 Notification.notification_id))
178 Notification.notification_id))
180
179
181 if filter_:
180 if filter_:
182 q = q.filter(Notification.type_.in_(filter_))
181 q = q.filter(Notification.type_.in_(filter_))
183
182
184 return q.all()
183 return q.all()
185
184
186 def mark_read(self, user, notification):
185 def mark_read(self, user, notification):
187 try:
186 try:
188 notification = self.__get_notification(notification)
187 notification = self.__get_notification(notification)
189 user = self._get_user(user)
188 user = self._get_user(user)
190 if notification and user:
189 if notification and user:
191 obj = UserNotification.query()\
190 obj = UserNotification.query()\
192 .filter(UserNotification.user == user)\
191 .filter(UserNotification.user == user)\
193 .filter(UserNotification.notification == notification)\
192 .filter(UserNotification.notification == notification)\
194 .one()
193 .one()
195 obj.read = True
194 obj.read = True
196 Session().add(obj)
195 Session().add(obj)
197 return True
196 return True
198 except Exception:
197 except Exception:
199 log.error(traceback.format_exc())
198 log.error(traceback.format_exc())
200 raise
199 raise
201
200
202 def mark_all_read_for_user(self, user, filter_=None):
201 def mark_all_read_for_user(self, user, filter_=None):
203 user = self._get_user(user)
202 user = self._get_user(user)
204 q = UserNotification.query()\
203 q = UserNotification.query()\
205 .filter(UserNotification.user == user)\
204 .filter(UserNotification.user == user)\
206 .filter(UserNotification.read == false())\
205 .filter(UserNotification.read == false())\
207 .join((
206 .join((
208 Notification, UserNotification.notification_id ==
207 Notification, UserNotification.notification_id ==
209 Notification.notification_id))
208 Notification.notification_id))
210 if filter_:
209 if filter_:
211 q = q.filter(Notification.type_.in_(filter_))
210 q = q.filter(Notification.type_.in_(filter_))
212
211
213 # this is a little inefficient but sqlalchemy doesn't support
212 # this is a little inefficient but sqlalchemy doesn't support
214 # update on joined tables :(
213 # update on joined tables :(
215 for obj in q.all():
214 for obj in q.all():
216 obj.read = True
215 obj.read = True
217 Session().add(obj)
216 Session().add(obj)
218
217
219 def get_unread_cnt_for_user(self, user):
218 def get_unread_cnt_for_user(self, user):
220 user = self._get_user(user)
219 user = self._get_user(user)
221 return UserNotification.query()\
220 return UserNotification.query()\
222 .filter(UserNotification.read == false())\
221 .filter(UserNotification.read == false())\
223 .filter(UserNotification.user == user).count()
222 .filter(UserNotification.user == user).count()
224
223
225 def get_unread_for_user(self, user):
224 def get_unread_for_user(self, user):
226 user = self._get_user(user)
225 user = self._get_user(user)
227 return [x.notification for x in UserNotification.query()
226 return [x.notification for x in UserNotification.query()
228 .filter(UserNotification.read == false())
227 .filter(UserNotification.read == false())
229 .filter(UserNotification.user == user).all()]
228 .filter(UserNotification.user == user).all()]
230
229
231 def get_user_notification(self, user, notification):
230 def get_user_notification(self, user, notification):
232 user = self._get_user(user)
231 user = self._get_user(user)
233 notification = self.__get_notification(notification)
232 notification = self.__get_notification(notification)
234
233
235 return UserNotification.query()\
234 return UserNotification.query()\
236 .filter(UserNotification.notification == notification)\
235 .filter(UserNotification.notification == notification)\
237 .filter(UserNotification.user == user).scalar()
236 .filter(UserNotification.user == user).scalar()
238
237
239 def make_description(self, notification, show_age=True):
238 def make_description(self, notification, show_age=True):
240 """
239 """
241 Creates a human readable description based on properties
240 Creates a human readable description based on properties
242 of notification object
241 of notification object
243 """
242 """
244
243
245 _map = {
244 _map = {
246 notification.TYPE_CHANGESET_COMMENT: [
245 notification.TYPE_CHANGESET_COMMENT: [
247 _('%(user)s commented on commit %(date_or_age)s'),
246 _('%(user)s commented on commit %(date_or_age)s'),
248 _('%(user)s commented on commit at %(date_or_age)s'),
247 _('%(user)s commented on commit at %(date_or_age)s'),
249 ],
248 ],
250 notification.TYPE_MESSAGE: [
249 notification.TYPE_MESSAGE: [
251 _('%(user)s sent message %(date_or_age)s'),
250 _('%(user)s sent message %(date_or_age)s'),
252 _('%(user)s sent message at %(date_or_age)s'),
251 _('%(user)s sent message at %(date_or_age)s'),
253 ],
252 ],
254 notification.TYPE_MENTION: [
253 notification.TYPE_MENTION: [
255 _('%(user)s mentioned you %(date_or_age)s'),
254 _('%(user)s mentioned you %(date_or_age)s'),
256 _('%(user)s mentioned you at %(date_or_age)s'),
255 _('%(user)s mentioned you at %(date_or_age)s'),
257 ],
256 ],
258 notification.TYPE_REGISTRATION: [
257 notification.TYPE_REGISTRATION: [
259 _('%(user)s registered in RhodeCode %(date_or_age)s'),
258 _('%(user)s registered in RhodeCode %(date_or_age)s'),
260 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
259 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
261 ],
260 ],
262 notification.TYPE_PULL_REQUEST: [
261 notification.TYPE_PULL_REQUEST: [
263 _('%(user)s opened new pull request %(date_or_age)s'),
262 _('%(user)s opened new pull request %(date_or_age)s'),
264 _('%(user)s opened new pull request at %(date_or_age)s'),
263 _('%(user)s opened new pull request at %(date_or_age)s'),
265 ],
264 ],
266 notification.TYPE_PULL_REQUEST_COMMENT: [
265 notification.TYPE_PULL_REQUEST_COMMENT: [
267 _('%(user)s commented on pull request %(date_or_age)s'),
266 _('%(user)s commented on pull request %(date_or_age)s'),
268 _('%(user)s commented on pull request at %(date_or_age)s'),
267 _('%(user)s commented on pull request at %(date_or_age)s'),
269 ],
268 ],
270 }
269 }
271
270
272 templates = _map[notification.type_]
271 templates = _map[notification.type_]
273
272
274 if show_age:
273 if show_age:
275 template = templates[0]
274 template = templates[0]
276 date_or_age = h.age(notification.created_on)
275 date_or_age = h.age(notification.created_on)
277 else:
276 else:
278 template = templates[1]
277 template = templates[1]
279 date_or_age = h.format_date(notification.created_on)
278 date_or_age = h.format_date(notification.created_on)
280
279
281 return template % {
280 return template % {
282 'user': notification.created_by_user.username,
281 'user': notification.created_by_user.username,
283 'date_or_age': date_or_age,
282 'date_or_age': date_or_age,
284 }
283 }
285
284
286
285
287 class EmailNotificationModel(BaseModel):
286 class EmailNotificationModel(BaseModel):
288 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
287 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
289 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
288 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
290 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
289 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
291 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
290 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
292 TYPE_MAIN = Notification.TYPE_MESSAGE
291 TYPE_MAIN = Notification.TYPE_MESSAGE
293
292
294 TYPE_PASSWORD_RESET = 'password_reset'
293 TYPE_PASSWORD_RESET = 'password_reset'
295 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
294 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
296 TYPE_EMAIL_TEST = 'email_test'
295 TYPE_EMAIL_TEST = 'email_test'
297 TYPE_TEST = 'test'
296 TYPE_TEST = 'test'
298
297
299 email_types = {
298 email_types = {
300 TYPE_MAIN: 'email_templates/main.mako',
299 TYPE_MAIN: 'email_templates/main.mako',
301 TYPE_TEST: 'email_templates/test.mako',
300 TYPE_TEST: 'email_templates/test.mako',
302 TYPE_EMAIL_TEST: 'email_templates/email_test.mako',
301 TYPE_EMAIL_TEST: 'email_templates/email_test.mako',
303 TYPE_REGISTRATION: 'email_templates/user_registration.mako',
302 TYPE_REGISTRATION: 'email_templates/user_registration.mako',
304 TYPE_PASSWORD_RESET: 'email_templates/password_reset.mako',
303 TYPE_PASSWORD_RESET: 'email_templates/password_reset.mako',
305 TYPE_PASSWORD_RESET_CONFIRMATION: 'email_templates/password_reset_confirmation.mako',
304 TYPE_PASSWORD_RESET_CONFIRMATION: 'email_templates/password_reset_confirmation.mako',
306 TYPE_COMMIT_COMMENT: 'email_templates/commit_comment.mako',
305 TYPE_COMMIT_COMMENT: 'email_templates/commit_comment.mako',
307 TYPE_PULL_REQUEST: 'email_templates/pull_request_review.mako',
306 TYPE_PULL_REQUEST: 'email_templates/pull_request_review.mako',
308 TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.mako',
307 TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.mako',
309 }
308 }
310
309
311 def __init__(self):
310 def __init__(self):
312 """
311 """
313 Example usage::
312 Example usage::
314
313
315 (subject, headers, email_body,
314 (subject, headers, email_body,
316 email_body_plaintext) = EmailNotificationModel().render_email(
315 email_body_plaintext) = EmailNotificationModel().render_email(
317 EmailNotificationModel.TYPE_TEST, **email_kwargs)
316 EmailNotificationModel.TYPE_TEST, **email_kwargs)
318
317
319 """
318 """
320 super(EmailNotificationModel, self).__init__()
319 super(EmailNotificationModel, self).__init__()
320 self.rhodecode_instance_name = None
321
321
322 def _update_kwargs_for_render(self, kwargs):
322 def _update_kwargs_for_render(self, kwargs):
323 """
323 """
324 Inject params required for Mako rendering
324 Inject params required for Mako rendering
325
325
326 :param kwargs:
326 :param kwargs:
327 :return:
327 :return:
328 """
328 """
329 rhodecode_name = self.rhodecode_instance_name
330 if not rhodecode_name:
331 try:
332 rc_config = SettingsModel().get_all_settings()
333 except Exception:
334 log.exception('failed to fetch settings')
335 rc_config = {}
336 rhodecode_name = rc_config.get('rhodecode_title', '')
337 kwargs['rhodecode_instance_name'] = rhodecode_name
338
329 _kwargs = {
339 _kwargs = {
330 'instance_url': h.url('home', qualified=True),
340 'instance_url': h.url('home', qualified=True),
331 'rhodecode_instance_name': getattr(c, 'rhodecode_name', '')
332 }
341 }
333 _kwargs.update(kwargs)
342 _kwargs.update(kwargs)
334 return _kwargs
343 return _kwargs
335
344
336 def get_renderer(self, type_):
345 def get_renderer(self, type_):
337 template_name = self.email_types[type_]
346 template_name = self.email_types[type_]
338 return PartialRenderer(template_name)
347 return PartialRenderer(template_name)
339
348
340 def render_email(self, type_, **kwargs):
349 def render_email(self, type_, **kwargs):
341 """
350 """
342 renders template for email, and returns a tuple of
351 renders template for email, and returns a tuple of
343 (subject, email_headers, email_html_body, email_plaintext_body)
352 (subject, email_headers, email_html_body, email_plaintext_body)
344 """
353 """
345 # translator and helpers inject
354 # translator and helpers inject
346 _kwargs = self._update_kwargs_for_render(kwargs)
355 _kwargs = self._update_kwargs_for_render(kwargs)
347
356
348 email_template = self.get_renderer(type_)
357 email_template = self.get_renderer(type_)
349
358
350 subject = email_template.render('subject', **_kwargs)
359 subject = email_template.render('subject', **_kwargs)
351
360
352 try:
361 try:
353 headers = email_template.render('headers', **_kwargs)
362 headers = email_template.render('headers', **_kwargs)
354 except AttributeError:
363 except AttributeError:
355 # it's not defined in template, ok we can skip it
364 # it's not defined in template, ok we can skip it
356 headers = ''
365 headers = ''
357
366
358 try:
367 try:
359 body_plaintext = email_template.render('body_plaintext', **_kwargs)
368 body_plaintext = email_template.render('body_plaintext', **_kwargs)
360 except AttributeError:
369 except AttributeError:
361 # it's not defined in template, ok we can skip it
370 # it's not defined in template, ok we can skip it
362 body_plaintext = ''
371 body_plaintext = ''
363
372
364 # render WHOLE template
373 # render WHOLE template
365 body = email_template.render(None, **_kwargs)
374 body = email_template.render(None, **_kwargs)
366
375
367 return subject, headers, body, body_plaintext
376 return subject, headers, body, body_plaintext
General Comments 0
You need to be logged in to leave comments. Login now