##// END OF EJS Templates
emails: added logic to allow overwriting the default email titles via rcextensions.
marcink -
r4448:824dc51f default
parent child Browse files
Show More
@@ -1,187 +1,203 b''
1 # This code allows override the integrations templates.
1 # Below code examples allows override the integrations templates, or email titles.
2 # Put this into the __init__.py file of rcextensions to override the templates
2 # Append selected parts at the end of the __init__.py file of rcextensions directory
3 # to override the templates
3
4
4
5
5 # EMAIL Integration
6 # EMAIL Integration
6 from rhodecode.integrations import email
7 from rhodecode.integrations import email
7 email.REPO_PUSH_TEMPLATE_HTML = email.Template('''
8 email.REPO_PUSH_TEMPLATE_HTML = email.Template('''
8 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
9 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
9 <html xmlns="http://www.w3.org/1999/xhtml">
10 <html xmlns="http://www.w3.org/1999/xhtml">
10 <head>
11 <head>
11 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
12 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
12 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
13 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
13 <title>${subject}</title>
14 <title>${subject}</title>
14 <style type="text/css">
15 <style type="text/css">
15 /* Based on The MailChimp Reset INLINE: Yes. */
16 /* Based on The MailChimp Reset INLINE: Yes. */
16 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
17 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
17 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
18 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
18 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
19 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
19 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
20 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
20 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
21 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
21 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
22 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
22 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
23 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
23 /* End reset */
24 /* End reset */
24
25
25 /* defaults for images*/
26 /* defaults for images*/
26 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
27 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
27 a img {border:none;}
28 a img {border:none;}
28 .image_fix {display:block;}
29 .image_fix {display:block;}
29
30
30 body {line-height:1.2em;}
31 body {line-height:1.2em;}
31 p {margin: 0 0 20px;}
32 p {margin: 0 0 20px;}
32 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
33 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
33 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
34 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
34 a:focus {outline:none;}
35 a:focus {outline:none;}
35 a:hover {color: #305b91;}
36 a:hover {color: #305b91;}
36 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
37 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
37 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
38 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
38 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
39 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
39 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
40 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
40 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
41 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
41 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
42 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
42 input:focus {outline: 1px solid #979797}
43 input:focus {outline: 1px solid #979797}
43 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
44 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
44 /* Put your iPhone 4g styles in here */
45 /* Put your iPhone 4g styles in here */
45 }
46 }
46
47
47 /* Android targeting */
48 /* Android targeting */
48 @media only screen and (-webkit-device-pixel-ratio:.75){
49 @media only screen and (-webkit-device-pixel-ratio:.75){
49 /* Put CSS for low density (ldpi) Android layouts in here */
50 /* Put CSS for low density (ldpi) Android layouts in here */
50 }
51 }
51 @media only screen and (-webkit-device-pixel-ratio:1){
52 @media only screen and (-webkit-device-pixel-ratio:1){
52 /* Put CSS for medium density (mdpi) Android layouts in here */
53 /* Put CSS for medium density (mdpi) Android layouts in here */
53 }
54 }
54 @media only screen and (-webkit-device-pixel-ratio:1.5){
55 @media only screen and (-webkit-device-pixel-ratio:1.5){
55 /* Put CSS for high density (hdpi) Android layouts in here */
56 /* Put CSS for high density (hdpi) Android layouts in here */
56 }
57 }
57 /* end Android targeting */
58 /* end Android targeting */
58
59
59 </style>
60 </style>
60
61
61 <!-- Targeting Windows Mobile -->
62 <!-- Targeting Windows Mobile -->
62 <!--[if IEMobile 7]>
63 <!--[if IEMobile 7]>
63 <style type="text/css">
64 <style type="text/css">
64
65
65 </style>
66 </style>
66 <![endif]-->
67 <![endif]-->
67
68
68 <!--[if gte mso 9]>
69 <!--[if gte mso 9]>
69 <style>
70 <style>
70 /* Target Outlook 2007 and 2010 */
71 /* Target Outlook 2007 and 2010 */
71 </style>
72 </style>
72 <![endif]-->
73 <![endif]-->
73 </head>
74 </head>
74 <body>
75 <body>
75 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
76 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
76 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
77 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
77 <tr>
78 <tr>
78 <td valign="top" style="padding:0;">
79 <td valign="top" style="padding:0;">
79 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
80 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
80 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
81 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
81 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
82 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
82 ${'RhodeCode'}
83 ${'RhodeCode'}
83 </a>
84 </a>
84 </td></tr>
85 </td></tr>
85 <tr>
86 <tr>
86 <td style="padding:15px;" valign="top">
87 <td style="padding:15px;" valign="top">
87 % if data['push']['commits']:
88 % if data['push']['commits']:
88 % for commit in data['push']['commits']:
89 % for commit in data['push']['commits']:
89 <a href="${commit['url']}">${commit['short_id']}</a> by ${commit['author']} at ${commit['date']} <br/>
90 <a href="${commit['url']}">${commit['short_id']}</a> by ${commit['author']} at ${commit['date']} <br/>
90 ${commit['message_html']} <br/>
91 ${commit['message_html']} <br/>
91 <br/>
92 <br/>
92 % endfor
93 % endfor
93 % else:
94 % else:
94 No commit data
95 No commit data
95 % endif
96 % endif
96 </td>
97 </td>
97 </tr>
98 </tr>
98 </table>
99 </table>
99 </td>
100 </td>
100 </tr>
101 </tr>
101 </table>
102 </table>
102 <!-- End of wrapper table -->
103 <!-- End of wrapper table -->
103 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;color:#666666;text-decoration:none;" href="${instance_url}">
104 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;color:#666666;text-decoration:none;" href="${instance_url}">
104 ${'This is a notification from RhodeCode. %(instance_url)s' % {'instance_url': instance_url}}
105 ${'This is a notification from RhodeCode. %(instance_url)s' % {'instance_url': instance_url}}
105 </a></p>
106 </a></p>
106 </body>
107 </body>
107 </html>
108 </html>
108 ''')
109 ''')
109
110
110
111
111 # JIRA Integration (EE ONLY)
112 # JIRA Integration (EE ONLY)
112 # available variables:
113 # available variables:
113 # url, short_id ,author
114 # url, short_id ,author
114 # branch, commit_message
115 # branch, commit_message
115 # commit (dict data for commit)
116 # commit (dict data for commit)
116 from rc_integrations import jira_tracker
117 from rc_integrations import jira_tracker
117
118
118 # used for references issues without transition, e.g `This ticket references PROJ-123`
119 # used for references issues without transition, e.g `This ticket references PROJ-123`
119 jira_tracker.COMMENT_TEMPLATE_COMMIT = jira_tracker.Template('''
120 jira_tracker.COMMENT_TEMPLATE_COMMIT = jira_tracker.Template('''
120 Commit `${short_id}` by ${author} on `${branch}` branch references this issue. \n
121 Commit `${short_id}` by ${author} on `${branch}` branch references this issue. \n
121 ${url}\n
122 ${url}\n
122
123
123 ## MODIFICATION add custom COMMIT message to the comment
124 ## MODIFICATION add custom COMMIT message to the comment
124 ${commit['message']}
125 ${commit['message']}
125 ''')
126 ''')
126
127
127 # used when there's a transition, e.g referenced issues status goes from
128 # used when there's a transition, e.g referenced issues status goes from
128 # open to resolved this is used in correlation with something like `closes PROJ-123`
129 # open to resolved this is used in correlation with something like `closes PROJ-123`
129 jira_tracker.COMMENT_TEMPLATE_COMMIT_WITH_STATUS = jira_tracker.Template('''
130 jira_tracker.COMMENT_TEMPLATE_COMMIT_WITH_STATUS = jira_tracker.Template('''
130 Commit `${short_id}` by ${author} on `${branch}` branch changed this issue. \n
131 Commit `${short_id}` by ${author} on `${branch}` branch changed this issue. \n
131 '{url}\n
132 '{url}\n
132
133
133 ## MODIFICATION add custom COMMIT message to the comment
134 ## MODIFICATION add custom COMMIT message to the comment
134 ${commit['message']}
135 ${commit['message']}
135 ''')
136 ''')
136
137
137 jira_tracker.COMMENT_TEMPLATE_PULL_REQUEST = jira_tracker.Template('''
138 jira_tracker.COMMENT_TEMPLATE_PULL_REQUEST = jira_tracker.Template('''
138 ${action} by ${author} (status: ${status}). \n
139 ${action} by ${author} (status: ${status}). \n
139 pull-request: ${url}
140 pull-request: ${url}
140 ''')
141 ''')
141
142
142
143
143 # REDMINE (EE ONLY)
144 # REDMINE (EE ONLY)
144 # available variables:
145 # available variables:
145 # url, short_id ,author
146 # url, short_id ,author
146 # branch, commit_message
147 # branch, commit_message
147 # commit (dict data for commit)
148 # commit (dict data for commit)
148 from rc_integrations import redmine_tracker
149 from rc_integrations import redmine_tracker
149
150
150 # used for references issues without transition, e.g `This ticket references #123`
151 # used for references issues without transition, e.g `This ticket references #123`
151 redmine_tracker.COMMENT_TEMPLATE_COMMIT = redmine_tracker.Template('''
152 redmine_tracker.COMMENT_TEMPLATE_COMMIT = redmine_tracker.Template('''
152 Commit `${short_id}` by ${author} on `${branch}` branch references this issue. \n
153 Commit `${short_id}` by ${author} on `${branch}` branch references this issue. \n
153 commit: ${url}\n
154 commit: ${url}\n
154
155
155 ## MODIFICATION add custom COMMIT message to the comment
156 ## MODIFICATION add custom COMMIT message to the comment
156 message:
157 message:
157 ```
158 ```
158 ${commit['message']}
159 ${commit['message']}
159 ```
160 ```
160
161
161 ''')
162 ''')
162
163
163 # used when there's a transition, e.g referenced issues status goes from
164 # used when there's a transition, e.g referenced issues status goes from
164 # open to resolved this is used in correlation with something like `closes #123`
165 # open to resolved this is used in correlation with something like `closes #123`
165 redmine_tracker.COMMENT_TEMPLATE_COMMIT_WITH_STATUS = redmine_tracker.Template('''
166 redmine_tracker.COMMENT_TEMPLATE_COMMIT_WITH_STATUS = redmine_tracker.Template('''
166 Commit `${short_id}` by ${author} on `${branch}` branch changed this issue. \n
167 Commit `${short_id}` by ${author} on `${branch}` branch changed this issue. \n
167 commit: ${url}\n
168 commit: ${url}\n
168
169
169 ## MODIFICATION add custom COMMIT message to the comment
170 ## MODIFICATION add custom COMMIT message to the comment
170 message:
171 message:
171 ```
172 ```
172 ${commit['message']}
173 ${commit['message']}
173 ```
174 ```
174
175
175 ''')
176 ''')
176
177
177 redmine_tracker.COMMENT_TEMPLATE_PULL_REQUEST = redmine_tracker.Template('''
178 redmine_tracker.COMMENT_TEMPLATE_PULL_REQUEST = redmine_tracker.Template('''
178 ${action} by ${author} (status: ${status}). \n'
179 ${action} by ${author} (status: ${status}). \n'
179 ${url}\n
180 ${url}\n
180
181
181 ## MODIFICATION add custom COMMIT message to the comment
182 ## MODIFICATION add custom COMMIT message to the comment
182 message:
183 message:
183 ```
184 ```
184 ${commit['message']}
185 ${commit['message']}
185 ```
186 ```
186
187
187 ''')
188 ''')
189
190
191 # Example to modify emails default title
192 from rhodecode.model import notification
193
194 notification.EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = '{updating_user} updated pull request. !{pr_id}: "{pr_title}"'
195 notification.EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = '{user} requested a pull request review. !{pr_id}: "{pr_title}"'
196
197 notification.EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"'
198 notification.EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = '{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"'
199 notification.EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"'
200
201 notification.EMAIL_COMMENT_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on commit `{commit_id}`'
202 notification.EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = '{mention_prefix}[status: {status}] {user} left a {comment_type} on commit `{commit_id}`'
203 notification.EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`'
@@ -1,406 +1,427 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 import premailer
29 import premailer
30 from pyramid.threadlocal import get_current_request
30 from pyramid.threadlocal import get_current_request
31 from sqlalchemy.sql.expression import false, true
31 from sqlalchemy.sql.expression import false, true
32
32
33 import rhodecode
33 import rhodecode
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.model import BaseModel
35 from rhodecode.model import BaseModel
36 from rhodecode.model.db import Notification, User, UserNotification
36 from rhodecode.model.db import Notification, User, UserNotification
37 from rhodecode.model.meta import Session
37 from rhodecode.model.meta import Session
38 from rhodecode.translation import TranslationString
38 from rhodecode.translation import TranslationString
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class NotificationModel(BaseModel):
43 class NotificationModel(BaseModel):
44
44
45 cls = Notification
45 cls = Notification
46
46
47 def __get_notification(self, notification):
47 def __get_notification(self, notification):
48 if isinstance(notification, Notification):
48 if isinstance(notification, Notification):
49 return notification
49 return notification
50 elif isinstance(notification, (int, long)):
50 elif isinstance(notification, (int, long)):
51 return Notification.get(notification)
51 return Notification.get(notification)
52 else:
52 else:
53 if notification:
53 if notification:
54 raise Exception('notification must be int, long or Instance'
54 raise Exception('notification must be int, long or Instance'
55 ' of Notification got %s' % type(notification))
55 ' of Notification got %s' % type(notification))
56
56
57 def create(
57 def create(
58 self, created_by, notification_subject, notification_body,
58 self, created_by, notification_subject, notification_body,
59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
60 mention_recipients=None, with_email=True, email_kwargs=None):
60 mention_recipients=None, with_email=True, email_kwargs=None):
61 """
61 """
62
62
63 Creates notification of given type
63 Creates notification of given type
64
64
65 :param created_by: int, str or User instance. User who created this
65 :param created_by: int, str or User instance. User who created this
66 notification
66 notification
67 :param notification_subject: subject of notification itself
67 :param notification_subject: subject of notification itself
68 :param notification_body: body of notification text
68 :param notification_body: body of notification text
69 :param notification_type: type of notification, based on that we
69 :param notification_type: type of notification, based on that we
70 pick templates
70 pick templates
71
71
72 :param recipients: list of int, str or User objects, when None
72 :param recipients: list of int, str or User objects, when None
73 is given send to all admins
73 is given send to all admins
74 :param mention_recipients: list of int, str or User objects,
74 :param mention_recipients: list of int, str or User objects,
75 that were mentioned
75 that were mentioned
76 :param with_email: send email with this notification
76 :param with_email: send email with this notification
77 :param email_kwargs: dict with arguments to generate email
77 :param email_kwargs: dict with arguments to generate email
78 """
78 """
79
79
80 from rhodecode.lib.celerylib import tasks, run_task
80 from rhodecode.lib.celerylib import tasks, run_task
81
81
82 if recipients and not getattr(recipients, '__iter__', False):
82 if recipients and not getattr(recipients, '__iter__', False):
83 raise Exception('recipients must be an iterable object')
83 raise Exception('recipients must be an iterable object')
84
84
85 created_by_obj = self._get_user(created_by)
85 created_by_obj = self._get_user(created_by)
86 # default MAIN body if not given
86 # default MAIN body if not given
87 email_kwargs = email_kwargs or {'body': notification_body}
87 email_kwargs = email_kwargs or {'body': notification_body}
88 mention_recipients = mention_recipients or set()
88 mention_recipients = mention_recipients or set()
89
89
90 if not created_by_obj:
90 if not created_by_obj:
91 raise Exception('unknown user %s' % created_by)
91 raise Exception('unknown user %s' % created_by)
92
92
93 if recipients is None:
93 if recipients is None:
94 # recipients is None means to all admins
94 # recipients is None means to all admins
95 recipients_objs = User.query().filter(User.admin == true()).all()
95 recipients_objs = User.query().filter(User.admin == true()).all()
96 log.debug('sending notifications %s to admins: %s',
96 log.debug('sending notifications %s to admins: %s',
97 notification_type, recipients_objs)
97 notification_type, recipients_objs)
98 else:
98 else:
99 recipients_objs = set()
99 recipients_objs = set()
100 for u in recipients:
100 for u in recipients:
101 obj = self._get_user(u)
101 obj = self._get_user(u)
102 if obj:
102 if obj:
103 recipients_objs.add(obj)
103 recipients_objs.add(obj)
104 else: # we didn't find this user, log the error and carry on
104 else: # we didn't find this user, log the error and carry on
105 log.error('cannot notify unknown user %r', u)
105 log.error('cannot notify unknown user %r', u)
106
106
107 if not recipients_objs:
107 if not recipients_objs:
108 raise Exception('no valid recipients specified')
108 raise Exception('no valid recipients specified')
109
109
110 log.debug('sending notifications %s to %s',
110 log.debug('sending notifications %s to %s',
111 notification_type, recipients_objs)
111 notification_type, recipients_objs)
112
112
113 # add mentioned users into recipients
113 # add mentioned users into recipients
114 final_recipients = set(recipients_objs).union(mention_recipients)
114 final_recipients = set(recipients_objs).union(mention_recipients)
115
115
116 notification = Notification.create(
116 notification = Notification.create(
117 created_by=created_by_obj, subject=notification_subject,
117 created_by=created_by_obj, subject=notification_subject,
118 body=notification_body, recipients=final_recipients,
118 body=notification_body, recipients=final_recipients,
119 type_=notification_type
119 type_=notification_type
120 )
120 )
121
121
122 if not with_email: # skip sending email, and just create notification
122 if not with_email: # skip sending email, and just create notification
123 return notification
123 return notification
124
124
125 # don't send email to person who created this comment
125 # don't send email to person who created this comment
126 rec_objs = set(recipients_objs).difference({created_by_obj})
126 rec_objs = set(recipients_objs).difference({created_by_obj})
127
127
128 # now notify all recipients in question
128 # now notify all recipients in question
129
129
130 for recipient in rec_objs.union(mention_recipients):
130 for recipient in rec_objs.union(mention_recipients):
131 # inject current recipient
131 # inject current recipient
132 email_kwargs['recipient'] = recipient
132 email_kwargs['recipient'] = recipient
133 email_kwargs['mention'] = recipient in mention_recipients
133 email_kwargs['mention'] = recipient in mention_recipients
134 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
134 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
135 notification_type, **email_kwargs)
135 notification_type, **email_kwargs)
136
136
137 extra_headers = None
137 extra_headers = None
138 if 'thread_ids' in email_kwargs:
138 if 'thread_ids' in email_kwargs:
139 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
139 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
140
140
141 log.debug('Creating notification email task for user:`%s`', recipient)
141 log.debug('Creating notification email task for user:`%s`', recipient)
142 task = run_task(
142 task = run_task(
143 tasks.send_email, recipient.email, subject,
143 tasks.send_email, recipient.email, subject,
144 email_body_plaintext, email_body, extra_headers=extra_headers)
144 email_body_plaintext, email_body, extra_headers=extra_headers)
145 log.debug('Created email task: %s', task)
145 log.debug('Created email task: %s', task)
146
146
147 return notification
147 return notification
148
148
149 def delete(self, user, notification):
149 def delete(self, user, notification):
150 # we don't want to remove actual notification just the assignment
150 # we don't want to remove actual notification just the assignment
151 try:
151 try:
152 notification = self.__get_notification(notification)
152 notification = self.__get_notification(notification)
153 user = self._get_user(user)
153 user = self._get_user(user)
154 if notification and user:
154 if notification and user:
155 obj = UserNotification.query()\
155 obj = UserNotification.query()\
156 .filter(UserNotification.user == user)\
156 .filter(UserNotification.user == user)\
157 .filter(UserNotification.notification == notification)\
157 .filter(UserNotification.notification == notification)\
158 .one()
158 .one()
159 Session().delete(obj)
159 Session().delete(obj)
160 return True
160 return True
161 except Exception:
161 except Exception:
162 log.error(traceback.format_exc())
162 log.error(traceback.format_exc())
163 raise
163 raise
164
164
165 def get_for_user(self, user, filter_=None):
165 def get_for_user(self, user, filter_=None):
166 """
166 """
167 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
168 """
168 """
169 user = self._get_user(user)
169 user = self._get_user(user)
170
170
171 q = UserNotification.query()\
171 q = UserNotification.query()\
172 .filter(UserNotification.user == user)\
172 .filter(UserNotification.user == user)\
173 .join((
173 .join((
174 Notification, UserNotification.notification_id ==
174 Notification, UserNotification.notification_id ==
175 Notification.notification_id))
175 Notification.notification_id))
176 if filter_ == ['all']:
176 if filter_ == ['all']:
177 q = q # no filter
177 q = q # no filter
178 elif filter_ == ['unread']:
178 elif filter_ == ['unread']:
179 q = q.filter(UserNotification.read == false())
179 q = q.filter(UserNotification.read == false())
180 elif filter_:
180 elif filter_:
181 q = q.filter(Notification.type_.in_(filter_))
181 q = q.filter(Notification.type_.in_(filter_))
182
182
183 return q
183 return q
184
184
185 def mark_read(self, user, notification):
185 def mark_read(self, user, notification):
186 try:
186 try:
187 notification = self.__get_notification(notification)
187 notification = self.__get_notification(notification)
188 user = self._get_user(user)
188 user = self._get_user(user)
189 if notification and user:
189 if notification and user:
190 obj = UserNotification.query()\
190 obj = UserNotification.query()\
191 .filter(UserNotification.user == user)\
191 .filter(UserNotification.user == user)\
192 .filter(UserNotification.notification == notification)\
192 .filter(UserNotification.notification == notification)\
193 .one()
193 .one()
194 obj.read = True
194 obj.read = True
195 Session().add(obj)
195 Session().add(obj)
196 return True
196 return True
197 except Exception:
197 except Exception:
198 log.error(traceback.format_exc())
198 log.error(traceback.format_exc())
199 raise
199 raise
200
200
201 def mark_all_read_for_user(self, user, filter_=None):
201 def mark_all_read_for_user(self, user, filter_=None):
202 user = self._get_user(user)
202 user = self._get_user(user)
203 q = UserNotification.query()\
203 q = UserNotification.query()\
204 .filter(UserNotification.user == user)\
204 .filter(UserNotification.user == user)\
205 .filter(UserNotification.read == false())\
205 .filter(UserNotification.read == false())\
206 .join((
206 .join((
207 Notification, UserNotification.notification_id ==
207 Notification, UserNotification.notification_id ==
208 Notification.notification_id))
208 Notification.notification_id))
209 if filter_ == ['unread']:
209 if filter_ == ['unread']:
210 q = q.filter(UserNotification.read == false())
210 q = q.filter(UserNotification.read == false())
211 elif filter_:
211 elif filter_:
212 q = q.filter(Notification.type_.in_(filter_))
212 q = q.filter(Notification.type_.in_(filter_))
213
213
214 # this is a little inefficient but sqlalchemy doesn't support
214 # this is a little inefficient but sqlalchemy doesn't support
215 # update on joined tables :(
215 # update on joined tables :(
216 for obj in q.all():
216 for obj in q.all():
217 obj.read = True
217 obj.read = True
218 Session().add(obj)
218 Session().add(obj)
219
219
220 def get_unread_cnt_for_user(self, user):
220 def get_unread_cnt_for_user(self, user):
221 user = self._get_user(user)
221 user = self._get_user(user)
222 return UserNotification.query()\
222 return UserNotification.query()\
223 .filter(UserNotification.read == false())\
223 .filter(UserNotification.read == false())\
224 .filter(UserNotification.user == user).count()
224 .filter(UserNotification.user == user).count()
225
225
226 def get_unread_for_user(self, user):
226 def get_unread_for_user(self, user):
227 user = self._get_user(user)
227 user = self._get_user(user)
228 return [x.notification for x in UserNotification.query()
228 return [x.notification for x in UserNotification.query()
229 .filter(UserNotification.read == false())
229 .filter(UserNotification.read == false())
230 .filter(UserNotification.user == user).all()]
230 .filter(UserNotification.user == user).all()]
231
231
232 def get_user_notification(self, user, notification):
232 def get_user_notification(self, user, notification):
233 user = self._get_user(user)
233 user = self._get_user(user)
234 notification = self.__get_notification(notification)
234 notification = self.__get_notification(notification)
235
235
236 return UserNotification.query()\
236 return UserNotification.query()\
237 .filter(UserNotification.notification == notification)\
237 .filter(UserNotification.notification == notification)\
238 .filter(UserNotification.user == user).scalar()
238 .filter(UserNotification.user == user).scalar()
239
239
240 def make_description(self, notification, translate, show_age=True):
240 def make_description(self, notification, translate, show_age=True):
241 """
241 """
242 Creates a human readable description based on properties
242 Creates a human readable description based on properties
243 of notification object
243 of notification object
244 """
244 """
245 _ = translate
245 _ = translate
246 _map = {
246 _map = {
247 notification.TYPE_CHANGESET_COMMENT: [
247 notification.TYPE_CHANGESET_COMMENT: [
248 _('%(user)s commented on commit %(date_or_age)s'),
248 _('%(user)s commented on commit %(date_or_age)s'),
249 _('%(user)s commented on commit at %(date_or_age)s'),
249 _('%(user)s commented on commit at %(date_or_age)s'),
250 ],
250 ],
251 notification.TYPE_MESSAGE: [
251 notification.TYPE_MESSAGE: [
252 _('%(user)s sent message %(date_or_age)s'),
252 _('%(user)s sent message %(date_or_age)s'),
253 _('%(user)s sent message at %(date_or_age)s'),
253 _('%(user)s sent message at %(date_or_age)s'),
254 ],
254 ],
255 notification.TYPE_MENTION: [
255 notification.TYPE_MENTION: [
256 _('%(user)s mentioned you %(date_or_age)s'),
256 _('%(user)s mentioned you %(date_or_age)s'),
257 _('%(user)s mentioned you at %(date_or_age)s'),
257 _('%(user)s mentioned you at %(date_or_age)s'),
258 ],
258 ],
259 notification.TYPE_REGISTRATION: [
259 notification.TYPE_REGISTRATION: [
260 _('%(user)s registered in RhodeCode %(date_or_age)s'),
260 _('%(user)s registered in RhodeCode %(date_or_age)s'),
261 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
261 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
262 ],
262 ],
263 notification.TYPE_PULL_REQUEST: [
263 notification.TYPE_PULL_REQUEST: [
264 _('%(user)s opened new pull request %(date_or_age)s'),
264 _('%(user)s opened new pull request %(date_or_age)s'),
265 _('%(user)s opened new pull request at %(date_or_age)s'),
265 _('%(user)s opened new pull request at %(date_or_age)s'),
266 ],
266 ],
267 notification.TYPE_PULL_REQUEST_UPDATE: [
267 notification.TYPE_PULL_REQUEST_UPDATE: [
268 _('%(user)s updated pull request %(date_or_age)s'),
268 _('%(user)s updated pull request %(date_or_age)s'),
269 _('%(user)s updated pull request at %(date_or_age)s'),
269 _('%(user)s updated pull request at %(date_or_age)s'),
270 ],
270 ],
271 notification.TYPE_PULL_REQUEST_COMMENT: [
271 notification.TYPE_PULL_REQUEST_COMMENT: [
272 _('%(user)s commented on pull request %(date_or_age)s'),
272 _('%(user)s commented on pull request %(date_or_age)s'),
273 _('%(user)s commented on pull request at %(date_or_age)s'),
273 _('%(user)s commented on pull request at %(date_or_age)s'),
274 ],
274 ],
275 }
275 }
276
276
277 templates = _map[notification.type_]
277 templates = _map[notification.type_]
278
278
279 if show_age:
279 if show_age:
280 template = templates[0]
280 template = templates[0]
281 date_or_age = h.age(notification.created_on)
281 date_or_age = h.age(notification.created_on)
282 if translate:
282 if translate:
283 date_or_age = translate(date_or_age)
283 date_or_age = translate(date_or_age)
284
284
285 if isinstance(date_or_age, TranslationString):
285 if isinstance(date_or_age, TranslationString):
286 date_or_age = date_or_age.interpolate()
286 date_or_age = date_or_age.interpolate()
287
287
288 else:
288 else:
289 template = templates[1]
289 template = templates[1]
290 date_or_age = h.format_date(notification.created_on)
290 date_or_age = h.format_date(notification.created_on)
291
291
292 return template % {
292 return template % {
293 'user': notification.created_by_user.username,
293 'user': notification.created_by_user.username,
294 'date_or_age': date_or_age,
294 'date_or_age': date_or_age,
295 }
295 }
296
296
297
297
298 # Templates for Titles, that could be overwritten by rcextensions
299 # Title of email for pull-request update
300 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
301 # Title of email for request for pull request review
302 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
303
304 # Title of email for general comment on pull request
305 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
306 # Title of email for general comment which includes status change on pull request
307 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
308 # Title of email for inline comment on a file in pull request
309 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
310
311 # Title of email for general comment on commit
312 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
313 # Title of email for general comment which includes status change on commit
314 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
315 # Title of email for inline comment on a file in commit
316 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
317
318
298 class EmailNotificationModel(BaseModel):
319 class EmailNotificationModel(BaseModel):
299 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
320 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
300 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
321 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
301 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
322 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
302 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
323 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
303 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
324 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
304 TYPE_MAIN = Notification.TYPE_MESSAGE
325 TYPE_MAIN = Notification.TYPE_MESSAGE
305
326
306 TYPE_PASSWORD_RESET = 'password_reset'
327 TYPE_PASSWORD_RESET = 'password_reset'
307 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
328 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
308 TYPE_EMAIL_TEST = 'email_test'
329 TYPE_EMAIL_TEST = 'email_test'
309 TYPE_EMAIL_EXCEPTION = 'exception'
330 TYPE_EMAIL_EXCEPTION = 'exception'
310 TYPE_TEST = 'test'
331 TYPE_TEST = 'test'
311
332
312 email_types = {
333 email_types = {
313 TYPE_MAIN:
334 TYPE_MAIN:
314 'rhodecode:templates/email_templates/main.mako',
335 'rhodecode:templates/email_templates/main.mako',
315 TYPE_TEST:
336 TYPE_TEST:
316 'rhodecode:templates/email_templates/test.mako',
337 'rhodecode:templates/email_templates/test.mako',
317 TYPE_EMAIL_EXCEPTION:
338 TYPE_EMAIL_EXCEPTION:
318 'rhodecode:templates/email_templates/exception_tracker.mako',
339 'rhodecode:templates/email_templates/exception_tracker.mako',
319 TYPE_EMAIL_TEST:
340 TYPE_EMAIL_TEST:
320 'rhodecode:templates/email_templates/email_test.mako',
341 'rhodecode:templates/email_templates/email_test.mako',
321 TYPE_REGISTRATION:
342 TYPE_REGISTRATION:
322 'rhodecode:templates/email_templates/user_registration.mako',
343 'rhodecode:templates/email_templates/user_registration.mako',
323 TYPE_PASSWORD_RESET:
344 TYPE_PASSWORD_RESET:
324 'rhodecode:templates/email_templates/password_reset.mako',
345 'rhodecode:templates/email_templates/password_reset.mako',
325 TYPE_PASSWORD_RESET_CONFIRMATION:
346 TYPE_PASSWORD_RESET_CONFIRMATION:
326 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
347 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
327 TYPE_COMMIT_COMMENT:
348 TYPE_COMMIT_COMMENT:
328 'rhodecode:templates/email_templates/commit_comment.mako',
349 'rhodecode:templates/email_templates/commit_comment.mako',
329 TYPE_PULL_REQUEST:
350 TYPE_PULL_REQUEST:
330 'rhodecode:templates/email_templates/pull_request_review.mako',
351 'rhodecode:templates/email_templates/pull_request_review.mako',
331 TYPE_PULL_REQUEST_COMMENT:
352 TYPE_PULL_REQUEST_COMMENT:
332 'rhodecode:templates/email_templates/pull_request_comment.mako',
353 'rhodecode:templates/email_templates/pull_request_comment.mako',
333 TYPE_PULL_REQUEST_UPDATE:
354 TYPE_PULL_REQUEST_UPDATE:
334 'rhodecode:templates/email_templates/pull_request_update.mako',
355 'rhodecode:templates/email_templates/pull_request_update.mako',
335 }
356 }
336
357
337 premailer_instance = premailer.Premailer(
358 premailer_instance = premailer.Premailer(
338 cssutils_logging_level=logging.ERROR,
359 cssutils_logging_level=logging.ERROR,
339 cssutils_logging_handler=logging.getLogger().handlers[0]
360 cssutils_logging_handler=logging.getLogger().handlers[0]
340 if logging.getLogger().handlers else None,
361 if logging.getLogger().handlers else None,
341 )
362 )
342
363
343 def __init__(self):
364 def __init__(self):
344 """
365 """
345 Example usage::
366 Example usage::
346
367
347 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
368 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
348 EmailNotificationModel.TYPE_TEST, **email_kwargs)
369 EmailNotificationModel.TYPE_TEST, **email_kwargs)
349
370
350 """
371 """
351 super(EmailNotificationModel, self).__init__()
372 super(EmailNotificationModel, self).__init__()
352 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
373 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
353
374
354 def _update_kwargs_for_render(self, kwargs):
375 def _update_kwargs_for_render(self, kwargs):
355 """
376 """
356 Inject params required for Mako rendering
377 Inject params required for Mako rendering
357
378
358 :param kwargs:
379 :param kwargs:
359 """
380 """
360
381
361 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
382 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
362 kwargs['rhodecode_version'] = rhodecode.__version__
383 kwargs['rhodecode_version'] = rhodecode.__version__
363 instance_url = h.route_url('home')
384 instance_url = h.route_url('home')
364 _kwargs = {
385 _kwargs = {
365 'instance_url': instance_url,
386 'instance_url': instance_url,
366 'whitespace_filter': self.whitespace_filter
387 'whitespace_filter': self.whitespace_filter
367 }
388 }
368 _kwargs.update(kwargs)
389 _kwargs.update(kwargs)
369 return _kwargs
390 return _kwargs
370
391
371 def whitespace_filter(self, text):
392 def whitespace_filter(self, text):
372 return text.replace('\n', '').replace('\t', '')
393 return text.replace('\n', '').replace('\t', '')
373
394
374 def get_renderer(self, type_, request):
395 def get_renderer(self, type_, request):
375 template_name = self.email_types[type_]
396 template_name = self.email_types[type_]
376 return request.get_partial_renderer(template_name)
397 return request.get_partial_renderer(template_name)
377
398
378 def render_email(self, type_, **kwargs):
399 def render_email(self, type_, **kwargs):
379 """
400 """
380 renders template for email, and returns a tuple of
401 renders template for email, and returns a tuple of
381 (subject, email_headers, email_html_body, email_plaintext_body)
402 (subject, email_headers, email_html_body, email_plaintext_body)
382 """
403 """
383 # translator and helpers inject
404 # translator and helpers inject
384 _kwargs = self._update_kwargs_for_render(kwargs)
405 _kwargs = self._update_kwargs_for_render(kwargs)
385 request = get_current_request()
406 request = get_current_request()
386 email_template = self.get_renderer(type_, request=request)
407 email_template = self.get_renderer(type_, request=request)
387
408
388 subject = email_template.render('subject', **_kwargs)
409 subject = email_template.render('subject', **_kwargs)
389
410
390 try:
411 try:
391 body_plaintext = email_template.render('body_plaintext', **_kwargs)
412 body_plaintext = email_template.render('body_plaintext', **_kwargs)
392 except AttributeError:
413 except AttributeError:
393 # it's not defined in template, ok we can skip it
414 # it's not defined in template, ok we can skip it
394 body_plaintext = ''
415 body_plaintext = ''
395
416
396 # render WHOLE template
417 # render WHOLE template
397 body = email_template.render(None, **_kwargs)
418 body = email_template.render(None, **_kwargs)
398
419
399 try:
420 try:
400 # Inline CSS styles and conversion
421 # Inline CSS styles and conversion
401 body = self.premailer_instance.transform(body)
422 body = self.premailer_instance.transform(body)
402 except Exception:
423 except Exception:
403 log.exception('Failed to parse body with premailer')
424 log.exception('Failed to parse body with premailer')
404 pass
425 pass
405
426
406 return subject, body, body_plaintext
427 return subject, body, body_plaintext
@@ -1,173 +1,176 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'repo_name': repo_name,
10 'repo_name': repo_name,
11 'status': status_change,
11 'status': status_change,
12 'comment_file': comment_file,
12 'comment_file': comment_file,
13 'comment_line': comment_line,
13 'comment_line': comment_line,
14 'comment_type': comment_type,
14 'comment_type': comment_type,
15 'comment_id': comment_id,
15 'comment_id': comment_id,
16
16
17 'commit_id': h.show_id(commit),
17 'commit_id': h.show_id(commit),
18 'mention_prefix': '[mention] ' if mention else '',
18 'mention_prefix': '[mention] ' if mention else '',
19 }
19 }
20
21
22 if comment_file:
23 subject_template = email_comment_file_subject_template or \
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}` in the `{repo_name}` repository').format(**data)
25 else:
26 if status_change:
27 subject_template = email_comment_status_change_subject_template or \
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on commit `{commit_id}` in the `{repo_name}` repository').format(**data)
29 else:
30 subject_template = email_comment_subject_template or \
31 _('{mention_prefix}{user} left a {comment_type} on commit `{commit_id}` in the `{repo_name}` repository').format(**data)
20 %>
32 %>
21
33
22
34
23 % if comment_file:
35 ${subject_template.format(**data) |n}
24 ${_('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)} ${_('in the `{repo_name}` repository').format(**data) |n}
25 % else:
26 % if status_change:
27 ${_('{mention_prefix}[status: {status}] {user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
28 % else:
29 ${_('{mention_prefix}{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
30 % endif
31 % endif
32
33 </%def>
36 </%def>
34
37
35 ## PLAINTEXT VERSION OF BODY
38 ## PLAINTEXT VERSION OF BODY
36 <%def name="body_plaintext()" filter="n,trim">
39 <%def name="body_plaintext()" filter="n,trim">
37 <%
40 <%
38 data = {
41 data = {
39 'user': h.person(user),
42 'user': h.person(user),
40 'repo_name': repo_name,
43 'repo_name': repo_name,
41 'status': status_change,
44 'status': status_change,
42 'comment_file': comment_file,
45 'comment_file': comment_file,
43 'comment_line': comment_line,
46 'comment_line': comment_line,
44 'comment_type': comment_type,
47 'comment_type': comment_type,
45 'comment_id': comment_id,
48 'comment_id': comment_id,
46
49
47 'commit_id': h.show_id(commit),
50 'commit_id': h.show_id(commit),
48 }
51 }
49 %>
52 %>
50
53
51 * ${_('Comment link')}: ${commit_comment_url}
54 * ${_('Comment link')}: ${commit_comment_url}
52
55
53 %if status_change:
56 %if status_change:
54 * ${_('Commit status')}: ${_('Status was changed to')}: *${status_change}*
57 * ${_('Commit status')}: ${_('Status was changed to')}: *${status_change}*
55
58
56 %endif
59 %endif
57 * ${_('Commit')}: ${h.show_id(commit)}
60 * ${_('Commit')}: ${h.show_id(commit)}
58
61
59 * ${_('Commit message')}: ${commit.message}
62 * ${_('Commit message')}: ${commit.message}
60
63
61 %if comment_file:
64 %if comment_file:
62 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
65 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
63
66
64 %endif
67 %endif
65 % if comment_type == 'todo':
68 % if comment_type == 'todo':
66 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
69 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
67 % else:
70 % else:
68 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
71 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
69 % endif
72 % endif
70
73
71 ${comment_body |n, trim}
74 ${comment_body |n, trim}
72
75
73 ---
76 ---
74 ${self.plaintext_footer()}
77 ${self.plaintext_footer()}
75 </%def>
78 </%def>
76
79
77
80
78 <%
81 <%
79 data = {
82 data = {
80 'user': h.person(user),
83 'user': h.person(user),
81 'comment_file': comment_file,
84 'comment_file': comment_file,
82 'comment_line': comment_line,
85 'comment_line': comment_line,
83 'comment_type': comment_type,
86 'comment_type': comment_type,
84 'comment_id': comment_id,
87 'comment_id': comment_id,
85 'renderer_type': renderer_type or 'plain',
88 'renderer_type': renderer_type or 'plain',
86
89
87 'repo': commit_target_repo_url,
90 'repo': commit_target_repo_url,
88 'repo_name': repo_name,
91 'repo_name': repo_name,
89 'commit_id': h.show_id(commit),
92 'commit_id': h.show_id(commit),
90 }
93 }
91 %>
94 %>
92
95
93 ## header
96 ## header
94 <table style="text-align:left;vertical-align:middle;width: 100%">
97 <table style="text-align:left;vertical-align:middle;width: 100%">
95 <tr>
98 <tr>
96 <td style="width:100%;border-bottom:1px solid #dbd9da;">
99 <td style="width:100%;border-bottom:1px solid #dbd9da;">
97
100
98 <div style="margin: 0; font-weight: bold">
101 <div style="margin: 0; font-weight: bold">
99 <div class="clear-both" style="margin-bottom: 4px">
102 <div class="clear-both" style="margin-bottom: 4px">
100 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
103 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
101 ${_('left a')}
104 ${_('left a')}
102 <a href="${commit_comment_url}" style="${base.link_css()}">
105 <a href="${commit_comment_url}" style="${base.link_css()}">
103 % if comment_file:
106 % if comment_file:
104 ${_('{comment_type} on file `{comment_file}` in commit.').format(**data)}
107 ${_('{comment_type} on file `{comment_file}` in commit.').format(**data)}
105 % else:
108 % else:
106 ${_('{comment_type} on commit.').format(**data) |n}
109 ${_('{comment_type} on commit.').format(**data) |n}
107 % endif
110 % endif
108 </a>
111 </a>
109 </div>
112 </div>
110 <div style="margin-top: 10px"></div>
113 <div style="margin-top: 10px"></div>
111 ${_('Commit')} <code>${data['commit_id']}</code> ${_('of repository')}: ${data['repo_name']}
114 ${_('Commit')} <code>${data['commit_id']}</code> ${_('of repository')}: ${data['repo_name']}
112 </div>
115 </div>
113
116
114 </td>
117 </td>
115 </tr>
118 </tr>
116
119
117 </table>
120 </table>
118 <div class="clear-both"></div>
121 <div class="clear-both"></div>
119 ## main body
122 ## main body
120 <table style="text-align:left;vertical-align:middle;width: 100%">
123 <table style="text-align:left;vertical-align:middle;width: 100%">
121
124
122 ## spacing def
125 ## spacing def
123 <tr>
126 <tr>
124 <td style="width: 130px"></td>
127 <td style="width: 130px"></td>
125 <td></td>
128 <td></td>
126 </tr>
129 </tr>
127
130
128 % if status_change:
131 % if status_change:
129 <tr>
132 <tr>
130 <td style="padding-right:20px;">${_('Commit Status')}:</td>
133 <td style="padding-right:20px;">${_('Commit Status')}:</td>
131 <td>
134 <td>
132 ${_('Status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}
135 ${_('Status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}
133 </td>
136 </td>
134 </tr>
137 </tr>
135 % endif
138 % endif
136
139
137 <tr>
140 <tr>
138 <td style="padding-right:20px;">${_('Commit')}:</td>
141 <td style="padding-right:20px;">${_('Commit')}:</td>
139 <td>
142 <td>
140 <a href="${commit_comment_url}" style="${base.link_css()}">${h.show_id(commit)}</a>
143 <a href="${commit_comment_url}" style="${base.link_css()}">${h.show_id(commit)}</a>
141 </td>
144 </td>
142 </tr>
145 </tr>
143 <tr>
146 <tr>
144 <td style="padding-right:20px;">${_('Commit message')}:</td>
147 <td style="padding-right:20px;">${_('Commit message')}:</td>
145 <td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td>
148 <td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td>
146 </tr>
149 </tr>
147
150
148 % if comment_file:
151 % if comment_file:
149 <tr>
152 <tr>
150 <td style="padding-right:20px;">${_('File')}:</td>
153 <td style="padding-right:20px;">${_('File')}:</td>
151 <td><a href="${commit_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
154 <td><a href="${commit_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
152 </tr>
155 </tr>
153 % endif
156 % endif
154
157
155 <tr style="border-bottom:1px solid #dbd9da;">
158 <tr style="border-bottom:1px solid #dbd9da;">
156 <td colspan="2" style="padding-right:20px;">
159 <td colspan="2" style="padding-right:20px;">
157 % if comment_type == 'todo':
160 % if comment_type == 'todo':
158 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
161 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
159 % else:
162 % else:
160 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
163 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
161 % endif
164 % endif
162 </td>
165 </td>
163 </tr>
166 </tr>
164
167
165 <tr>
168 <tr>
166 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
169 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
167 </tr>
170 </tr>
168
171
169 <tr>
172 <tr>
170 <td><a href="${commit_comment_reply_url}">${_('Reply')}</a></td>
173 <td><a href="${commit_comment_reply_url}">${_('Reply')}</a></td>
171 <td></td>
174 <td></td>
172 </tr>
175 </tr>
173 </table>
176 </table>
@@ -1,204 +1,206 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'repo_name': repo_name,
10 'repo_name': repo_name,
11 'status': status_change,
11 'status': status_change,
12 'comment_file': comment_file,
12 'comment_file': comment_file,
13 'comment_line': comment_line,
13 'comment_line': comment_line,
14 'comment_type': comment_type,
14 'comment_type': comment_type,
15 'comment_id': comment_id,
15 'comment_id': comment_id,
16
16
17 'pr_title': pull_request.title,
17 'pr_title': pull_request.title,
18 'pr_id': pull_request.pull_request_id,
18 'pr_id': pull_request.pull_request_id,
19 'mention_prefix': '[mention] ' if mention else '',
19 'mention_prefix': '[mention] ' if mention else '',
20 }
20 }
21
22 if comment_file:
23 subject_template = email_pr_comment_file_subject_template or \
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
25 else:
26 if status_change:
27 subject_template = email_pr_comment_status_change_subject_template or \
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
29 else:
30 subject_template = email_pr_comment_subject_template or \
31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
21 %>
32 %>
22
33
23
34
24 % if comment_file:
35 ${subject_template.format(**data) |n}
25 ${_('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data) |n}
26 % else:
27 % if status_change:
28 ${_('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
29 % else:
30 ${_('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
31 % endif
32 % endif
33
34 </%def>
36 </%def>
35
37
36 ## PLAINTEXT VERSION OF BODY
38 ## PLAINTEXT VERSION OF BODY
37 <%def name="body_plaintext()" filter="n,trim">
39 <%def name="body_plaintext()" filter="n,trim">
38 <%
40 <%
39 data = {
41 data = {
40 'user': h.person(user),
42 'user': h.person(user),
41 'repo_name': repo_name,
43 'repo_name': repo_name,
42 'status': status_change,
44 'status': status_change,
43 'comment_file': comment_file,
45 'comment_file': comment_file,
44 'comment_line': comment_line,
46 'comment_line': comment_line,
45 'comment_type': comment_type,
47 'comment_type': comment_type,
46 'comment_id': comment_id,
48 'comment_id': comment_id,
47
49
48 'pr_title': pull_request.title,
50 'pr_title': pull_request.title,
49 'pr_id': pull_request.pull_request_id,
51 'pr_id': pull_request.pull_request_id,
50 'source_ref_type': pull_request.source_ref_parts.type,
52 'source_ref_type': pull_request.source_ref_parts.type,
51 'source_ref_name': pull_request.source_ref_parts.name,
53 'source_ref_name': pull_request.source_ref_parts.name,
52 'target_ref_type': pull_request.target_ref_parts.type,
54 'target_ref_type': pull_request.target_ref_parts.type,
53 'target_ref_name': pull_request.target_ref_parts.name,
55 'target_ref_name': pull_request.target_ref_parts.name,
54 'source_repo': pull_request_source_repo.repo_name,
56 'source_repo': pull_request_source_repo.repo_name,
55 'target_repo': pull_request_target_repo.repo_name,
57 'target_repo': pull_request_target_repo.repo_name,
56 'source_repo_url': pull_request_source_repo_url,
58 'source_repo_url': pull_request_source_repo_url,
57 'target_repo_url': pull_request_target_repo_url,
59 'target_repo_url': pull_request_target_repo_url,
58 }
60 }
59 %>
61 %>
60
62
61 * ${_('Comment link')}: ${pr_comment_url}
63 * ${_('Comment link')}: ${pr_comment_url}
62
64
63 * ${_('Pull Request')}: !${pull_request.pull_request_id}
65 * ${_('Pull Request')}: !${pull_request.pull_request_id}
64
66
65 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
67 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
66
68
67 %if status_change and not closing_pr:
69 %if status_change and not closing_pr:
68 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
70 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
69
71
70 %elif status_change and closing_pr:
72 %elif status_change and closing_pr:
71 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
73 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
72
74
73 %endif
75 %endif
74 %if comment_file:
76 %if comment_file:
75 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
77 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
76
78
77 %endif
79 %endif
78 % if comment_type == 'todo':
80 % if comment_type == 'todo':
79 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
81 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
80 % else:
82 % else:
81 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
83 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
82 % endif
84 % endif
83
85
84 ${comment_body |n, trim}
86 ${comment_body |n, trim}
85
87
86 ---
88 ---
87 ${self.plaintext_footer()}
89 ${self.plaintext_footer()}
88 </%def>
90 </%def>
89
91
90
92
91 <%
93 <%
92 data = {
94 data = {
93 'user': h.person(user),
95 'user': h.person(user),
94 'comment_file': comment_file,
96 'comment_file': comment_file,
95 'comment_line': comment_line,
97 'comment_line': comment_line,
96 'comment_type': comment_type,
98 'comment_type': comment_type,
97 'comment_id': comment_id,
99 'comment_id': comment_id,
98 'renderer_type': renderer_type or 'plain',
100 'renderer_type': renderer_type or 'plain',
99
101
100 'pr_title': pull_request.title,
102 'pr_title': pull_request.title,
101 'pr_id': pull_request.pull_request_id,
103 'pr_id': pull_request.pull_request_id,
102 'status': status_change,
104 'status': status_change,
103 'source_ref_type': pull_request.source_ref_parts.type,
105 'source_ref_type': pull_request.source_ref_parts.type,
104 'source_ref_name': pull_request.source_ref_parts.name,
106 'source_ref_name': pull_request.source_ref_parts.name,
105 'target_ref_type': pull_request.target_ref_parts.type,
107 'target_ref_type': pull_request.target_ref_parts.type,
106 'target_ref_name': pull_request.target_ref_parts.name,
108 'target_ref_name': pull_request.target_ref_parts.name,
107 'source_repo': pull_request_source_repo.repo_name,
109 'source_repo': pull_request_source_repo.repo_name,
108 'target_repo': pull_request_target_repo.repo_name,
110 'target_repo': pull_request_target_repo.repo_name,
109 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
111 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
110 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
112 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
111 }
113 }
112 %>
114 %>
113
115
114 ## header
116 ## header
115 <table style="text-align:left;vertical-align:middle;width: 100%">
117 <table style="text-align:left;vertical-align:middle;width: 100%">
116 <tr>
118 <tr>
117 <td style="width:100%;border-bottom:1px solid #dbd9da;">
119 <td style="width:100%;border-bottom:1px solid #dbd9da;">
118
120
119 <div style="margin: 0; font-weight: bold">
121 <div style="margin: 0; font-weight: bold">
120 <div class="clear-both" style="margin-bottom: 4px">
122 <div class="clear-both" style="margin-bottom: 4px">
121 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
123 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
122 ${_('left a')}
124 ${_('left a')}
123 <a href="${pr_comment_url}" style="${base.link_css()}">
125 <a href="${pr_comment_url}" style="${base.link_css()}">
124 % if comment_file:
126 % if comment_file:
125 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
127 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
126 % else:
128 % else:
127 ${_('{comment_type} on pull request.').format(**data) |n}
129 ${_('{comment_type} on pull request.').format(**data) |n}
128 % endif
130 % endif
129 </a>
131 </a>
130 </div>
132 </div>
131 <div style="margin-top: 10px"></div>
133 <div style="margin-top: 10px"></div>
132 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
134 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
133 </div>
135 </div>
134
136
135 </td>
137 </td>
136 </tr>
138 </tr>
137
139
138 </table>
140 </table>
139 <div class="clear-both"></div>
141 <div class="clear-both"></div>
140 ## main body
142 ## main body
141 <table style="text-align:left;vertical-align:middle;width: 100%">
143 <table style="text-align:left;vertical-align:middle;width: 100%">
142
144
143 ## spacing def
145 ## spacing def
144 <tr>
146 <tr>
145 <td style="width: 130px"></td>
147 <td style="width: 130px"></td>
146 <td></td>
148 <td></td>
147 </tr>
149 </tr>
148
150
149 % if status_change:
151 % if status_change:
150 <tr>
152 <tr>
151 <td style="padding-right:20px;">${_('Review Status')}:</td>
153 <td style="padding-right:20px;">${_('Review Status')}:</td>
152 <td>
154 <td>
153 % if closing_pr:
155 % if closing_pr:
154 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
156 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
155 % else:
157 % else:
156 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
158 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
157 % endif
159 % endif
158 </td>
160 </td>
159 </tr>
161 </tr>
160 % endif
162 % endif
161 <tr>
163 <tr>
162 <td style="padding-right:20px;">${_('Pull request')}:</td>
164 <td style="padding-right:20px;">${_('Pull request')}:</td>
163 <td>
165 <td>
164 <a href="${pull_request_url}" style="${base.link_css()}">
166 <a href="${pull_request_url}" style="${base.link_css()}">
165 !${pull_request.pull_request_id}
167 !${pull_request.pull_request_id}
166 </a>
168 </a>
167 </td>
169 </td>
168 </tr>
170 </tr>
169
171
170 <tr>
172 <tr>
171 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
173 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
172 <td style="line-height:20px;">
174 <td style="line-height:20px;">
173 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
175 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
174 &rarr;
176 &rarr;
175 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
177 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
176 </td>
178 </td>
177 </tr>
179 </tr>
178
180
179 % if comment_file:
181 % if comment_file:
180 <tr>
182 <tr>
181 <td style="padding-right:20px;">${_('File')}:</td>
183 <td style="padding-right:20px;">${_('File')}:</td>
182 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
184 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
183 </tr>
185 </tr>
184 % endif
186 % endif
185
187
186 <tr style="border-bottom:1px solid #dbd9da;">
188 <tr style="border-bottom:1px solid #dbd9da;">
187 <td colspan="2" style="padding-right:20px;">
189 <td colspan="2" style="padding-right:20px;">
188 % if comment_type == 'todo':
190 % if comment_type == 'todo':
189 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
191 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
190 % else:
192 % else:
191 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
193 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
192 % endif
194 % endif
193 </td>
195 </td>
194 </tr>
196 </tr>
195
197
196 <tr>
198 <tr>
197 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
199 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
198 </tr>
200 </tr>
199
201
200 <tr>
202 <tr>
201 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
203 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
202 <td></td>
204 <td></td>
203 </tr>
205 </tr>
204 </table>
206 </table>
@@ -1,144 +1,146 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'pr_id': pull_request.pull_request_id,
10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title,
11 'pr_title': pull_request.title,
12 }
12 }
13
14 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
13 %>
15 %>
14
16
15 ${_('{user} requested a pull request review. !{pr_id}: "{pr_title}"').format(**data) |n}
17 ${subject_template.format(**data) |n}
16 </%def>
18 </%def>
17
19
18 ## PLAINTEXT VERSION OF BODY
20 ## PLAINTEXT VERSION OF BODY
19 <%def name="body_plaintext()" filter="n,trim">
21 <%def name="body_plaintext()" filter="n,trim">
20 <%
22 <%
21 data = {
23 data = {
22 'user': h.person(user),
24 'user': h.person(user),
23 'pr_id': pull_request.pull_request_id,
25 'pr_id': pull_request.pull_request_id,
24 'pr_title': pull_request.title,
26 'pr_title': pull_request.title,
25 'source_ref_type': pull_request.source_ref_parts.type,
27 'source_ref_type': pull_request.source_ref_parts.type,
26 'source_ref_name': pull_request.source_ref_parts.name,
28 'source_ref_name': pull_request.source_ref_parts.name,
27 'target_ref_type': pull_request.target_ref_parts.type,
29 'target_ref_type': pull_request.target_ref_parts.type,
28 'target_ref_name': pull_request.target_ref_parts.name,
30 'target_ref_name': pull_request.target_ref_parts.name,
29 'repo_url': pull_request_source_repo_url,
31 'repo_url': pull_request_source_repo_url,
30 'source_repo': pull_request_source_repo.repo_name,
32 'source_repo': pull_request_source_repo.repo_name,
31 'target_repo': pull_request_target_repo.repo_name,
33 'target_repo': pull_request_target_repo.repo_name,
32 'source_repo_url': pull_request_source_repo_url,
34 'source_repo_url': pull_request_source_repo_url,
33 'target_repo_url': pull_request_target_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
34 }
36 }
35 %>
37 %>
36
38
37 * ${_('Pull Request link')}: ${pull_request_url}
39 * ${_('Pull Request link')}: ${pull_request_url}
38
40
39 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
41 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
40
42
41 * ${_('Title')}: ${pull_request.title}
43 * ${_('Title')}: ${pull_request.title}
42
44
43 * ${_('Description')}:
45 * ${_('Description')}:
44
46
45 ${pull_request.description | trim}
47 ${pull_request.description | trim}
46
48
47
49
48 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
50 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
49
51
50 % for commit_id, message in pull_request_commits:
52 % for commit_id, message in pull_request_commits:
51 - ${h.short_id(commit_id)}
53 - ${h.short_id(commit_id)}
52 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
54 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
53
55
54 % endfor
56 % endfor
55
57
56 ---
58 ---
57 ${self.plaintext_footer()}
59 ${self.plaintext_footer()}
58 </%def>
60 </%def>
59 <%
61 <%
60 data = {
62 data = {
61 'user': h.person(user),
63 'user': h.person(user),
62 'pr_id': pull_request.pull_request_id,
64 'pr_id': pull_request.pull_request_id,
63 'pr_title': pull_request.title,
65 'pr_title': pull_request.title,
64 'source_ref_type': pull_request.source_ref_parts.type,
66 'source_ref_type': pull_request.source_ref_parts.type,
65 'source_ref_name': pull_request.source_ref_parts.name,
67 'source_ref_name': pull_request.source_ref_parts.name,
66 'target_ref_type': pull_request.target_ref_parts.type,
68 'target_ref_type': pull_request.target_ref_parts.type,
67 'target_ref_name': pull_request.target_ref_parts.name,
69 'target_ref_name': pull_request.target_ref_parts.name,
68 'repo_url': pull_request_source_repo_url,
70 'repo_url': pull_request_source_repo_url,
69 'source_repo': pull_request_source_repo.repo_name,
71 'source_repo': pull_request_source_repo.repo_name,
70 'target_repo': pull_request_target_repo.repo_name,
72 'target_repo': pull_request_target_repo.repo_name,
71 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
73 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
72 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
74 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
73 }
75 }
74 %>
76 %>
75 ## header
77 ## header
76 <table style="text-align:left;vertical-align:middle;width: 100%">
78 <table style="text-align:left;vertical-align:middle;width: 100%">
77 <tr>
79 <tr>
78 <td style="width:100%;border-bottom:1px solid #dbd9da;">
80 <td style="width:100%;border-bottom:1px solid #dbd9da;">
79
81
80 <div style="margin: 0; font-weight: bold">
82 <div style="margin: 0; font-weight: bold">
81 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
83 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
82 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
84 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
83 ${_('requested a')}
85 ${_('requested a')}
84 <a href="${pull_request_url}" style="${base.link_css()}">
86 <a href="${pull_request_url}" style="${base.link_css()}">
85 ${_('pull request review.').format(**data) }
87 ${_('pull request review.').format(**data) }
86 </a>
88 </a>
87 </div>
89 </div>
88 <div style="margin-top: 10px"></div>
90 <div style="margin-top: 10px"></div>
89 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
91 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
90 </div>
92 </div>
91
93
92 </td>
94 </td>
93 </tr>
95 </tr>
94
96
95 </table>
97 </table>
96 <div class="clear-both"></div>
98 <div class="clear-both"></div>
97 ## main body
99 ## main body
98 <table style="text-align:left;vertical-align:middle;width: 100%">
100 <table style="text-align:left;vertical-align:middle;width: 100%">
99 ## spacing def
101 ## spacing def
100 <tr>
102 <tr>
101 <td style="width: 130px"></td>
103 <td style="width: 130px"></td>
102 <td></td>
104 <td></td>
103 </tr>
105 </tr>
104
106
105 <tr>
107 <tr>
106 <td style="padding-right:20px;">${_('Pull request')}:</td>
108 <td style="padding-right:20px;">${_('Pull request')}:</td>
107 <td>
109 <td>
108 <a href="${pull_request_url}" style="${base.link_css()}">
110 <a href="${pull_request_url}" style="${base.link_css()}">
109 !${pull_request.pull_request_id}
111 !${pull_request.pull_request_id}
110 </a>
112 </a>
111 </td>
113 </td>
112 </tr>
114 </tr>
113
115
114 <tr>
116 <tr>
115 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
117 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
116 <td style="line-height:20px;">
118 <td style="line-height:20px;">
117 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
119 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
118 &rarr;
120 &rarr;
119 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
121 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
120 </td>
122 </td>
121 </tr>
123 </tr>
122
124
123 <tr>
125 <tr>
124 <td style="padding-right:20px;">${_('Description')}:</td>
126 <td style="padding-right:20px;">${_('Description')}:</td>
125 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
127 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
126 </tr>
128 </tr>
127 <tr>
129 <tr>
128 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
130 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
129 <td></td>
131 <td></td>
130 </tr>
132 </tr>
131
133
132 <tr>
134 <tr>
133 <td colspan="2">
135 <td colspan="2">
134 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
136 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
135 % for commit_id, message in pull_request_commits:
137 % for commit_id, message in pull_request_commits:
136 <li style="margin:0 0 1em;">
138 <li style="margin:0 0 1em;">
137 <pre style="margin:0 0 .5em"><a href="${h.route_path('repo_commit', repo_name=pull_request_source_repo.repo_name, commit_id=commit_id)}" style="${base.link_css()}">${h.short_id(commit_id)}</a></pre>
139 <pre style="margin:0 0 .5em"><a href="${h.route_path('repo_commit', repo_name=pull_request_source_repo.repo_name, commit_id=commit_id)}" style="${base.link_css()}">${h.short_id(commit_id)}</a></pre>
138 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
140 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
139 </li>
141 </li>
140 % endfor
142 % endfor
141 </ol>
143 </ol>
142 </td>
144 </td>
143 </tr>
145 </tr>
144 </table>
146 </table>
@@ -1,170 +1,172 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'updating_user': '@'+h.person(updating_user),
9 'updating_user': '@'+h.person(updating_user),
10 'pr_id': pull_request.pull_request_id,
10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title,
11 'pr_title': pull_request.title,
12 }
12 }
13
14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
13 %>
15 %>
14
16
15 ${_('{updating_user} updated pull request. !{pr_id}: "{pr_title}"').format(**data) |n}
17 ${subject_template.format(**data) |n}
16 </%def>
18 </%def>
17
19
18 ## PLAINTEXT VERSION OF BODY
20 ## PLAINTEXT VERSION OF BODY
19 <%def name="body_plaintext()" filter="n,trim">
21 <%def name="body_plaintext()" filter="n,trim">
20 <%
22 <%
21 data = {
23 data = {
22 'updating_user': h.person(updating_user),
24 'updating_user': h.person(updating_user),
23 'pr_id': pull_request.pull_request_id,
25 'pr_id': pull_request.pull_request_id,
24 'pr_title': pull_request.title,
26 'pr_title': pull_request.title,
25 'source_ref_type': pull_request.source_ref_parts.type,
27 'source_ref_type': pull_request.source_ref_parts.type,
26 'source_ref_name': pull_request.source_ref_parts.name,
28 'source_ref_name': pull_request.source_ref_parts.name,
27 'target_ref_type': pull_request.target_ref_parts.type,
29 'target_ref_type': pull_request.target_ref_parts.type,
28 'target_ref_name': pull_request.target_ref_parts.name,
30 'target_ref_name': pull_request.target_ref_parts.name,
29 'repo_url': pull_request_source_repo_url,
31 'repo_url': pull_request_source_repo_url,
30 'source_repo': pull_request_source_repo.repo_name,
32 'source_repo': pull_request_source_repo.repo_name,
31 'target_repo': pull_request_target_repo.repo_name,
33 'target_repo': pull_request_target_repo.repo_name,
32 'source_repo_url': pull_request_source_repo_url,
34 'source_repo_url': pull_request_source_repo_url,
33 'target_repo_url': pull_request_target_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
34 }
36 }
35 %>
37 %>
36
38
37 * ${_('Pull Request link')}: ${pull_request_url}
39 * ${_('Pull Request link')}: ${pull_request_url}
38
40
39 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
41 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
40
42
41 * ${_('Title')}: ${pull_request.title}
43 * ${_('Title')}: ${pull_request.title}
42
44
43 * ${_('Description')}:
45 * ${_('Description')}:
44
46
45 ${pull_request.description | trim}
47 ${pull_request.description | trim}
46
48
47 * Changed commits:
49 * Changed commits:
48
50
49 - Added: ${len(added_commits)}
51 - Added: ${len(added_commits)}
50 - Removed: ${len(removed_commits)}
52 - Removed: ${len(removed_commits)}
51
53
52 * Changed files:
54 * Changed files:
53
55
54 %if not changed_files:
56 %if not changed_files:
55 No file changes found
57 No file changes found
56 %else:
58 %else:
57 %for file_name in added_files:
59 %for file_name in added_files:
58 - A `${file_name}`
60 - A `${file_name}`
59 %endfor
61 %endfor
60 %for file_name in modified_files:
62 %for file_name in modified_files:
61 - M `${file_name}`
63 - M `${file_name}`
62 %endfor
64 %endfor
63 %for file_name in removed_files:
65 %for file_name in removed_files:
64 - R `${file_name}`
66 - R `${file_name}`
65 %endfor
67 %endfor
66 %endif
68 %endif
67
69
68 ---
70 ---
69 ${self.plaintext_footer()}
71 ${self.plaintext_footer()}
70 </%def>
72 </%def>
71 <%
73 <%
72 data = {
74 data = {
73 'updating_user': h.person(updating_user),
75 'updating_user': h.person(updating_user),
74 'pr_id': pull_request.pull_request_id,
76 'pr_id': pull_request.pull_request_id,
75 'pr_title': pull_request.title,
77 'pr_title': pull_request.title,
76 'source_ref_type': pull_request.source_ref_parts.type,
78 'source_ref_type': pull_request.source_ref_parts.type,
77 'source_ref_name': pull_request.source_ref_parts.name,
79 'source_ref_name': pull_request.source_ref_parts.name,
78 'target_ref_type': pull_request.target_ref_parts.type,
80 'target_ref_type': pull_request.target_ref_parts.type,
79 'target_ref_name': pull_request.target_ref_parts.name,
81 'target_ref_name': pull_request.target_ref_parts.name,
80 'repo_url': pull_request_source_repo_url,
82 'repo_url': pull_request_source_repo_url,
81 'source_repo': pull_request_source_repo.repo_name,
83 'source_repo': pull_request_source_repo.repo_name,
82 'target_repo': pull_request_target_repo.repo_name,
84 'target_repo': pull_request_target_repo.repo_name,
83 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
85 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
84 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
86 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
85 }
87 }
86 %>
88 %>
87
89
88 ## header
90 ## header
89 <table style="text-align:left;vertical-align:middle;width: 100%">
91 <table style="text-align:left;vertical-align:middle;width: 100%">
90 <tr>
92 <tr>
91 <td style="width:100%;border-bottom:1px solid #dbd9da;">
93 <td style="width:100%;border-bottom:1px solid #dbd9da;">
92
94
93 <div style="margin: 0; font-weight: bold">
95 <div style="margin: 0; font-weight: bold">
94 <div class="clear-both" style="margin-bottom: 4px">
96 <div class="clear-both" style="margin-bottom: 4px">
95 <span style="color:#7E7F7F">@${h.person(updating_user.username)}</span>
97 <span style="color:#7E7F7F">@${h.person(updating_user.username)}</span>
96 ${_('updated')}
98 ${_('updated')}
97 <a href="${pull_request_url}" style="${base.link_css()}">
99 <a href="${pull_request_url}" style="${base.link_css()}">
98 ${_('pull request.').format(**data) }
100 ${_('pull request.').format(**data) }
99 </a>
101 </a>
100 </div>
102 </div>
101 <div style="margin-top: 10px"></div>
103 <div style="margin-top: 10px"></div>
102 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
104 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
103 </div>
105 </div>
104
106
105 </td>
107 </td>
106 </tr>
108 </tr>
107
109
108 </table>
110 </table>
109 <div class="clear-both"></div>
111 <div class="clear-both"></div>
110 ## main body
112 ## main body
111 <table style="text-align:left;vertical-align:middle;width: 100%">
113 <table style="text-align:left;vertical-align:middle;width: 100%">
112 ## spacing def
114 ## spacing def
113 <tr>
115 <tr>
114 <td style="width: 130px"></td>
116 <td style="width: 130px"></td>
115 <td></td>
117 <td></td>
116 </tr>
118 </tr>
117
119
118 <tr>
120 <tr>
119 <td style="padding-right:20px;">${_('Pull request')}:</td>
121 <td style="padding-right:20px;">${_('Pull request')}:</td>
120 <td>
122 <td>
121 <a href="${pull_request_url}" style="${base.link_css()}">
123 <a href="${pull_request_url}" style="${base.link_css()}">
122 !${pull_request.pull_request_id}
124 !${pull_request.pull_request_id}
123 </a>
125 </a>
124 </td>
126 </td>
125 </tr>
127 </tr>
126
128
127 <tr>
129 <tr>
128 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
130 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
129 <td style="line-height:20px;">
131 <td style="line-height:20px;">
130 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
132 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
131 &rarr;
133 &rarr;
132 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
134 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
133 </td>
135 </td>
134 </tr>
136 </tr>
135
137
136 <tr>
138 <tr>
137 <td style="padding-right:20px;">${_('Description')}:</td>
139 <td style="padding-right:20px;">${_('Description')}:</td>
138 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
140 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
139 </tr>
141 </tr>
140 <tr>
142 <tr>
141 <td style="padding-right:20px;">${_('Changes')}:</td>
143 <td style="padding-right:20px;">${_('Changes')}:</td>
142 <td>
144 <td>
143 <strong>Changed commits:</strong>
145 <strong>Changed commits:</strong>
144 <ul class="changes-ul">
146 <ul class="changes-ul">
145 <li>- Added: ${len(added_commits)}</li>
147 <li>- Added: ${len(added_commits)}</li>
146 <li>- Removed: ${len(removed_commits)}</li>
148 <li>- Removed: ${len(removed_commits)}</li>
147 </ul>
149 </ul>
148
150
149 <strong>Changed files:</strong>
151 <strong>Changed files:</strong>
150 <ul class="changes-ul">
152 <ul class="changes-ul">
151
153
152 %if not changed_files:
154 %if not changed_files:
153 <li>No file changes found</li>
155 <li>No file changes found</li>
154 %else:
156 %else:
155 %for file_name in added_files:
157 %for file_name in added_files:
156 <li>- A <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
158 <li>- A <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
157 %endfor
159 %endfor
158 %for file_name in modified_files:
160 %for file_name in modified_files:
159 <li>- M <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
161 <li>- M <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
160 %endfor
162 %endfor
161 %for file_name in removed_files:
163 %for file_name in removed_files:
162 <li>- R <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
164 <li>- R <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
163 %endfor
165 %endfor
164 %endif
166 %endif
165
167
166 </ul>
168 </ul>
167 </td>
169 </td>
168 </tr>
170 </tr>
169
171
170 </table>
172 </table>
General Comments 0
You need to be logged in to leave comments. Login now