##// END OF EJS Templates
emails: updated emails design and data structure they provide....
dan -
r4038:4a4a02a9 default
parent child Browse files
Show More
@@ -0,0 +1,29 b''
1 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
2
3 <html>
4 <head></head>
5
6 <body>
7
8 SUBJECT:
9 <pre>${c.subject}</pre>
10
11 HEADERS:
12 <pre>
13 ${c.headers}
14 </pre>
15
16 PLAINTEXT:
17 <pre>
18 ${c.email_body_plaintext|n}
19 </pre>
20
21 </body>
22 </html>
23 <br/><br/>
24
25 HTML:
26
27 ${c.email_body|n}
28
29
@@ -0,0 +1,49 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
3
4 <%def name="title()">
5 ${_('Show notification')} ${c.rhodecode_user.username}
6 %if c.rhodecode_name:
7 &middot; ${h.branding(c.rhodecode_name)}
8 %endif
9 </%def>
10
11 <%def name="breadcrumbs_links()">
12 ${h.link_to(_('My Notifications'), h.route_path('notifications_show_all'))}
13 &raquo;
14 ${_('Show notification')}
15 </%def>
16
17 <%def name="menu_bar_nav()">
18 ${self.menu_items(active='admin')}
19 </%def>
20
21 <%def name="main()">
22 <div class="box">
23
24 <!-- box / title -->
25 <div class="title">
26 Rendered plain text using markup renderer
27 </div>
28 <div class="table">
29 <div >
30 <div class="notification-header">
31 GRAVATAR
32 <div class="desc">
33 DESC
34 </div>
35 </div>
36 <div class="notification-body">
37 <div class="notification-subject">
38 <h3>${_('Subject')}: ${c.subject}</h3>
39 </div>
40 ${c.email_body|n}
41 </div>
42 </div>
43 </div>
44 </div>
45
46 </%def>
47
48
49
@@ -0,0 +1,34 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/debug_style/index.html"/>
3
4 <%def name="breadcrumbs_links()">
5 ${h.link_to(_('Style'), h.route_path('debug_style_home'))}
6 &raquo;
7 ${c.active}
8 </%def>
9
10
11 <%def name="real_main()">
12 <div class="box">
13 <div class="title">
14 ${self.breadcrumbs()}
15 </div>
16
17 <div class='sidebar-col-wrapper'>
18 ${self.sidebar()}
19 <div class="main-content">
20 <h2>Emails</h2>
21 <ul>
22 % for elem in sorted(c.email_types.keys()):
23 <li>
24 <a href="${request.route_path('debug_style_email', email_id=elem, _query={'user':c.rhodecode_user.username})}">${elem}</a>
25 |
26 <a href="${request.route_path('debug_style_email_plain_rendered', email_id=elem, _query={'user':c.rhodecode_user.username})}">plain rendered</a>
27 </li>
28 % endfor
29 </ul>
30
31 </div> <!-- .main-content -->
32 </div>
33 </div> <!-- .box -->
34 </%def>
@@ -1,51 +1,59 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import ADMIN_PREFIX
21 21 from rhodecode.lib.utils2 import str2bool
22 22
23 23
24 24 class DebugStylePredicate(object):
25 25 def __init__(self, val, config):
26 26 self.val = val
27 27
28 28 def text(self):
29 29 return 'debug style route = %s' % self.val
30 30
31 31 phash = text
32 32
33 33 def __call__(self, info, request):
34 34 return str2bool(request.registry.settings.get('debug_style'))
35 35
36 36
37 37 def includeme(config):
38 38 config.add_route_predicate(
39 39 'debug_style', DebugStylePredicate)
40 40
41 41 config.add_route(
42 42 name='debug_style_home',
43 43 pattern=ADMIN_PREFIX + '/debug_style',
44 44 debug_style=True)
45 45 config.add_route(
46 name='debug_style_email',
47 pattern=ADMIN_PREFIX + '/debug_style/email/{email_id}',
48 debug_style=True)
49 config.add_route(
50 name='debug_style_email_plain_rendered',
51 pattern=ADMIN_PREFIX + '/debug_style/email-rendered/{email_id}',
52 debug_style=True)
53 config.add_route(
46 54 name='debug_style_template',
47 55 pattern=ADMIN_PREFIX + '/debug_style/t/{t_path}',
48 56 debug_style=True)
49 57
50 58 # Scan module for configuration decorators.
51 59 config.scan('.views', ignore='.tests')
@@ -1,59 +1,338 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import logging
23 import datetime
23 24
24 25 from pyramid.view import view_config
25 26 from pyramid.renderers import render_to_response
26 27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib.celerylib import run_task, tasks
29 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.model.db import User
31 from rhodecode.model.notification import EmailNotificationModel
27 32
28 33 log = logging.getLogger(__name__)
29 34
30 35
31 36 class DebugStyleView(BaseAppView):
32 37 def load_default_context(self):
33 38 c = self._get_local_tmpl_context()
34 39
35 40 return c
36 41
37 42 @view_config(
38 43 route_name='debug_style_home', request_method='GET',
39 44 renderer=None)
40 45 def index(self):
41 46 c = self.load_default_context()
42 47 c.active = 'index'
43 48
44 49 return render_to_response(
45 50 'debug_style/index.html', self._get_template_context(c),
46 51 request=self.request)
47 52
48 53 @view_config(
54 route_name='debug_style_email', request_method='GET',
55 renderer=None)
56 @view_config(
57 route_name='debug_style_email_plain_rendered', request_method='GET',
58 renderer=None)
59 def render_email(self):
60 c = self.load_default_context()
61 email_id = self.request.matchdict['email_id']
62 c.active = 'emails'
63
64 pr = AttributeDict(
65 pull_request_id=123,
66 title='digital_ocean: fix redis, elastic search start on boot, '
67 'fix fd limits on supervisor, set postgres 11 version',
68 description='''
69 Check if we should use full-topic or mini-topic.
70
71 - full topic produces some problems with merge states etc
72 - server-mini-topic needs probably tweeks.
73 ''',
74 repo_name='foobar',
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
77 )
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
81
82 email_kwargs = {
83 'test': {},
84 'message': {
85 'body': 'message body !'
86 },
87 'email_test': {
88 'user': user,
89 'date': datetime.datetime.now(),
90 'rhodecode_version': c.rhodecode_version
91 },
92 'password_reset': {
93 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
94
95 'user': user,
96 'date': datetime.datetime.now(),
97 'email': 'test@rhodecode.com',
98 'first_admin_email': User.get_first_super_admin().email
99 },
100 'password_reset_confirmation': {
101 'new_password': 'new-password-example',
102 'user': user,
103 'date': datetime.datetime.now(),
104 'email': 'test@rhodecode.com',
105 'first_admin_email': User.get_first_super_admin().email
106 },
107 'registration': {
108 'user': user,
109 'date': datetime.datetime.now(),
110 },
111
112 'pull_request_comment': {
113 'user': user,
114
115 'status_change': None,
116 'status_change_type': None,
117
118 'pull_request': pr,
119 'pull_request_commits': [],
120
121 'pull_request_target_repo': target_repo,
122 'pull_request_target_repo_url': 'http://target-repo/url',
123
124 'pull_request_source_repo': source_repo,
125 'pull_request_source_repo_url': 'http://source-repo/url',
126
127 'pull_request_url': 'http://localhost/pr1',
128 'pr_comment_url': 'http://comment-url',
129
130 'comment_file': None,
131 'comment_line': None,
132 'comment_type': 'note',
133 'comment_body': 'This is my comment body. *I like !*',
134
135 'renderer_type': 'markdown',
136 'mention': True,
137
138 },
139 'pull_request_comment+status': {
140 'user': user,
141
142 'status_change': 'approved',
143 'status_change_type': 'approved',
144
145 'pull_request': pr,
146 'pull_request_commits': [],
147
148 'pull_request_target_repo': target_repo,
149 'pull_request_target_repo_url': 'http://target-repo/url',
150
151 'pull_request_source_repo': source_repo,
152 'pull_request_source_repo_url': 'http://source-repo/url',
153
154 'pull_request_url': 'http://localhost/pr1',
155 'pr_comment_url': 'http://comment-url',
156
157 'comment_type': 'todo',
158 'comment_file': None,
159 'comment_line': None,
160 'comment_body': '''
161 I think something like this would be better
162
163 ```py
164
165 def db():
166 global connection
167 return connection
168
169 ```
170
171 ''',
172
173 'renderer_type': 'markdown',
174 'mention': True,
175
176 },
177 'pull_request_comment+file': {
178 'user': user,
179
180 'status_change': None,
181 'status_change_type': None,
182
183 'pull_request': pr,
184 'pull_request_commits': [],
185
186 'pull_request_target_repo': target_repo,
187 'pull_request_target_repo_url': 'http://target-repo/url',
188
189 'pull_request_source_repo': source_repo,
190 'pull_request_source_repo_url': 'http://source-repo/url',
191
192 'pull_request_url': 'http://localhost/pr1',
193
194 'pr_comment_url': 'http://comment-url',
195
196 'comment_file': 'rhodecode/model/db.py',
197 'comment_line': 'o1210',
198 'comment_type': 'todo',
199 'comment_body': '''
200 I like this !
201
202 But please check this code::
203
204 def main():
205 print 'ok'
206
207 This should work better !
208 ''',
209
210 'renderer_type': 'rst',
211 'mention': True,
212
213 },
214
215 'cs_comment': {
216 'user': user,
217 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
218 'status_change': None,
219 'status_change_type': None,
220
221 'commit_target_repo_url': 'http://foo.example.com/#comment1',
222 'repo_name': 'test-repo',
223 'comment_type': 'note',
224 'comment_file': None,
225 'comment_line': None,
226 'commit_comment_url': 'http://comment-url',
227 'comment_body': 'This is my comment body. *I like !*',
228 'renderer_type': 'markdown',
229 'mention': True,
230 },
231 'cs_comment+status': {
232 'user': user,
233 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
234 'status_change': 'approved',
235 'status_change_type': 'approved',
236
237 'commit_target_repo_url': 'http://foo.example.com/#comment1',
238 'repo_name': 'test-repo',
239 'comment_type': 'note',
240 'comment_file': None,
241 'comment_line': None,
242 'commit_comment_url': 'http://comment-url',
243 'comment_body': '''
244 Hello **world**
245
246 This is a multiline comment :)
247
248 - list
249 - list2
250 ''',
251 'renderer_type': 'markdown',
252 'mention': True,
253 },
254 'cs_comment+file': {
255 'user': user,
256 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
257 'status_change': None,
258 'status_change_type': None,
259
260 'commit_target_repo_url': 'http://foo.example.com/#comment1',
261 'repo_name': 'test-repo',
262
263 'comment_type': 'note',
264 'comment_file': 'test-file.py',
265 'comment_line': 'n100',
266
267 'commit_comment_url': 'http://comment-url',
268 'comment_body': 'This is my comment body. *I like !*',
269 'renderer_type': 'markdown',
270 'mention': True,
271 },
272
273 'pull_request': {
274 'user': user,
275 'pull_request': pr,
276 'pull_request_commits': [
277 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
278 my-account: moved email closer to profile as it's similar data just moved outside.
279 '''),
280 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
281 users: description edit fixes
282
283 - tests
284 - added metatags info
285 '''),
286 ],
287
288 'pull_request_target_repo': target_repo,
289 'pull_request_target_repo_url': 'http://target-repo/url',
290
291 'pull_request_source_repo': source_repo,
292 'pull_request_source_repo_url': 'http://source-repo/url',
293
294 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
295 }
296
297 }
298
299 template_type = email_id.split('+')[0]
300 (c.subject, c.headers, c.email_body,
301 c.email_body_plaintext) = EmailNotificationModel().render_email(
302 template_type, **email_kwargs.get(email_id, {}))
303
304 test_email = self.request.GET.get('email')
305 if test_email:
306 recipients = [test_email]
307 run_task(tasks.send_email, recipients, c.subject,
308 c.email_body_plaintext, c.email_body)
309
310 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
311 template = 'debug_style/email_plain_rendered.mako'
312 else:
313 template = 'debug_style/email.mako'
314 return render_to_response(
315 template, self._get_template_context(c),
316 request=self.request)
317
318 @view_config(
49 319 route_name='debug_style_template', request_method='GET',
50 320 renderer=None)
51 321 def template(self):
52 322 t_path = self.request.matchdict['t_path']
53 323 c = self.load_default_context()
54 324 c.active = os.path.splitext(t_path)[0]
55 325 c.came_from = ''
326 c.email_types = {
327 'cs_comment+file': {},
328 'cs_comment+status': {},
329
330 'pull_request_comment+file': {},
331 'pull_request_comment+status': {},
332 }
333 c.email_types.update(EmailNotificationModel.email_types)
56 334
57 335 return render_to_response(
58 336 'debug_style/' + t_path, self._get_template_context(c),
59 request=self.request) No newline at end of file
337 request=self.request)
338
@@ -1,320 +1,320 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytest
22 22
23 23 from rhodecode.tests import TestController
24 24
25 25 from rhodecode.model.db import (
26 26 ChangesetComment, Notification, UserNotification)
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.lib import helpers as h
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 32 import urllib
33 33
34 34 base_url = {
35 35 'repo_commit': '/{repo_name}/changeset/{commit_id}',
36 36 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
37 37 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
38 38 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
39 39 }[name].format(**kwargs)
40 40
41 41 if params:
42 42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
43 43 return base_url
44 44
45 45
46 46 @pytest.mark.backends("git", "hg", "svn")
47 47 class TestRepoCommitCommentsView(TestController):
48 48
49 49 @pytest.fixture(autouse=True)
50 50 def prepare(self, request, baseapp):
51 51 for x in ChangesetComment.query().all():
52 52 Session().delete(x)
53 53 Session().commit()
54 54
55 55 for x in Notification.query().all():
56 56 Session().delete(x)
57 57 Session().commit()
58 58
59 59 request.addfinalizer(self.cleanup)
60 60
61 61 def cleanup(self):
62 62 for x in ChangesetComment.query().all():
63 63 Session().delete(x)
64 64 Session().commit()
65 65
66 66 for x in Notification.query().all():
67 67 Session().delete(x)
68 68 Session().commit()
69 69
70 70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 71 def test_create(self, comment_type, backend):
72 72 self.log_user()
73 73 commit = backend.repo.get_commit('300')
74 74 commit_id = commit.raw_id
75 75 text = u'CommentOnCommit'
76 76
77 77 params = {'text': text, 'csrf_token': self.csrf_token,
78 78 'comment_type': comment_type}
79 79 self.app.post(
80 80 route_path('repo_commit_comment_create',
81 81 repo_name=backend.repo_name, commit_id=commit_id),
82 82 params=params)
83 83
84 84 response = self.app.get(
85 85 route_path('repo_commit',
86 86 repo_name=backend.repo_name, commit_id=commit_id))
87 87
88 88 # test DB
89 89 assert ChangesetComment.query().count() == 1
90 90 assert_comment_links(response, ChangesetComment.query().count(), 0)
91 91
92 92 assert Notification.query().count() == 1
93 93 assert ChangesetComment.query().count() == 1
94 94
95 95 notification = Notification.query().all()[0]
96 96
97 97 comment_id = ChangesetComment.query().first().comment_id
98 98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99 99
100 100 author = notification.created_by_user.username_and_name
101 sbj = '{0} left a {1} on commit `{2}` in the {3} repository'.format(
101 sbj = '@{0} left a {1} on commit `{2}` in the `{3}` repository'.format(
102 102 author, comment_type, h.show_id(commit), backend.repo_name)
103 103 assert sbj == notification.subject
104 104
105 105 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
106 106 backend.repo_name, commit_id, comment_id))
107 107 assert lnk in notification.body
108 108
109 109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 110 def test_create_inline(self, comment_type, backend):
111 111 self.log_user()
112 112 commit = backend.repo.get_commit('300')
113 113 commit_id = commit.raw_id
114 114 text = u'CommentOnCommit'
115 115 f_path = 'vcs/web/simplevcs/views/repository.py'
116 116 line = 'n1'
117 117
118 118 params = {'text': text, 'f_path': f_path, 'line': line,
119 119 'comment_type': comment_type,
120 120 'csrf_token': self.csrf_token}
121 121
122 122 self.app.post(
123 123 route_path('repo_commit_comment_create',
124 124 repo_name=backend.repo_name, commit_id=commit_id),
125 125 params=params)
126 126
127 127 response = self.app.get(
128 128 route_path('repo_commit',
129 129 repo_name=backend.repo_name, commit_id=commit_id))
130 130
131 131 # test DB
132 132 assert ChangesetComment.query().count() == 1
133 133 assert_comment_links(response, 0, ChangesetComment.query().count())
134 134
135 135 if backend.alias == 'svn':
136 136 response.mustcontain(
137 137 '''data-f-path="vcs/commands/summary.py" '''
138 138 '''data-anchor-id="c-300-ad05457a43f8"'''
139 139 )
140 140 if backend.alias == 'git':
141 141 response.mustcontain(
142 142 '''data-f-path="vcs/backends/hg.py" '''
143 143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 144 )
145 145
146 146 if backend.alias == 'hg':
147 147 response.mustcontain(
148 148 '''data-f-path="vcs/backends/hg.py" '''
149 149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 150 )
151 151
152 152 assert Notification.query().count() == 1
153 153 assert ChangesetComment.query().count() == 1
154 154
155 155 notification = Notification.query().all()[0]
156 156 comment = ChangesetComment.query().first()
157 157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158 158
159 159 assert comment.revision == commit_id
160 160
161 161 author = notification.created_by_user.username_and_name
162 sbj = '{0} left a {1} on file `{2}` in commit `{3}` in the {4} repository'.format(
162 sbj = '@{0} left a {1} on file `{2}` in commit `{3}` in the `{4}` repository'.format(
163 163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164 164
165 165 assert sbj == notification.subject
166 166
167 167 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
168 168 backend.repo_name, commit_id, comment.comment_id))
169 169 assert lnk in notification.body
170 170 assert 'on line n1' in notification.body
171 171
172 172 def test_create_with_mention(self, backend):
173 173 self.log_user()
174 174
175 175 commit_id = backend.repo.get_commit('300').raw_id
176 176 text = u'@test_regular check CommentOnCommit'
177 177
178 178 params = {'text': text, 'csrf_token': self.csrf_token}
179 179 self.app.post(
180 180 route_path('repo_commit_comment_create',
181 181 repo_name=backend.repo_name, commit_id=commit_id),
182 182 params=params)
183 183
184 184 response = self.app.get(
185 185 route_path('repo_commit',
186 186 repo_name=backend.repo_name, commit_id=commit_id))
187 187 # test DB
188 188 assert ChangesetComment.query().count() == 1
189 189 assert_comment_links(response, ChangesetComment.query().count(), 0)
190 190
191 191 notification = Notification.query().one()
192 192
193 193 assert len(notification.recipients) == 2
194 194 users = [x.username for x in notification.recipients]
195 195
196 196 # test_regular gets notification by @mention
197 197 assert sorted(users) == [u'test_admin', u'test_regular']
198 198
199 199 def test_create_with_status_change(self, backend):
200 200 self.log_user()
201 201 commit = backend.repo.get_commit('300')
202 202 commit_id = commit.raw_id
203 203 text = u'CommentOnCommit'
204 204 f_path = 'vcs/web/simplevcs/views/repository.py'
205 205 line = 'n1'
206 206
207 207 params = {'text': text, 'changeset_status': 'approved',
208 208 'csrf_token': self.csrf_token}
209 209
210 210 self.app.post(
211 211 route_path(
212 212 'repo_commit_comment_create',
213 213 repo_name=backend.repo_name, commit_id=commit_id),
214 214 params=params)
215 215
216 216 response = self.app.get(
217 217 route_path('repo_commit',
218 218 repo_name=backend.repo_name, commit_id=commit_id))
219 219
220 220 # test DB
221 221 assert ChangesetComment.query().count() == 1
222 222 assert_comment_links(response, ChangesetComment.query().count(), 0)
223 223
224 224 assert Notification.query().count() == 1
225 225 assert ChangesetComment.query().count() == 1
226 226
227 227 notification = Notification.query().all()[0]
228 228
229 229 comment_id = ChangesetComment.query().first().comment_id
230 230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231 231
232 232 author = notification.created_by_user.username_and_name
233 sbj = '[status: Approved] {0} left a note on commit `{1}` in the {2} repository'.format(
233 sbj = '[status: Approved] @{0} left a note on commit `{1}` in the `{2}` repository'.format(
234 234 author, h.show_id(commit), backend.repo_name)
235 235 assert sbj == notification.subject
236 236
237 237 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
238 238 backend.repo_name, commit_id, comment_id))
239 239 assert lnk in notification.body
240 240
241 241 def test_delete(self, backend):
242 242 self.log_user()
243 243 commit_id = backend.repo.get_commit('300').raw_id
244 244 text = u'CommentOnCommit'
245 245
246 246 params = {'text': text, 'csrf_token': self.csrf_token}
247 247 self.app.post(
248 248 route_path(
249 249 'repo_commit_comment_create',
250 250 repo_name=backend.repo_name, commit_id=commit_id),
251 251 params=params)
252 252
253 253 comments = ChangesetComment.query().all()
254 254 assert len(comments) == 1
255 255 comment_id = comments[0].comment_id
256 256
257 257 self.app.post(
258 258 route_path('repo_commit_comment_delete',
259 259 repo_name=backend.repo_name,
260 260 commit_id=commit_id,
261 261 comment_id=comment_id),
262 262 params={'csrf_token': self.csrf_token})
263 263
264 264 comments = ChangesetComment.query().all()
265 265 assert len(comments) == 0
266 266
267 267 response = self.app.get(
268 268 route_path('repo_commit',
269 269 repo_name=backend.repo_name, commit_id=commit_id))
270 270 assert_comment_links(response, 0, 0)
271 271
272 272 @pytest.mark.parametrize('renderer, input, output', [
273 273 ('rst', 'plain text', '<p>plain text</p>'),
274 274 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
275 275 ('rst', '*italics*', '<em>italics</em>'),
276 276 ('rst', '**bold**', '<strong>bold</strong>'),
277 277 ('markdown', 'plain text', '<p>plain text</p>'),
278 278 ('markdown', '# header', '<h1>header</h1>'),
279 279 ('markdown', '*italics*', '<em>italics</em>'),
280 280 ('markdown', '**bold**', '<strong>bold</strong>'),
281 281 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
282 282 'md-header', 'md-italics', 'md-bold', ])
283 283 def test_preview(self, renderer, input, output, backend, xhr_header):
284 284 self.log_user()
285 285 params = {
286 286 'renderer': renderer,
287 287 'text': input,
288 288 'csrf_token': self.csrf_token
289 289 }
290 290 commit_id = '0' * 16 # fake this for tests
291 291 response = self.app.post(
292 292 route_path('repo_commit_comment_preview',
293 293 repo_name=backend.repo_name, commit_id=commit_id,),
294 294 params=params,
295 295 extra_environ=xhr_header)
296 296
297 297 response.mustcontain(output)
298 298
299 299
300 300 def assert_comment_links(response, comments, inline_comments):
301 301 if comments == 1:
302 302 comments_text = "%d General" % comments
303 303 else:
304 304 comments_text = "%d General" % comments
305 305
306 306 if inline_comments == 1:
307 307 inline_comments_text = "%d Inline" % inline_comments
308 308 else:
309 309 inline_comments_text = "%d Inline" % inline_comments
310 310
311 311 if comments:
312 312 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
313 313 else:
314 314 response.mustcontain(comments_text)
315 315
316 316 if inline_comments:
317 317 response.mustcontain(
318 318 'id="inline-comments-counter">%s' % inline_comments_text)
319 319 else:
320 320 response.mustcontain(inline_comments_text)
@@ -1,1221 +1,1217 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import mock
21 21 import pytest
22 22
23 23 import rhodecode
24 24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 25 from rhodecode.lib.vcs.nodes import FileNode
26 26 from rhodecode.lib import helpers as h
27 27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 28 from rhodecode.model.db import (
29 29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.tests import (
34 34 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 38 import urllib
39 39
40 40 base_url = {
41 41 'repo_changelog': '/{repo_name}/changelog',
42 42 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 43 'repo_commits': '/{repo_name}/commits',
44 44 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 45 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 46 'pullrequest_show_all': '/{repo_name}/pull-request',
47 47 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 48 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 49 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 50 'pullrequest_new': '/{repo_name}/pull-request/new',
51 51 'pullrequest_create': '/{repo_name}/pull-request/create',
52 52 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 53 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 54 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 55 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 56 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 57 }[name].format(**kwargs)
58 58
59 59 if params:
60 60 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
61 61 return base_url
62 62
63 63
64 64 @pytest.mark.usefixtures('app', 'autologin_user')
65 65 @pytest.mark.backends("git", "hg")
66 66 class TestPullrequestsView(object):
67 67
68 68 def test_index(self, backend):
69 69 self.app.get(route_path(
70 70 'pullrequest_new',
71 71 repo_name=backend.repo_name))
72 72
73 73 def test_option_menu_create_pull_request_exists(self, backend):
74 74 repo_name = backend.repo_name
75 75 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
76 76
77 77 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
78 78 'pullrequest_new', repo_name=repo_name)
79 79 response.mustcontain(create_pr_link)
80 80
81 81 def test_create_pr_form_with_raw_commit_id(self, backend):
82 82 repo = backend.repo
83 83
84 84 self.app.get(
85 85 route_path('pullrequest_new', repo_name=repo.repo_name,
86 86 commit=repo.get_commit().raw_id),
87 87 status=200)
88 88
89 89 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
90 90 @pytest.mark.parametrize('range_diff', ["0", "1"])
91 91 def test_show(self, pr_util, pr_merge_enabled, range_diff):
92 92 pull_request = pr_util.create_pull_request(
93 93 mergeable=pr_merge_enabled, enable_notifications=False)
94 94
95 95 response = self.app.get(route_path(
96 96 'pullrequest_show',
97 97 repo_name=pull_request.target_repo.scm_instance().name,
98 98 pull_request_id=pull_request.pull_request_id,
99 99 params={'range-diff': range_diff}))
100 100
101 101 for commit_id in pull_request.revisions:
102 102 response.mustcontain(commit_id)
103 103
104 104 assert pull_request.target_ref_parts.type in response
105 105 assert pull_request.target_ref_parts.name in response
106 106 target_clone_url = pull_request.target_repo.clone_url()
107 107 assert target_clone_url in response
108 108
109 109 assert 'class="pull-request-merge"' in response
110 110 if pr_merge_enabled:
111 111 response.mustcontain('Pull request reviewer approval is pending')
112 112 else:
113 113 response.mustcontain('Server-side pull request merging is disabled.')
114 114
115 115 if range_diff == "1":
116 116 response.mustcontain('Turn off: Show the diff as commit range')
117 117
118 118 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
119 119 # Logout
120 120 response = self.app.post(
121 121 h.route_path('logout'),
122 122 params={'csrf_token': csrf_token})
123 123 # Login as regular user
124 124 response = self.app.post(h.route_path('login'),
125 125 {'username': TEST_USER_REGULAR_LOGIN,
126 126 'password': 'test12'})
127 127
128 128 pull_request = pr_util.create_pull_request(
129 129 author=TEST_USER_REGULAR_LOGIN)
130 130
131 131 response = self.app.get(route_path(
132 132 'pullrequest_show',
133 133 repo_name=pull_request.target_repo.scm_instance().name,
134 134 pull_request_id=pull_request.pull_request_id))
135 135
136 136 response.mustcontain('Server-side pull request merging is disabled.')
137 137
138 138 assert_response = response.assert_response()
139 139 # for regular user without a merge permissions, we don't see it
140 140 assert_response.no_element_exists('#close-pull-request-action')
141 141
142 142 user_util.grant_user_permission_to_repo(
143 143 pull_request.target_repo,
144 144 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
145 145 'repository.write')
146 146 response = self.app.get(route_path(
147 147 'pullrequest_show',
148 148 repo_name=pull_request.target_repo.scm_instance().name,
149 149 pull_request_id=pull_request.pull_request_id))
150 150
151 151 response.mustcontain('Server-side pull request merging is disabled.')
152 152
153 153 assert_response = response.assert_response()
154 154 # now regular user has a merge permissions, we have CLOSE button
155 155 assert_response.one_element_exists('#close-pull-request-action')
156 156
157 157 def test_show_invalid_commit_id(self, pr_util):
158 158 # Simulating invalid revisions which will cause a lookup error
159 159 pull_request = pr_util.create_pull_request()
160 160 pull_request.revisions = ['invalid']
161 161 Session().add(pull_request)
162 162 Session().commit()
163 163
164 164 response = self.app.get(route_path(
165 165 'pullrequest_show',
166 166 repo_name=pull_request.target_repo.scm_instance().name,
167 167 pull_request_id=pull_request.pull_request_id))
168 168
169 169 for commit_id in pull_request.revisions:
170 170 response.mustcontain(commit_id)
171 171
172 172 def test_show_invalid_source_reference(self, pr_util):
173 173 pull_request = pr_util.create_pull_request()
174 174 pull_request.source_ref = 'branch:b:invalid'
175 175 Session().add(pull_request)
176 176 Session().commit()
177 177
178 178 self.app.get(route_path(
179 179 'pullrequest_show',
180 180 repo_name=pull_request.target_repo.scm_instance().name,
181 181 pull_request_id=pull_request.pull_request_id))
182 182
183 183 def test_edit_title_description(self, pr_util, csrf_token):
184 184 pull_request = pr_util.create_pull_request()
185 185 pull_request_id = pull_request.pull_request_id
186 186
187 187 response = self.app.post(
188 188 route_path('pullrequest_update',
189 189 repo_name=pull_request.target_repo.repo_name,
190 190 pull_request_id=pull_request_id),
191 191 params={
192 192 'edit_pull_request': 'true',
193 193 'title': 'New title',
194 194 'description': 'New description',
195 195 'csrf_token': csrf_token})
196 196
197 197 assert_session_flash(
198 198 response, u'Pull request title & description updated.',
199 199 category='success')
200 200
201 201 pull_request = PullRequest.get(pull_request_id)
202 202 assert pull_request.title == 'New title'
203 203 assert pull_request.description == 'New description'
204 204
205 205 def test_edit_title_description_closed(self, pr_util, csrf_token):
206 206 pull_request = pr_util.create_pull_request()
207 207 pull_request_id = pull_request.pull_request_id
208 208 repo_name = pull_request.target_repo.repo_name
209 209 pr_util.close()
210 210
211 211 response = self.app.post(
212 212 route_path('pullrequest_update',
213 213 repo_name=repo_name, pull_request_id=pull_request_id),
214 214 params={
215 215 'edit_pull_request': 'true',
216 216 'title': 'New title',
217 217 'description': 'New description',
218 218 'csrf_token': csrf_token}, status=200)
219 219 assert_session_flash(
220 220 response, u'Cannot update closed pull requests.',
221 221 category='error')
222 222
223 223 def test_update_invalid_source_reference(self, pr_util, csrf_token):
224 224 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
225 225
226 226 pull_request = pr_util.create_pull_request()
227 227 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
228 228 Session().add(pull_request)
229 229 Session().commit()
230 230
231 231 pull_request_id = pull_request.pull_request_id
232 232
233 233 response = self.app.post(
234 234 route_path('pullrequest_update',
235 235 repo_name=pull_request.target_repo.repo_name,
236 236 pull_request_id=pull_request_id),
237 237 params={'update_commits': 'true', 'csrf_token': csrf_token})
238 238
239 239 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
240 240 UpdateFailureReason.MISSING_SOURCE_REF])
241 241 assert_session_flash(response, expected_msg, category='error')
242 242
243 243 def test_missing_target_reference(self, pr_util, csrf_token):
244 244 from rhodecode.lib.vcs.backends.base import MergeFailureReason
245 245 pull_request = pr_util.create_pull_request(
246 246 approved=True, mergeable=True)
247 247 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
248 248 pull_request.target_ref = unicode_reference
249 249 Session().add(pull_request)
250 250 Session().commit()
251 251
252 252 pull_request_id = pull_request.pull_request_id
253 253 pull_request_url = route_path(
254 254 'pullrequest_show',
255 255 repo_name=pull_request.target_repo.repo_name,
256 256 pull_request_id=pull_request_id)
257 257
258 258 response = self.app.get(pull_request_url)
259 259 target_ref_id = 'invalid-branch'
260 260 merge_resp = MergeResponse(
261 261 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
262 262 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
263 263 response.assert_response().element_contains(
264 264 'span[data-role="merge-message"]', merge_resp.merge_status_message)
265 265
266 266 def test_comment_and_close_pull_request_custom_message_approved(
267 267 self, pr_util, csrf_token, xhr_header):
268 268
269 269 pull_request = pr_util.create_pull_request(approved=True)
270 270 pull_request_id = pull_request.pull_request_id
271 271 author = pull_request.user_id
272 272 repo = pull_request.target_repo.repo_id
273 273
274 274 self.app.post(
275 275 route_path('pullrequest_comment_create',
276 276 repo_name=pull_request.target_repo.scm_instance().name,
277 277 pull_request_id=pull_request_id),
278 278 params={
279 279 'close_pull_request': '1',
280 280 'text': 'Closing a PR',
281 281 'csrf_token': csrf_token},
282 282 extra_environ=xhr_header,)
283 283
284 284 journal = UserLog.query()\
285 285 .filter(UserLog.user_id == author)\
286 286 .filter(UserLog.repository_id == repo) \
287 287 .order_by(UserLog.user_log_id.asc()) \
288 288 .all()
289 289 assert journal[-1].action == 'repo.pull_request.close'
290 290
291 291 pull_request = PullRequest.get(pull_request_id)
292 292 assert pull_request.is_closed()
293 293
294 294 status = ChangesetStatusModel().get_status(
295 295 pull_request.source_repo, pull_request=pull_request)
296 296 assert status == ChangesetStatus.STATUS_APPROVED
297 297 comments = ChangesetComment().query() \
298 298 .filter(ChangesetComment.pull_request == pull_request) \
299 299 .order_by(ChangesetComment.comment_id.asc())\
300 300 .all()
301 301 assert comments[-1].text == 'Closing a PR'
302 302
303 303 def test_comment_force_close_pull_request_rejected(
304 304 self, pr_util, csrf_token, xhr_header):
305 305 pull_request = pr_util.create_pull_request()
306 306 pull_request_id = pull_request.pull_request_id
307 307 PullRequestModel().update_reviewers(
308 308 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
309 309 pull_request.author)
310 310 author = pull_request.user_id
311 311 repo = pull_request.target_repo.repo_id
312 312
313 313 self.app.post(
314 314 route_path('pullrequest_comment_create',
315 315 repo_name=pull_request.target_repo.scm_instance().name,
316 316 pull_request_id=pull_request_id),
317 317 params={
318 318 'close_pull_request': '1',
319 319 'csrf_token': csrf_token},
320 320 extra_environ=xhr_header)
321 321
322 322 pull_request = PullRequest.get(pull_request_id)
323 323
324 324 journal = UserLog.query()\
325 325 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
326 326 .order_by(UserLog.user_log_id.asc()) \
327 327 .all()
328 328 assert journal[-1].action == 'repo.pull_request.close'
329 329
330 330 # check only the latest status, not the review status
331 331 status = ChangesetStatusModel().get_status(
332 332 pull_request.source_repo, pull_request=pull_request)
333 333 assert status == ChangesetStatus.STATUS_REJECTED
334 334
335 335 def test_comment_and_close_pull_request(
336 336 self, pr_util, csrf_token, xhr_header):
337 337 pull_request = pr_util.create_pull_request()
338 338 pull_request_id = pull_request.pull_request_id
339 339
340 340 response = self.app.post(
341 341 route_path('pullrequest_comment_create',
342 342 repo_name=pull_request.target_repo.scm_instance().name,
343 343 pull_request_id=pull_request.pull_request_id),
344 344 params={
345 345 'close_pull_request': 'true',
346 346 'csrf_token': csrf_token},
347 347 extra_environ=xhr_header)
348 348
349 349 assert response.json
350 350
351 351 pull_request = PullRequest.get(pull_request_id)
352 352 assert pull_request.is_closed()
353 353
354 354 # check only the latest status, not the review status
355 355 status = ChangesetStatusModel().get_status(
356 356 pull_request.source_repo, pull_request=pull_request)
357 357 assert status == ChangesetStatus.STATUS_REJECTED
358 358
359 359 def test_create_pull_request(self, backend, csrf_token):
360 360 commits = [
361 361 {'message': 'ancestor'},
362 362 {'message': 'change'},
363 363 {'message': 'change2'},
364 364 ]
365 365 commit_ids = backend.create_master_repo(commits)
366 366 target = backend.create_repo(heads=['ancestor'])
367 367 source = backend.create_repo(heads=['change2'])
368 368
369 369 response = self.app.post(
370 370 route_path('pullrequest_create', repo_name=source.repo_name),
371 371 [
372 372 ('source_repo', source.repo_name),
373 373 ('source_ref', 'branch:default:' + commit_ids['change2']),
374 374 ('target_repo', target.repo_name),
375 375 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
376 376 ('common_ancestor', commit_ids['ancestor']),
377 377 ('pullrequest_title', 'Title'),
378 378 ('pullrequest_desc', 'Description'),
379 379 ('description_renderer', 'markdown'),
380 380 ('__start__', 'review_members:sequence'),
381 381 ('__start__', 'reviewer:mapping'),
382 382 ('user_id', '1'),
383 383 ('__start__', 'reasons:sequence'),
384 384 ('reason', 'Some reason'),
385 385 ('__end__', 'reasons:sequence'),
386 386 ('__start__', 'rules:sequence'),
387 387 ('__end__', 'rules:sequence'),
388 388 ('mandatory', 'False'),
389 389 ('__end__', 'reviewer:mapping'),
390 390 ('__end__', 'review_members:sequence'),
391 391 ('__start__', 'revisions:sequence'),
392 392 ('revisions', commit_ids['change']),
393 393 ('revisions', commit_ids['change2']),
394 394 ('__end__', 'revisions:sequence'),
395 395 ('user', ''),
396 396 ('csrf_token', csrf_token),
397 397 ],
398 398 status=302)
399 399
400 400 location = response.headers['Location']
401 401 pull_request_id = location.rsplit('/', 1)[1]
402 402 assert pull_request_id != 'new'
403 403 pull_request = PullRequest.get(int(pull_request_id))
404 404
405 405 # check that we have now both revisions
406 406 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
407 407 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
408 408 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
409 409 assert pull_request.target_ref == expected_target_ref
410 410
411 411 def test_reviewer_notifications(self, backend, csrf_token):
412 412 # We have to use the app.post for this test so it will create the
413 413 # notifications properly with the new PR
414 414 commits = [
415 415 {'message': 'ancestor',
416 416 'added': [FileNode('file_A', content='content_of_ancestor')]},
417 417 {'message': 'change',
418 418 'added': [FileNode('file_a', content='content_of_change')]},
419 419 {'message': 'change-child'},
420 420 {'message': 'ancestor-child', 'parents': ['ancestor'],
421 421 'added': [
422 422 FileNode('file_B', content='content_of_ancestor_child')]},
423 423 {'message': 'ancestor-child-2'},
424 424 ]
425 425 commit_ids = backend.create_master_repo(commits)
426 426 target = backend.create_repo(heads=['ancestor-child'])
427 427 source = backend.create_repo(heads=['change'])
428 428
429 429 response = self.app.post(
430 430 route_path('pullrequest_create', repo_name=source.repo_name),
431 431 [
432 432 ('source_repo', source.repo_name),
433 433 ('source_ref', 'branch:default:' + commit_ids['change']),
434 434 ('target_repo', target.repo_name),
435 435 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
436 436 ('common_ancestor', commit_ids['ancestor']),
437 437 ('pullrequest_title', 'Title'),
438 438 ('pullrequest_desc', 'Description'),
439 439 ('description_renderer', 'markdown'),
440 440 ('__start__', 'review_members:sequence'),
441 441 ('__start__', 'reviewer:mapping'),
442 442 ('user_id', '2'),
443 443 ('__start__', 'reasons:sequence'),
444 444 ('reason', 'Some reason'),
445 445 ('__end__', 'reasons:sequence'),
446 446 ('__start__', 'rules:sequence'),
447 447 ('__end__', 'rules:sequence'),
448 448 ('mandatory', 'False'),
449 449 ('__end__', 'reviewer:mapping'),
450 450 ('__end__', 'review_members:sequence'),
451 451 ('__start__', 'revisions:sequence'),
452 452 ('revisions', commit_ids['change']),
453 453 ('__end__', 'revisions:sequence'),
454 454 ('user', ''),
455 455 ('csrf_token', csrf_token),
456 456 ],
457 457 status=302)
458 458
459 459 location = response.headers['Location']
460 460
461 461 pull_request_id = location.rsplit('/', 1)[1]
462 462 assert pull_request_id != 'new'
463 463 pull_request = PullRequest.get(int(pull_request_id))
464 464
465 465 # Check that a notification was made
466 466 notifications = Notification.query()\
467 467 .filter(Notification.created_by == pull_request.author.user_id,
468 468 Notification.type_ == Notification.TYPE_PULL_REQUEST,
469 469 Notification.subject.contains(
470 "wants you to review pull request #%s" % pull_request_id))
470 "requested a pull request review. !%s" % pull_request_id))
471 471 assert len(notifications.all()) == 1
472 472
473 473 # Change reviewers and check that a notification was made
474 474 PullRequestModel().update_reviewers(
475 475 pull_request.pull_request_id, [(1, [], False, [])],
476 476 pull_request.author)
477 477 assert len(notifications.all()) == 2
478 478
479 479 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
480 480 csrf_token):
481 481 commits = [
482 482 {'message': 'ancestor',
483 483 'added': [FileNode('file_A', content='content_of_ancestor')]},
484 484 {'message': 'change',
485 485 'added': [FileNode('file_a', content='content_of_change')]},
486 486 {'message': 'change-child'},
487 487 {'message': 'ancestor-child', 'parents': ['ancestor'],
488 488 'added': [
489 489 FileNode('file_B', content='content_of_ancestor_child')]},
490 490 {'message': 'ancestor-child-2'},
491 491 ]
492 492 commit_ids = backend.create_master_repo(commits)
493 493 target = backend.create_repo(heads=['ancestor-child'])
494 494 source = backend.create_repo(heads=['change'])
495 495
496 496 response = self.app.post(
497 497 route_path('pullrequest_create', repo_name=source.repo_name),
498 498 [
499 499 ('source_repo', source.repo_name),
500 500 ('source_ref', 'branch:default:' + commit_ids['change']),
501 501 ('target_repo', target.repo_name),
502 502 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
503 503 ('common_ancestor', commit_ids['ancestor']),
504 504 ('pullrequest_title', 'Title'),
505 505 ('pullrequest_desc', 'Description'),
506 506 ('description_renderer', 'markdown'),
507 507 ('__start__', 'review_members:sequence'),
508 508 ('__start__', 'reviewer:mapping'),
509 509 ('user_id', '1'),
510 510 ('__start__', 'reasons:sequence'),
511 511 ('reason', 'Some reason'),
512 512 ('__end__', 'reasons:sequence'),
513 513 ('__start__', 'rules:sequence'),
514 514 ('__end__', 'rules:sequence'),
515 515 ('mandatory', 'False'),
516 516 ('__end__', 'reviewer:mapping'),
517 517 ('__end__', 'review_members:sequence'),
518 518 ('__start__', 'revisions:sequence'),
519 519 ('revisions', commit_ids['change']),
520 520 ('__end__', 'revisions:sequence'),
521 521 ('user', ''),
522 522 ('csrf_token', csrf_token),
523 523 ],
524 524 status=302)
525 525
526 526 location = response.headers['Location']
527 527
528 528 pull_request_id = location.rsplit('/', 1)[1]
529 529 assert pull_request_id != 'new'
530 530 pull_request = PullRequest.get(int(pull_request_id))
531 531
532 532 # target_ref has to point to the ancestor's commit_id in order to
533 533 # show the correct diff
534 534 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
535 535 assert pull_request.target_ref == expected_target_ref
536 536
537 537 # Check generated diff contents
538 538 response = response.follow()
539 539 assert 'content_of_ancestor' not in response.body
540 540 assert 'content_of_ancestor-child' not in response.body
541 541 assert 'content_of_change' in response.body
542 542
543 543 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
544 544 # Clear any previous calls to rcextensions
545 545 rhodecode.EXTENSIONS.calls.clear()
546 546
547 547 pull_request = pr_util.create_pull_request(
548 548 approved=True, mergeable=True)
549 549 pull_request_id = pull_request.pull_request_id
550 550 repo_name = pull_request.target_repo.scm_instance().name,
551 551
552 response = self.app.post(
553 route_path('pullrequest_merge',
552 url = route_path('pullrequest_merge',
554 553 repo_name=str(repo_name[0]),
555 pull_request_id=pull_request_id),
556 params={'csrf_token': csrf_token}).follow()
554 pull_request_id=pull_request_id)
555 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
557 556
558 557 pull_request = PullRequest.get(pull_request_id)
559 558
560 559 assert response.status_int == 200
561 560 assert pull_request.is_closed()
562 561 assert_pull_request_status(
563 562 pull_request, ChangesetStatus.STATUS_APPROVED)
564 563
565 564 # Check the relevant log entries were added
566 565 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
567 566 actions = [log.action for log in user_logs]
568 567 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
569 568 expected_actions = [
570 569 u'repo.pull_request.close',
571 570 u'repo.pull_request.merge',
572 571 u'repo.pull_request.comment.create'
573 572 ]
574 573 assert actions == expected_actions
575 574
576 575 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
577 576 actions = [log for log in user_logs]
578 577 assert actions[-1].action == 'user.push'
579 578 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
580 579
581 580 # Check post_push rcextension was really executed
582 581 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
583 582 assert len(push_calls) == 1
584 583 unused_last_call_args, last_call_kwargs = push_calls[0]
585 584 assert last_call_kwargs['action'] == 'push'
586 585 assert last_call_kwargs['commit_ids'] == pr_commit_ids
587 586
588 587 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
589 588 pull_request = pr_util.create_pull_request(mergeable=False)
590 589 pull_request_id = pull_request.pull_request_id
591 590 pull_request = PullRequest.get(pull_request_id)
592 591
593 592 response = self.app.post(
594 593 route_path('pullrequest_merge',
595 594 repo_name=pull_request.target_repo.scm_instance().name,
596 595 pull_request_id=pull_request.pull_request_id),
597 596 params={'csrf_token': csrf_token}).follow()
598 597
599 598 assert response.status_int == 200
600 599 response.mustcontain(
601 600 'Merge is not currently possible because of below failed checks.')
602 601 response.mustcontain('Server-side pull request merging is disabled.')
603 602
604 603 @pytest.mark.skip_backends('svn')
605 604 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
606 605 pull_request = pr_util.create_pull_request(mergeable=True)
607 606 pull_request_id = pull_request.pull_request_id
608 607 repo_name = pull_request.target_repo.scm_instance().name
609 608
610 609 response = self.app.post(
611 610 route_path('pullrequest_merge',
612 611 repo_name=repo_name, pull_request_id=pull_request_id),
613 612 params={'csrf_token': csrf_token}).follow()
614 613
615 614 assert response.status_int == 200
616 615
617 616 response.mustcontain(
618 617 'Merge is not currently possible because of below failed checks.')
619 618 response.mustcontain('Pull request reviewer approval is pending.')
620 619
621 620 def test_merge_pull_request_renders_failure_reason(
622 621 self, user_regular, csrf_token, pr_util):
623 622 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
624 623 pull_request_id = pull_request.pull_request_id
625 624 repo_name = pull_request.target_repo.scm_instance().name
626 625
627 626 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
628 627 MergeFailureReason.PUSH_FAILED,
629 628 metadata={'target': 'shadow repo',
630 629 'merge_commit': 'xxx'})
631 630 model_patcher = mock.patch.multiple(
632 631 PullRequestModel,
633 632 merge_repo=mock.Mock(return_value=merge_resp),
634 633 merge_status=mock.Mock(return_value=(True, 'WRONG_MESSAGE')))
635 634
636 635 with model_patcher:
637 636 response = self.app.post(
638 637 route_path('pullrequest_merge',
639 638 repo_name=repo_name,
640 639 pull_request_id=pull_request_id),
641 640 params={'csrf_token': csrf_token}, status=302)
642 641
643 642 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
644 643 metadata={'target': 'shadow repo',
645 644 'merge_commit': 'xxx'})
646 645 assert_session_flash(response, merge_resp.merge_status_message)
647 646
648 647 def test_update_source_revision(self, backend, csrf_token):
649 648 commits = [
650 649 {'message': 'ancestor'},
651 650 {'message': 'change'},
652 651 {'message': 'change-2'},
653 652 ]
654 653 commit_ids = backend.create_master_repo(commits)
655 654 target = backend.create_repo(heads=['ancestor'])
656 655 source = backend.create_repo(heads=['change'])
657 656
658 657 # create pr from a in source to A in target
659 658 pull_request = PullRequest()
660 659
661 660 pull_request.source_repo = source
662 661 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
663 662 branch=backend.default_branch_name, commit_id=commit_ids['change'])
664 663
665 664 pull_request.target_repo = target
666 665 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
667 666 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
668 667
669 668 pull_request.revisions = [commit_ids['change']]
670 669 pull_request.title = u"Test"
671 670 pull_request.description = u"Description"
672 671 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
673 672 pull_request.pull_request_state = PullRequest.STATE_CREATED
674 673 Session().add(pull_request)
675 674 Session().commit()
676 675 pull_request_id = pull_request.pull_request_id
677 676
678 677 # source has ancestor - change - change-2
679 678 backend.pull_heads(source, heads=['change-2'])
680 679
681 680 # update PR
682 681 self.app.post(
683 682 route_path('pullrequest_update',
684 683 repo_name=target.repo_name, pull_request_id=pull_request_id),
685 684 params={'update_commits': 'true', 'csrf_token': csrf_token})
686 685
687 686 response = self.app.get(
688 687 route_path('pullrequest_show',
689 688 repo_name=target.repo_name,
690 689 pull_request_id=pull_request.pull_request_id))
691 690
692 691 assert response.status_int == 200
693 692 assert 'Pull request updated to' in response.body
694 693 assert 'with 1 added, 0 removed commits.' in response.body
695 694
696 695 # check that we have now both revisions
697 696 pull_request = PullRequest.get(pull_request_id)
698 697 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
699 698
700 699 def test_update_target_revision(self, backend, csrf_token):
701 700 commits = [
702 701 {'message': 'ancestor'},
703 702 {'message': 'change'},
704 703 {'message': 'ancestor-new', 'parents': ['ancestor']},
705 704 {'message': 'change-rebased'},
706 705 ]
707 706 commit_ids = backend.create_master_repo(commits)
708 707 target = backend.create_repo(heads=['ancestor'])
709 708 source = backend.create_repo(heads=['change'])
710 709
711 710 # create pr from a in source to A in target
712 711 pull_request = PullRequest()
713 712
714 713 pull_request.source_repo = source
715 714 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
716 715 branch=backend.default_branch_name, commit_id=commit_ids['change'])
717 716
718 717 pull_request.target_repo = target
719 718 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
720 719 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
721 720
722 721 pull_request.revisions = [commit_ids['change']]
723 722 pull_request.title = u"Test"
724 723 pull_request.description = u"Description"
725 724 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
726 725 pull_request.pull_request_state = PullRequest.STATE_CREATED
727 726
728 727 Session().add(pull_request)
729 728 Session().commit()
730 729 pull_request_id = pull_request.pull_request_id
731 730
732 731 # target has ancestor - ancestor-new
733 732 # source has ancestor - ancestor-new - change-rebased
734 733 backend.pull_heads(target, heads=['ancestor-new'])
735 734 backend.pull_heads(source, heads=['change-rebased'])
736 735
737 736 # update PR
738 self.app.post(
739 route_path('pullrequest_update',
737 url = route_path('pullrequest_update',
740 738 repo_name=target.repo_name,
741 pull_request_id=pull_request_id),
739 pull_request_id=pull_request_id)
740 self.app.post(url,
742 741 params={'update_commits': 'true', 'csrf_token': csrf_token},
743 742 status=200)
744 743
745 744 # check that we have now both revisions
746 745 pull_request = PullRequest.get(pull_request_id)
747 746 assert pull_request.revisions == [commit_ids['change-rebased']]
748 747 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
749 748 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
750 749
751 750 response = self.app.get(
752 751 route_path('pullrequest_show',
753 752 repo_name=target.repo_name,
754 753 pull_request_id=pull_request.pull_request_id))
755 754 assert response.status_int == 200
756 755 assert 'Pull request updated to' in response.body
757 756 assert 'with 1 added, 1 removed commits.' in response.body
758 757
759 758 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
760 759 backend = backend_git
761 760 commits = [
762 761 {'message': 'master-commit-1'},
763 762 {'message': 'master-commit-2-change-1'},
764 763 {'message': 'master-commit-3-change-2'},
765 764
766 765 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
767 766 {'message': 'feat-commit-2'},
768 767 ]
769 768 commit_ids = backend.create_master_repo(commits)
770 769 target = backend.create_repo(heads=['master-commit-3-change-2'])
771 770 source = backend.create_repo(heads=['feat-commit-2'])
772 771
773 772 # create pr from a in source to A in target
774 773 pull_request = PullRequest()
775 774 pull_request.source_repo = source
776 775
777 776 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
778 777 branch=backend.default_branch_name,
779 778 commit_id=commit_ids['master-commit-3-change-2'])
780 779
781 780 pull_request.target_repo = target
782 781 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
783 782 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
784 783
785 784 pull_request.revisions = [
786 785 commit_ids['feat-commit-1'],
787 786 commit_ids['feat-commit-2']
788 787 ]
789 788 pull_request.title = u"Test"
790 789 pull_request.description = u"Description"
791 790 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
792 791 pull_request.pull_request_state = PullRequest.STATE_CREATED
793 792 Session().add(pull_request)
794 793 Session().commit()
795 794 pull_request_id = pull_request.pull_request_id
796 795
797 796 # PR is created, now we simulate a force-push into target,
798 797 # that drops a 2 last commits
799 798 vcsrepo = target.scm_instance()
800 799 vcsrepo.config.clear_section('hooks')
801 800 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
802 801
803 802 # update PR
804 self.app.post(
805 route_path('pullrequest_update',
803 url = route_path('pullrequest_update',
806 804 repo_name=target.repo_name,
807 pull_request_id=pull_request_id),
805 pull_request_id=pull_request_id)
806 self.app.post(url,
808 807 params={'update_commits': 'true', 'csrf_token': csrf_token},
809 808 status=200)
810 809
811 810 response = self.app.get(route_path('pullrequest_new', repo_name=target.repo_name))
812 811 assert response.status_int == 200
813 812 response.mustcontain('Pull request updated to')
814 813 response.mustcontain('with 0 added, 0 removed commits.')
815 814
816 815 def test_update_of_ancestor_reference(self, backend, csrf_token):
817 816 commits = [
818 817 {'message': 'ancestor'},
819 818 {'message': 'change'},
820 819 {'message': 'change-2'},
821 820 {'message': 'ancestor-new', 'parents': ['ancestor']},
822 821 {'message': 'change-rebased'},
823 822 ]
824 823 commit_ids = backend.create_master_repo(commits)
825 824 target = backend.create_repo(heads=['ancestor'])
826 825 source = backend.create_repo(heads=['change'])
827 826
828 827 # create pr from a in source to A in target
829 828 pull_request = PullRequest()
830 829 pull_request.source_repo = source
831 830
832 831 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
833 832 branch=backend.default_branch_name, commit_id=commit_ids['change'])
834 833 pull_request.target_repo = target
835 834 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
836 835 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
837 836 pull_request.revisions = [commit_ids['change']]
838 837 pull_request.title = u"Test"
839 838 pull_request.description = u"Description"
840 839 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
841 840 pull_request.pull_request_state = PullRequest.STATE_CREATED
842 841 Session().add(pull_request)
843 842 Session().commit()
844 843 pull_request_id = pull_request.pull_request_id
845 844
846 845 # target has ancestor - ancestor-new
847 846 # source has ancestor - ancestor-new - change-rebased
848 847 backend.pull_heads(target, heads=['ancestor-new'])
849 848 backend.pull_heads(source, heads=['change-rebased'])
850 849
851 850 # update PR
852 851 self.app.post(
853 852 route_path('pullrequest_update',
854 853 repo_name=target.repo_name, pull_request_id=pull_request_id),
855 854 params={'update_commits': 'true', 'csrf_token': csrf_token},
856 855 status=200)
857 856
858 857 # Expect the target reference to be updated correctly
859 858 pull_request = PullRequest.get(pull_request_id)
860 859 assert pull_request.revisions == [commit_ids['change-rebased']]
861 860 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
862 861 branch=backend.default_branch_name,
863 862 commit_id=commit_ids['ancestor-new'])
864 863 assert pull_request.target_ref == expected_target_ref
865 864
866 865 def test_remove_pull_request_branch(self, backend_git, csrf_token):
867 866 branch_name = 'development'
868 867 commits = [
869 868 {'message': 'initial-commit'},
870 869 {'message': 'old-feature'},
871 870 {'message': 'new-feature', 'branch': branch_name},
872 871 ]
873 872 repo = backend_git.create_repo(commits)
874 873 repo_name = repo.repo_name
875 874 commit_ids = backend_git.commit_ids
876 875
877 876 pull_request = PullRequest()
878 877 pull_request.source_repo = repo
879 878 pull_request.target_repo = repo
880 879 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
881 880 branch=branch_name, commit_id=commit_ids['new-feature'])
882 881 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
883 882 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
884 883 pull_request.revisions = [commit_ids['new-feature']]
885 884 pull_request.title = u"Test"
886 885 pull_request.description = u"Description"
887 886 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
888 887 pull_request.pull_request_state = PullRequest.STATE_CREATED
889 888 Session().add(pull_request)
890 889 Session().commit()
891 890
892 891 pull_request_id = pull_request.pull_request_id
893 892
894 893 vcs = repo.scm_instance()
895 894 vcs.remove_ref('refs/heads/{}'.format(branch_name))
896 895
897 896 response = self.app.get(route_path(
898 897 'pullrequest_show',
899 898 repo_name=repo_name,
900 899 pull_request_id=pull_request_id))
901 900
902 901 assert response.status_int == 200
903 902
904 903 response.assert_response().element_contains(
905 904 '#changeset_compare_view_content .alert strong',
906 905 'Missing commits')
907 906 response.assert_response().element_contains(
908 907 '#changeset_compare_view_content .alert',
909 908 'This pull request cannot be displayed, because one or more'
910 909 ' commits no longer exist in the source repository.')
911 910
912 911 def test_strip_commits_from_pull_request(
913 912 self, backend, pr_util, csrf_token):
914 913 commits = [
915 914 {'message': 'initial-commit'},
916 915 {'message': 'old-feature'},
917 916 {'message': 'new-feature', 'parents': ['initial-commit']},
918 917 ]
919 918 pull_request = pr_util.create_pull_request(
920 919 commits, target_head='initial-commit', source_head='new-feature',
921 920 revisions=['new-feature'])
922 921
923 922 vcs = pr_util.source_repository.scm_instance()
924 923 if backend.alias == 'git':
925 924 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
926 925 else:
927 926 vcs.strip(pr_util.commit_ids['new-feature'])
928 927
929 928 response = self.app.get(route_path(
930 929 'pullrequest_show',
931 930 repo_name=pr_util.target_repository.repo_name,
932 931 pull_request_id=pull_request.pull_request_id))
933 932
934 933 assert response.status_int == 200
935 934
936 935 response.assert_response().element_contains(
937 936 '#changeset_compare_view_content .alert strong',
938 937 'Missing commits')
939 938 response.assert_response().element_contains(
940 939 '#changeset_compare_view_content .alert',
941 940 'This pull request cannot be displayed, because one or more'
942 941 ' commits no longer exist in the source repository.')
943 942 response.assert_response().element_contains(
944 943 '#update_commits',
945 944 'Update commits')
946 945
947 946 def test_strip_commits_and_update(
948 947 self, backend, pr_util, csrf_token):
949 948 commits = [
950 949 {'message': 'initial-commit'},
951 950 {'message': 'old-feature'},
952 951 {'message': 'new-feature', 'parents': ['old-feature']},
953 952 ]
954 953 pull_request = pr_util.create_pull_request(
955 954 commits, target_head='old-feature', source_head='new-feature',
956 955 revisions=['new-feature'], mergeable=True)
957 956
958 957 vcs = pr_util.source_repository.scm_instance()
959 958 if backend.alias == 'git':
960 959 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
961 960 else:
962 961 vcs.strip(pr_util.commit_ids['new-feature'])
963 962
964 response = self.app.post(
965 route_path('pullrequest_update',
963 url = route_path('pullrequest_update',
966 964 repo_name=pull_request.target_repo.repo_name,
967 pull_request_id=pull_request.pull_request_id),
965 pull_request_id=pull_request.pull_request_id)
966 response = self.app.post(url,
968 967 params={'update_commits': 'true',
969 968 'csrf_token': csrf_token})
970 969
971 970 assert response.status_int == 200
972 971 assert response.body == 'true'
973 972
974 973 # Make sure that after update, it won't raise 500 errors
975 974 response = self.app.get(route_path(
976 975 'pullrequest_show',
977 976 repo_name=pr_util.target_repository.repo_name,
978 977 pull_request_id=pull_request.pull_request_id))
979 978
980 979 assert response.status_int == 200
981 980 response.assert_response().element_contains(
982 981 '#changeset_compare_view_content .alert strong',
983 982 'Missing commits')
984 983
985 984 def test_branch_is_a_link(self, pr_util):
986 985 pull_request = pr_util.create_pull_request()
987 986 pull_request.source_ref = 'branch:origin:1234567890abcdef'
988 987 pull_request.target_ref = 'branch:target:abcdef1234567890'
989 988 Session().add(pull_request)
990 989 Session().commit()
991 990
992 991 response = self.app.get(route_path(
993 992 'pullrequest_show',
994 993 repo_name=pull_request.target_repo.scm_instance().name,
995 994 pull_request_id=pull_request.pull_request_id))
996 995 assert response.status_int == 200
997 996
998 997 origin = response.assert_response().get_element('.pr-origininfo .tag')
999 998 origin_children = origin.getchildren()
1000 999 assert len(origin_children) == 1
1001 1000 target = response.assert_response().get_element('.pr-targetinfo .tag')
1002 1001 target_children = target.getchildren()
1003 1002 assert len(target_children) == 1
1004 1003
1005 1004 expected_origin_link = route_path(
1006 1005 'repo_commits',
1007 1006 repo_name=pull_request.source_repo.scm_instance().name,
1008 1007 params=dict(branch='origin'))
1009 1008 expected_target_link = route_path(
1010 1009 'repo_commits',
1011 1010 repo_name=pull_request.target_repo.scm_instance().name,
1012 1011 params=dict(branch='target'))
1013 1012 assert origin_children[0].attrib['href'] == expected_origin_link
1014 1013 assert origin_children[0].text == 'branch: origin'
1015 1014 assert target_children[0].attrib['href'] == expected_target_link
1016 1015 assert target_children[0].text == 'branch: target'
1017 1016
1018 1017 def test_bookmark_is_not_a_link(self, pr_util):
1019 1018 pull_request = pr_util.create_pull_request()
1020 1019 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1021 1020 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1022 1021 Session().add(pull_request)
1023 1022 Session().commit()
1024 1023
1025 1024 response = self.app.get(route_path(
1026 1025 'pullrequest_show',
1027 1026 repo_name=pull_request.target_repo.scm_instance().name,
1028 1027 pull_request_id=pull_request.pull_request_id))
1029 1028 assert response.status_int == 200
1030 1029
1031 1030 origin = response.assert_response().get_element('.pr-origininfo .tag')
1032 1031 assert origin.text.strip() == 'bookmark: origin'
1033 1032 assert origin.getchildren() == []
1034 1033
1035 1034 target = response.assert_response().get_element('.pr-targetinfo .tag')
1036 1035 assert target.text.strip() == 'bookmark: target'
1037 1036 assert target.getchildren() == []
1038 1037
1039 1038 def test_tag_is_not_a_link(self, pr_util):
1040 1039 pull_request = pr_util.create_pull_request()
1041 1040 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1042 1041 pull_request.target_ref = 'tag:target:abcdef1234567890'
1043 1042 Session().add(pull_request)
1044 1043 Session().commit()
1045 1044
1046 1045 response = self.app.get(route_path(
1047 1046 'pullrequest_show',
1048 1047 repo_name=pull_request.target_repo.scm_instance().name,
1049 1048 pull_request_id=pull_request.pull_request_id))
1050 1049 assert response.status_int == 200
1051 1050
1052 1051 origin = response.assert_response().get_element('.pr-origininfo .tag')
1053 1052 assert origin.text.strip() == 'tag: origin'
1054 1053 assert origin.getchildren() == []
1055 1054
1056 1055 target = response.assert_response().get_element('.pr-targetinfo .tag')
1057 1056 assert target.text.strip() == 'tag: target'
1058 1057 assert target.getchildren() == []
1059 1058
1060 1059 @pytest.mark.parametrize('mergeable', [True, False])
1061 1060 def test_shadow_repository_link(
1062 1061 self, mergeable, pr_util, http_host_only_stub):
1063 1062 """
1064 1063 Check that the pull request summary page displays a link to the shadow
1065 1064 repository if the pull request is mergeable. If it is not mergeable
1066 1065 the link should not be displayed.
1067 1066 """
1068 1067 pull_request = pr_util.create_pull_request(
1069 1068 mergeable=mergeable, enable_notifications=False)
1070 1069 target_repo = pull_request.target_repo.scm_instance()
1071 1070 pr_id = pull_request.pull_request_id
1072 1071 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1073 1072 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1074 1073
1075 1074 response = self.app.get(route_path(
1076 1075 'pullrequest_show',
1077 1076 repo_name=target_repo.name,
1078 1077 pull_request_id=pr_id))
1079 1078
1080 1079 if mergeable:
1081 1080 response.assert_response().element_value_contains(
1082 1081 'input.pr-mergeinfo', shadow_url)
1083 1082 response.assert_response().element_value_contains(
1084 1083 'input.pr-mergeinfo ', 'pr-merge')
1085 1084 else:
1086 1085 response.assert_response().no_element_exists('.pr-mergeinfo')
1087 1086
1088 1087
1089 1088 @pytest.mark.usefixtures('app')
1090 1089 @pytest.mark.backends("git", "hg")
1091 1090 class TestPullrequestsControllerDelete(object):
1092 1091 def test_pull_request_delete_button_permissions_admin(
1093 1092 self, autologin_user, user_admin, pr_util):
1094 1093 pull_request = pr_util.create_pull_request(
1095 1094 author=user_admin.username, enable_notifications=False)
1096 1095
1097 1096 response = self.app.get(route_path(
1098 1097 'pullrequest_show',
1099 1098 repo_name=pull_request.target_repo.scm_instance().name,
1100 1099 pull_request_id=pull_request.pull_request_id))
1101 1100
1102 1101 response.mustcontain('id="delete_pullrequest"')
1103 1102 response.mustcontain('Confirm to delete this pull request')
1104 1103
1105 1104 def test_pull_request_delete_button_permissions_owner(
1106 1105 self, autologin_regular_user, user_regular, pr_util):
1107 1106 pull_request = pr_util.create_pull_request(
1108 1107 author=user_regular.username, enable_notifications=False)
1109 1108
1110 1109 response = self.app.get(route_path(
1111 1110 'pullrequest_show',
1112 1111 repo_name=pull_request.target_repo.scm_instance().name,
1113 1112 pull_request_id=pull_request.pull_request_id))
1114 1113
1115 1114 response.mustcontain('id="delete_pullrequest"')
1116 1115 response.mustcontain('Confirm to delete this pull request')
1117 1116
1118 1117 def test_pull_request_delete_button_permissions_forbidden(
1119 1118 self, autologin_regular_user, user_regular, user_admin, pr_util):
1120 1119 pull_request = pr_util.create_pull_request(
1121 1120 author=user_admin.username, enable_notifications=False)
1122 1121
1123 1122 response = self.app.get(route_path(
1124 1123 'pullrequest_show',
1125 1124 repo_name=pull_request.target_repo.scm_instance().name,
1126 1125 pull_request_id=pull_request.pull_request_id))
1127 1126 response.mustcontain(no=['id="delete_pullrequest"'])
1128 1127 response.mustcontain(no=['Confirm to delete this pull request'])
1129 1128
1130 1129 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1131 1130 self, autologin_regular_user, user_regular, user_admin, pr_util,
1132 1131 user_util):
1133 1132
1134 1133 pull_request = pr_util.create_pull_request(
1135 1134 author=user_admin.username, enable_notifications=False)
1136 1135
1137 1136 user_util.grant_user_permission_to_repo(
1138 1137 pull_request.target_repo, user_regular,
1139 1138 'repository.write')
1140 1139
1141 1140 response = self.app.get(route_path(
1142 1141 'pullrequest_show',
1143 1142 repo_name=pull_request.target_repo.scm_instance().name,
1144 1143 pull_request_id=pull_request.pull_request_id))
1145 1144
1146 1145 response.mustcontain('id="open_edit_pullrequest"')
1147 1146 response.mustcontain('id="delete_pullrequest"')
1148 1147 response.mustcontain(no=['Confirm to delete this pull request'])
1149 1148
1150 1149 def test_delete_comment_returns_404_if_comment_does_not_exist(
1151 1150 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1152 1151
1153 1152 pull_request = pr_util.create_pull_request(
1154 1153 author=user_admin.username, enable_notifications=False)
1155 1154
1156 1155 self.app.post(
1157 1156 route_path(
1158 1157 'pullrequest_comment_delete',
1159 1158 repo_name=pull_request.target_repo.scm_instance().name,
1160 1159 pull_request_id=pull_request.pull_request_id,
1161 1160 comment_id=1024404),
1162 1161 extra_environ=xhr_header,
1163 1162 params={'csrf_token': csrf_token},
1164 1163 status=404
1165 1164 )
1166 1165
1167 1166 def test_delete_comment(
1168 1167 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1169 1168
1170 1169 pull_request = pr_util.create_pull_request(
1171 1170 author=user_admin.username, enable_notifications=False)
1172 1171 comment = pr_util.create_comment()
1173 1172 comment_id = comment.comment_id
1174 1173
1175 1174 response = self.app.post(
1176 1175 route_path(
1177 1176 'pullrequest_comment_delete',
1178 1177 repo_name=pull_request.target_repo.scm_instance().name,
1179 1178 pull_request_id=pull_request.pull_request_id,
1180 1179 comment_id=comment_id),
1181 1180 extra_environ=xhr_header,
1182 1181 params={'csrf_token': csrf_token},
1183 1182 status=200
1184 1183 )
1185 1184 assert response.body == 'true'
1186 1185
1187 1186 @pytest.mark.parametrize('url_type', [
1188 1187 'pullrequest_new',
1189 1188 'pullrequest_create',
1190 1189 'pullrequest_update',
1191 1190 'pullrequest_merge',
1192 1191 ])
1193 1192 def test_pull_request_is_forbidden_on_archived_repo(
1194 1193 self, autologin_user, backend, xhr_header, user_util, url_type):
1195 1194
1196 1195 # create a temporary repo
1197 1196 source = user_util.create_repo(repo_type=backend.alias)
1198 1197 repo_name = source.repo_name
1199 1198 repo = Repository.get_by_repo_name(repo_name)
1200 1199 repo.archived = True
1201 1200 Session().commit()
1202 1201
1203 1202 response = self.app.get(
1204 1203 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1205 1204
1206 1205 msg = 'Action not supported for archived repository.'
1207 1206 assert_session_flash(response, msg)
1208 1207
1209 1208
1210 1209 def assert_pull_request_status(pull_request, expected_status):
1211 status = ChangesetStatusModel().calculated_review_status(
1212 pull_request=pull_request)
1210 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1213 1211 assert status == expected_status
1214 1212
1215 1213
1216 1214 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1217 1215 @pytest.mark.usefixtures("autologin_user")
1218 1216 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1219 response = app.get(
1220 route_path(route, repo_name=backend_svn.repo_name), status=404)
1221
1217 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,734 +1,740 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from pyramid.threadlocal import get_current_registry, get_current_request
30 30 from sqlalchemy.sql.expression import null
31 31 from sqlalchemy.sql.functions import coalesce
32 32
33 33 from rhodecode.lib import helpers as h, diffs, channelstream
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 36 from rhodecode.model import BaseModel
37 37 from rhodecode.model.db import (
38 38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
39 39 from rhodecode.model.notification import NotificationModel
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.settings import VcsSettingsModel
42 42 from rhodecode.model.notification import EmailNotificationModel
43 43 from rhodecode.model.validation_schema.schemas import comment_schema
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class CommentsModel(BaseModel):
50 50
51 51 cls = ChangesetComment
52 52
53 53 DIFF_CONTEXT_BEFORE = 3
54 54 DIFF_CONTEXT_AFTER = 3
55 55
56 56 def __get_commit_comment(self, changeset_comment):
57 57 return self._get_instance(ChangesetComment, changeset_comment)
58 58
59 59 def __get_pull_request(self, pull_request):
60 60 return self._get_instance(PullRequest, pull_request)
61 61
62 62 def _extract_mentions(self, s):
63 63 user_objects = []
64 64 for username in extract_mentioned_users(s):
65 65 user_obj = User.get_by_username(username, case_insensitive=True)
66 66 if user_obj:
67 67 user_objects.append(user_obj)
68 68 return user_objects
69 69
70 70 def _get_renderer(self, global_renderer='rst', request=None):
71 71 request = request or get_current_request()
72 72
73 73 try:
74 74 global_renderer = request.call_context.visual.default_renderer
75 75 except AttributeError:
76 76 log.debug("Renderer not set, falling back "
77 77 "to default renderer '%s'", global_renderer)
78 78 except Exception:
79 79 log.error(traceback.format_exc())
80 80 return global_renderer
81 81
82 82 def aggregate_comments(self, comments, versions, show_version, inline=False):
83 83 # group by versions, and count until, and display objects
84 84
85 85 comment_groups = collections.defaultdict(list)
86 86 [comment_groups[
87 87 _co.pull_request_version_id].append(_co) for _co in comments]
88 88
89 89 def yield_comments(pos):
90 90 for co in comment_groups[pos]:
91 91 yield co
92 92
93 93 comment_versions = collections.defaultdict(
94 94 lambda: collections.defaultdict(list))
95 95 prev_prvid = -1
96 96 # fake last entry with None, to aggregate on "latest" version which
97 97 # doesn't have an pull_request_version_id
98 98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
99 99 prvid = ver.pull_request_version_id
100 100 if prev_prvid == -1:
101 101 prev_prvid = prvid
102 102
103 103 for co in yield_comments(prvid):
104 104 comment_versions[prvid]['at'].append(co)
105 105
106 106 # save until
107 107 current = comment_versions[prvid]['at']
108 108 prev_until = comment_versions[prev_prvid]['until']
109 109 cur_until = prev_until + current
110 110 comment_versions[prvid]['until'].extend(cur_until)
111 111
112 112 # save outdated
113 113 if inline:
114 114 outdated = [x for x in cur_until
115 115 if x.outdated_at_version(show_version)]
116 116 else:
117 117 outdated = [x for x in cur_until
118 118 if x.older_than_version(show_version)]
119 119 display = [x for x in cur_until if x not in outdated]
120 120
121 121 comment_versions[prvid]['outdated'] = outdated
122 122 comment_versions[prvid]['display'] = display
123 123
124 124 prev_prvid = prvid
125 125
126 126 return comment_versions
127 127
128 128 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
129 129 qry = Session().query(ChangesetComment) \
130 130 .filter(ChangesetComment.repo == repo)
131 131
132 132 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
133 133 qry = qry.filter(ChangesetComment.comment_type == comment_type)
134 134
135 135 if user:
136 136 user = self._get_user(user)
137 137 if user:
138 138 qry = qry.filter(ChangesetComment.user_id == user.user_id)
139 139
140 140 if commit_id:
141 141 qry = qry.filter(ChangesetComment.revision == commit_id)
142 142
143 143 qry = qry.order_by(ChangesetComment.created_on)
144 144 return qry.all()
145 145
146 146 def get_repository_unresolved_todos(self, repo):
147 147 todos = Session().query(ChangesetComment) \
148 148 .filter(ChangesetComment.repo == repo) \
149 149 .filter(ChangesetComment.resolved_by == None) \
150 150 .filter(ChangesetComment.comment_type
151 151 == ChangesetComment.COMMENT_TYPE_TODO)
152 152 todos = todos.all()
153 153
154 154 return todos
155 155
156 156 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
157 157
158 158 todos = Session().query(ChangesetComment) \
159 159 .filter(ChangesetComment.pull_request == pull_request) \
160 160 .filter(ChangesetComment.resolved_by == None) \
161 161 .filter(ChangesetComment.comment_type
162 162 == ChangesetComment.COMMENT_TYPE_TODO)
163 163
164 164 if not show_outdated:
165 165 todos = todos.filter(
166 166 coalesce(ChangesetComment.display_state, '') !=
167 167 ChangesetComment.COMMENT_OUTDATED)
168 168
169 169 todos = todos.all()
170 170
171 171 return todos
172 172
173 173 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
174 174
175 175 todos = Session().query(ChangesetComment) \
176 176 .filter(ChangesetComment.pull_request == pull_request) \
177 177 .filter(ChangesetComment.resolved_by != None) \
178 178 .filter(ChangesetComment.comment_type
179 179 == ChangesetComment.COMMENT_TYPE_TODO)
180 180
181 181 if not show_outdated:
182 182 todos = todos.filter(
183 183 coalesce(ChangesetComment.display_state, '') !=
184 184 ChangesetComment.COMMENT_OUTDATED)
185 185
186 186 todos = todos.all()
187 187
188 188 return todos
189 189
190 190 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
191 191
192 192 todos = Session().query(ChangesetComment) \
193 193 .filter(ChangesetComment.revision == commit_id) \
194 194 .filter(ChangesetComment.resolved_by == None) \
195 195 .filter(ChangesetComment.comment_type
196 196 == ChangesetComment.COMMENT_TYPE_TODO)
197 197
198 198 if not show_outdated:
199 199 todos = todos.filter(
200 200 coalesce(ChangesetComment.display_state, '') !=
201 201 ChangesetComment.COMMENT_OUTDATED)
202 202
203 203 todos = todos.all()
204 204
205 205 return todos
206 206
207 207 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
208 208
209 209 todos = Session().query(ChangesetComment) \
210 210 .filter(ChangesetComment.revision == commit_id) \
211 211 .filter(ChangesetComment.resolved_by != None) \
212 212 .filter(ChangesetComment.comment_type
213 213 == ChangesetComment.COMMENT_TYPE_TODO)
214 214
215 215 if not show_outdated:
216 216 todos = todos.filter(
217 217 coalesce(ChangesetComment.display_state, '') !=
218 218 ChangesetComment.COMMENT_OUTDATED)
219 219
220 220 todos = todos.all()
221 221
222 222 return todos
223 223
224 224 def _log_audit_action(self, action, action_data, auth_user, comment):
225 225 audit_logger.store(
226 226 action=action,
227 227 action_data=action_data,
228 228 user=auth_user,
229 229 repo=comment.repo)
230 230
231 231 def create(self, text, repo, user, commit_id=None, pull_request=None,
232 232 f_path=None, line_no=None, status_change=None,
233 233 status_change_type=None, comment_type=None,
234 234 resolves_comment_id=None, closing_pr=False, send_email=True,
235 235 renderer=None, auth_user=None):
236 236 """
237 237 Creates new comment for commit or pull request.
238 238 IF status_change is not none this comment is associated with a
239 239 status change of commit or commit associated with pull request
240 240
241 241 :param text:
242 242 :param repo:
243 243 :param user:
244 244 :param commit_id:
245 245 :param pull_request:
246 246 :param f_path:
247 247 :param line_no:
248 248 :param status_change: Label for status change
249 249 :param comment_type: Type of comment
250 250 :param status_change_type: type of status change
251 251 :param closing_pr:
252 252 :param send_email:
253 253 :param renderer: pick renderer for this comment
254 254 """
255 255
256 256 if not text:
257 257 log.warning('Missing text for comment, skipping...')
258 258 return
259 259 request = get_current_request()
260 260 _ = request.translate
261 261
262 262 if not renderer:
263 263 renderer = self._get_renderer(request=request)
264 264
265 265 repo = self._get_repo(repo)
266 266 user = self._get_user(user)
267 267 auth_user = auth_user or user
268 268
269 269 schema = comment_schema.CommentSchema()
270 270 validated_kwargs = schema.deserialize(dict(
271 271 comment_body=text,
272 272 comment_type=comment_type,
273 273 comment_file=f_path,
274 274 comment_line=line_no,
275 275 renderer_type=renderer,
276 276 status_change=status_change_type,
277 277 resolves_comment_id=resolves_comment_id,
278 278 repo=repo.repo_id,
279 279 user=user.user_id,
280 280 ))
281 281
282 282 comment = ChangesetComment()
283 283 comment.renderer = validated_kwargs['renderer_type']
284 284 comment.text = validated_kwargs['comment_body']
285 285 comment.f_path = validated_kwargs['comment_file']
286 286 comment.line_no = validated_kwargs['comment_line']
287 287 comment.comment_type = validated_kwargs['comment_type']
288 288
289 289 comment.repo = repo
290 290 comment.author = user
291 291 resolved_comment = self.__get_commit_comment(
292 292 validated_kwargs['resolves_comment_id'])
293 293 # check if the comment actually belongs to this PR
294 294 if resolved_comment and resolved_comment.pull_request and \
295 295 resolved_comment.pull_request != pull_request:
296 296 log.warning('Comment tried to resolved unrelated todo comment: %s',
297 297 resolved_comment)
298 298 # comment not bound to this pull request, forbid
299 299 resolved_comment = None
300 300
301 301 elif resolved_comment and resolved_comment.repo and \
302 302 resolved_comment.repo != repo:
303 303 log.warning('Comment tried to resolved unrelated todo comment: %s',
304 304 resolved_comment)
305 305 # comment not bound to this repo, forbid
306 306 resolved_comment = None
307 307
308 308 comment.resolved_comment = resolved_comment
309 309
310 310 pull_request_id = pull_request
311 311
312 312 commit_obj = None
313 313 pull_request_obj = None
314 314
315 315 if commit_id:
316 316 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
317 317 # do a lookup, so we don't pass something bad here
318 318 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
319 319 comment.revision = commit_obj.raw_id
320 320
321 321 elif pull_request_id:
322 322 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
323 323 pull_request_obj = self.__get_pull_request(pull_request_id)
324 324 comment.pull_request = pull_request_obj
325 325 else:
326 326 raise Exception('Please specify commit or pull_request_id')
327 327
328 328 Session().add(comment)
329 329 Session().flush()
330 330 kwargs = {
331 331 'user': user,
332 332 'renderer_type': renderer,
333 333 'repo_name': repo.repo_name,
334 334 'status_change': status_change,
335 335 'status_change_type': status_change_type,
336 336 'comment_body': text,
337 337 'comment_file': f_path,
338 338 'comment_line': line_no,
339 339 'comment_type': comment_type or 'note'
340 340 }
341 341
342 342 if commit_obj:
343 343 recipients = ChangesetComment.get_users(
344 344 revision=commit_obj.raw_id)
345 345 # add commit author if it's in RhodeCode system
346 346 cs_author = User.get_from_cs_author(commit_obj.author)
347 347 if not cs_author:
348 348 # use repo owner if we cannot extract the author correctly
349 349 cs_author = repo.user
350 350 recipients += [cs_author]
351 351
352 352 commit_comment_url = self.get_url(comment, request=request)
353 353
354 354 target_repo_url = h.link_to(
355 355 repo.repo_name,
356 356 h.route_url('repo_summary', repo_name=repo.repo_name))
357 357
358 358 # commit specifics
359 359 kwargs.update({
360 360 'commit': commit_obj,
361 361 'commit_message': commit_obj.message,
362 'commit_target_repo': target_repo_url,
362 'commit_target_repo_url': target_repo_url,
363 363 'commit_comment_url': commit_comment_url,
364 364 })
365 365
366 366 elif pull_request_obj:
367 367 # get the current participants of this pull request
368 368 recipients = ChangesetComment.get_users(
369 369 pull_request_id=pull_request_obj.pull_request_id)
370 370 # add pull request author
371 371 recipients += [pull_request_obj.author]
372 372
373 373 # add the reviewers to notification
374 374 recipients += [x.user for x in pull_request_obj.reviewers]
375 375
376 376 pr_target_repo = pull_request_obj.target_repo
377 377 pr_source_repo = pull_request_obj.source_repo
378 378
379 379 pr_comment_url = h.route_url(
380 380 'pullrequest_show',
381 381 repo_name=pr_target_repo.repo_name,
382 382 pull_request_id=pull_request_obj.pull_request_id,
383 383 _anchor='comment-%s' % comment.comment_id)
384 384
385 pr_url = h.route_url(
386 'pullrequest_show',
387 repo_name=pr_target_repo.repo_name,
388 pull_request_id=pull_request_obj.pull_request_id, )
389
385 390 # set some variables for email notification
386 391 pr_target_repo_url = h.route_url(
387 392 'repo_summary', repo_name=pr_target_repo.repo_name)
388 393
389 394 pr_source_repo_url = h.route_url(
390 395 'repo_summary', repo_name=pr_source_repo.repo_name)
391 396
392 397 # pull request specifics
393 398 kwargs.update({
394 399 'pull_request': pull_request_obj,
395 400 'pr_id': pull_request_obj.pull_request_id,
396 'pr_target_repo': pr_target_repo,
397 'pr_target_repo_url': pr_target_repo_url,
398 'pr_source_repo': pr_source_repo,
399 'pr_source_repo_url': pr_source_repo_url,
401 'pull_request_url': pr_url,
402 'pull_request_target_repo': pr_target_repo,
403 'pull_request_target_repo_url': pr_target_repo_url,
404 'pull_request_source_repo': pr_source_repo,
405 'pull_request_source_repo_url': pr_source_repo_url,
400 406 'pr_comment_url': pr_comment_url,
401 407 'pr_closing': closing_pr,
402 408 })
403 409 if send_email:
404 410 # pre-generate the subject for notification itself
405 411 (subject,
406 412 _h, _e, # we don't care about those
407 413 body_plaintext) = EmailNotificationModel().render_email(
408 414 notification_type, **kwargs)
409 415
410 416 mention_recipients = set(
411 417 self._extract_mentions(text)).difference(recipients)
412 418
413 419 # create notification objects, and emails
414 420 NotificationModel().create(
415 421 created_by=user,
416 422 notification_subject=subject,
417 423 notification_body=body_plaintext,
418 424 notification_type=notification_type,
419 425 recipients=recipients,
420 426 mention_recipients=mention_recipients,
421 427 email_kwargs=kwargs,
422 428 )
423 429
424 430 Session().flush()
425 431 if comment.pull_request:
426 432 action = 'repo.pull_request.comment.create'
427 433 else:
428 434 action = 'repo.commit.comment.create'
429 435
430 436 comment_data = comment.get_api_data()
431 437 self._log_audit_action(
432 438 action, {'data': comment_data}, auth_user, comment)
433 439
434 440 msg_url = ''
435 441 channel = None
436 442 if commit_obj:
437 443 msg_url = commit_comment_url
438 444 repo_name = repo.repo_name
439 445 channel = u'/repo${}$/commit/{}'.format(
440 446 repo_name,
441 447 commit_obj.raw_id
442 448 )
443 449 elif pull_request_obj:
444 450 msg_url = pr_comment_url
445 451 repo_name = pr_target_repo.repo_name
446 452 channel = u'/repo${}$/pr/{}'.format(
447 453 repo_name,
448 454 pull_request_id
449 455 )
450 456
451 457 message = '<strong>{}</strong> {} - ' \
452 458 '<a onclick="window.location=\'{}\';' \
453 459 'window.location.reload()">' \
454 460 '<strong>{}</strong></a>'
455 461 message = message.format(
456 462 user.username, _('made a comment'), msg_url,
457 463 _('Show it now'))
458 464
459 465 channelstream.post_message(
460 466 channel, message, user.username,
461 467 registry=get_current_registry())
462 468
463 469 return comment
464 470
465 471 def delete(self, comment, auth_user):
466 472 """
467 473 Deletes given comment
468 474 """
469 475 comment = self.__get_commit_comment(comment)
470 476 old_data = comment.get_api_data()
471 477 Session().delete(comment)
472 478
473 479 if comment.pull_request:
474 480 action = 'repo.pull_request.comment.delete'
475 481 else:
476 482 action = 'repo.commit.comment.delete'
477 483
478 484 self._log_audit_action(
479 485 action, {'old_data': old_data}, auth_user, comment)
480 486
481 487 return comment
482 488
483 489 def get_all_comments(self, repo_id, revision=None, pull_request=None):
484 490 q = ChangesetComment.query()\
485 491 .filter(ChangesetComment.repo_id == repo_id)
486 492 if revision:
487 493 q = q.filter(ChangesetComment.revision == revision)
488 494 elif pull_request:
489 495 pull_request = self.__get_pull_request(pull_request)
490 496 q = q.filter(ChangesetComment.pull_request == pull_request)
491 497 else:
492 498 raise Exception('Please specify commit or pull_request')
493 499 q = q.order_by(ChangesetComment.created_on)
494 500 return q.all()
495 501
496 502 def get_url(self, comment, request=None, permalink=False):
497 503 if not request:
498 504 request = get_current_request()
499 505
500 506 comment = self.__get_commit_comment(comment)
501 507 if comment.pull_request:
502 508 pull_request = comment.pull_request
503 509 if permalink:
504 510 return request.route_url(
505 511 'pull_requests_global',
506 512 pull_request_id=pull_request.pull_request_id,
507 513 _anchor='comment-%s' % comment.comment_id)
508 514 else:
509 515 return request.route_url(
510 516 'pullrequest_show',
511 517 repo_name=safe_str(pull_request.target_repo.repo_name),
512 518 pull_request_id=pull_request.pull_request_id,
513 519 _anchor='comment-%s' % comment.comment_id)
514 520
515 521 else:
516 522 repo = comment.repo
517 523 commit_id = comment.revision
518 524
519 525 if permalink:
520 526 return request.route_url(
521 527 'repo_commit', repo_name=safe_str(repo.repo_id),
522 528 commit_id=commit_id,
523 529 _anchor='comment-%s' % comment.comment_id)
524 530
525 531 else:
526 532 return request.route_url(
527 533 'repo_commit', repo_name=safe_str(repo.repo_name),
528 534 commit_id=commit_id,
529 535 _anchor='comment-%s' % comment.comment_id)
530 536
531 537 def get_comments(self, repo_id, revision=None, pull_request=None):
532 538 """
533 539 Gets main comments based on revision or pull_request_id
534 540
535 541 :param repo_id:
536 542 :param revision:
537 543 :param pull_request:
538 544 """
539 545
540 546 q = ChangesetComment.query()\
541 547 .filter(ChangesetComment.repo_id == repo_id)\
542 548 .filter(ChangesetComment.line_no == None)\
543 549 .filter(ChangesetComment.f_path == None)
544 550 if revision:
545 551 q = q.filter(ChangesetComment.revision == revision)
546 552 elif pull_request:
547 553 pull_request = self.__get_pull_request(pull_request)
548 554 q = q.filter(ChangesetComment.pull_request == pull_request)
549 555 else:
550 556 raise Exception('Please specify commit or pull_request')
551 557 q = q.order_by(ChangesetComment.created_on)
552 558 return q.all()
553 559
554 560 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
555 561 q = self._get_inline_comments_query(repo_id, revision, pull_request)
556 562 return self._group_comments_by_path_and_line_number(q)
557 563
558 564 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
559 565 version=None):
560 566 inline_cnt = 0
561 567 for fname, per_line_comments in inline_comments.iteritems():
562 568 for lno, comments in per_line_comments.iteritems():
563 569 for comm in comments:
564 570 if not comm.outdated_at_version(version) and skip_outdated:
565 571 inline_cnt += 1
566 572
567 573 return inline_cnt
568 574
569 575 def get_outdated_comments(self, repo_id, pull_request):
570 576 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
571 577 # of a pull request.
572 578 q = self._all_inline_comments_of_pull_request(pull_request)
573 579 q = q.filter(
574 580 ChangesetComment.display_state ==
575 581 ChangesetComment.COMMENT_OUTDATED
576 582 ).order_by(ChangesetComment.comment_id.asc())
577 583
578 584 return self._group_comments_by_path_and_line_number(q)
579 585
580 586 def _get_inline_comments_query(self, repo_id, revision, pull_request):
581 587 # TODO: johbo: Split this into two methods: One for PR and one for
582 588 # commit.
583 589 if revision:
584 590 q = Session().query(ChangesetComment).filter(
585 591 ChangesetComment.repo_id == repo_id,
586 592 ChangesetComment.line_no != null(),
587 593 ChangesetComment.f_path != null(),
588 594 ChangesetComment.revision == revision)
589 595
590 596 elif pull_request:
591 597 pull_request = self.__get_pull_request(pull_request)
592 598 if not CommentsModel.use_outdated_comments(pull_request):
593 599 q = self._visible_inline_comments_of_pull_request(pull_request)
594 600 else:
595 601 q = self._all_inline_comments_of_pull_request(pull_request)
596 602
597 603 else:
598 604 raise Exception('Please specify commit or pull_request_id')
599 605 q = q.order_by(ChangesetComment.comment_id.asc())
600 606 return q
601 607
602 608 def _group_comments_by_path_and_line_number(self, q):
603 609 comments = q.all()
604 610 paths = collections.defaultdict(lambda: collections.defaultdict(list))
605 611 for co in comments:
606 612 paths[co.f_path][co.line_no].append(co)
607 613 return paths
608 614
609 615 @classmethod
610 616 def needed_extra_diff_context(cls):
611 617 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
612 618
613 619 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
614 620 if not CommentsModel.use_outdated_comments(pull_request):
615 621 return
616 622
617 623 comments = self._visible_inline_comments_of_pull_request(pull_request)
618 624 comments_to_outdate = comments.all()
619 625
620 626 for comment in comments_to_outdate:
621 627 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
622 628
623 629 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
624 630 diff_line = _parse_comment_line_number(comment.line_no)
625 631
626 632 try:
627 633 old_context = old_diff_proc.get_context_of_line(
628 634 path=comment.f_path, diff_line=diff_line)
629 635 new_context = new_diff_proc.get_context_of_line(
630 636 path=comment.f_path, diff_line=diff_line)
631 637 except (diffs.LineNotInDiffException,
632 638 diffs.FileNotInDiffException):
633 639 comment.display_state = ChangesetComment.COMMENT_OUTDATED
634 640 return
635 641
636 642 if old_context == new_context:
637 643 return
638 644
639 645 if self._should_relocate_diff_line(diff_line):
640 646 new_diff_lines = new_diff_proc.find_context(
641 647 path=comment.f_path, context=old_context,
642 648 offset=self.DIFF_CONTEXT_BEFORE)
643 649 if not new_diff_lines:
644 650 comment.display_state = ChangesetComment.COMMENT_OUTDATED
645 651 else:
646 652 new_diff_line = self._choose_closest_diff_line(
647 653 diff_line, new_diff_lines)
648 654 comment.line_no = _diff_to_comment_line_number(new_diff_line)
649 655 else:
650 656 comment.display_state = ChangesetComment.COMMENT_OUTDATED
651 657
652 658 def _should_relocate_diff_line(self, diff_line):
653 659 """
654 660 Checks if relocation shall be tried for the given `diff_line`.
655 661
656 662 If a comment points into the first lines, then we can have a situation
657 663 that after an update another line has been added on top. In this case
658 664 we would find the context still and move the comment around. This
659 665 would be wrong.
660 666 """
661 667 should_relocate = (
662 668 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
663 669 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
664 670 return should_relocate
665 671
666 672 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
667 673 candidate = new_diff_lines[0]
668 674 best_delta = _diff_line_delta(diff_line, candidate)
669 675 for new_diff_line in new_diff_lines[1:]:
670 676 delta = _diff_line_delta(diff_line, new_diff_line)
671 677 if delta < best_delta:
672 678 candidate = new_diff_line
673 679 best_delta = delta
674 680 return candidate
675 681
676 682 def _visible_inline_comments_of_pull_request(self, pull_request):
677 683 comments = self._all_inline_comments_of_pull_request(pull_request)
678 684 comments = comments.filter(
679 685 coalesce(ChangesetComment.display_state, '') !=
680 686 ChangesetComment.COMMENT_OUTDATED)
681 687 return comments
682 688
683 689 def _all_inline_comments_of_pull_request(self, pull_request):
684 690 comments = Session().query(ChangesetComment)\
685 691 .filter(ChangesetComment.line_no != None)\
686 692 .filter(ChangesetComment.f_path != None)\
687 693 .filter(ChangesetComment.pull_request == pull_request)
688 694 return comments
689 695
690 696 def _all_general_comments_of_pull_request(self, pull_request):
691 697 comments = Session().query(ChangesetComment)\
692 698 .filter(ChangesetComment.line_no == None)\
693 699 .filter(ChangesetComment.f_path == None)\
694 700 .filter(ChangesetComment.pull_request == pull_request)
695 701 return comments
696 702
697 703 @staticmethod
698 704 def use_outdated_comments(pull_request):
699 705 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
700 706 settings = settings_model.get_general_settings()
701 707 return settings.get('rhodecode_use_outdated_comments', False)
702 708
703 709
704 710 def _parse_comment_line_number(line_no):
705 711 """
706 712 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
707 713 """
708 714 old_line = None
709 715 new_line = None
710 716 if line_no.startswith('o'):
711 717 old_line = int(line_no[1:])
712 718 elif line_no.startswith('n'):
713 719 new_line = int(line_no[1:])
714 720 else:
715 721 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
716 722 return diffs.DiffLineNumber(old_line, new_line)
717 723
718 724
719 725 def _diff_to_comment_line_number(diff_line):
720 726 if diff_line.new is not None:
721 727 return u'n{}'.format(diff_line.new)
722 728 elif diff_line.old is not None:
723 729 return u'o{}'.format(diff_line.old)
724 730 return u''
725 731
726 732
727 733 def _diff_line_delta(a, b):
728 734 if None not in (a.new, b.new):
729 735 return abs(a.new - b.new)
730 736 elif None not in (a.old, b.old):
731 737 return abs(a.old - b.old)
732 738 else:
733 739 raise ValueError(
734 740 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,1005 +1,1007 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28 import ipaddress
29 29
30 30 from pyramid.threadlocal import get_current_request
31 31 from sqlalchemy.exc import DatabaseError
32 32
33 33 from rhodecode import events
34 34 from rhodecode.lib.user_log_filter import user_log_filter
35 35 from rhodecode.lib.utils2 import (
36 36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 37 AttributeDict, str2bool)
38 38 from rhodecode.lib.exceptions import (
39 39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 40 UserOwnsUserGroupsException, NotAllowedToCreateUserError, UserOwnsArtifactsException)
41 41 from rhodecode.lib.caching_query import FromCache
42 42 from rhodecode.model import BaseModel
43 43 from rhodecode.model.auth_token import AuthTokenModel
44 44 from rhodecode.model.db import (
45 45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
46 46 UserEmailMap, UserIpMap, UserLog)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(
61 61 FromCache("sql_cache_short", "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def _serialize_user(self, user):
68 68 import rhodecode.lib.helpers as h
69 69
70 70 return {
71 71 'id': user.user_id,
72 72 'first_name': user.first_name,
73 73 'last_name': user.last_name,
74 74 'username': user.username,
75 75 'email': user.email,
76 76 'icon_link': h.gravatar_url(user.email, 30),
77 77 'profile_link': h.link_to_user(user),
78 78 'value_display': h.escape(h.person(user)),
79 79 'value': user.username,
80 80 'value_type': 'user',
81 81 'active': user.active,
82 82 }
83 83
84 84 def get_users(self, name_contains=None, limit=20, only_active=True):
85 85
86 86 query = self.sa.query(User)
87 87 if only_active:
88 88 query = query.filter(User.active == true())
89 89
90 90 if name_contains:
91 91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
92 92 query = query.filter(
93 93 or_(
94 94 User.name.ilike(ilike_expression),
95 95 User.lastname.ilike(ilike_expression),
96 96 User.username.ilike(ilike_expression)
97 97 )
98 98 )
99 99 query = query.limit(limit)
100 100 users = query.all()
101 101
102 102 _users = [
103 103 self._serialize_user(user) for user in users
104 104 ]
105 105 return _users
106 106
107 107 def get_by_username(self, username, cache=False, case_insensitive=False):
108 108
109 109 if case_insensitive:
110 110 user = self.sa.query(User).filter(User.username.ilike(username))
111 111 else:
112 112 user = self.sa.query(User)\
113 113 .filter(User.username == username)
114 114 if cache:
115 115 name_key = _hash_key(username)
116 116 user = user.options(
117 117 FromCache("sql_cache_short", "get_user_%s" % name_key))
118 118 return user.scalar()
119 119
120 120 def get_by_email(self, email, cache=False, case_insensitive=False):
121 121 return User.get_by_email(email, case_insensitive, cache)
122 122
123 123 def get_by_auth_token(self, auth_token, cache=False):
124 124 return User.get_by_auth_token(auth_token, cache)
125 125
126 126 def get_active_user_count(self, cache=False):
127 127 qry = User.query().filter(
128 128 User.active == true()).filter(
129 129 User.username != User.DEFAULT_USER)
130 130 if cache:
131 131 qry = qry.options(
132 132 FromCache("sql_cache_short", "get_active_users"))
133 133 return qry.count()
134 134
135 135 def create(self, form_data, cur_user=None):
136 136 if not cur_user:
137 137 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
138 138
139 139 user_data = {
140 140 'username': form_data['username'],
141 141 'password': form_data['password'],
142 142 'email': form_data['email'],
143 143 'firstname': form_data['firstname'],
144 144 'lastname': form_data['lastname'],
145 145 'active': form_data['active'],
146 146 'extern_type': form_data['extern_type'],
147 147 'extern_name': form_data['extern_name'],
148 148 'admin': False,
149 149 'cur_user': cur_user
150 150 }
151 151
152 152 if 'create_repo_group' in form_data:
153 153 user_data['create_repo_group'] = str2bool(
154 154 form_data.get('create_repo_group'))
155 155
156 156 try:
157 157 if form_data.get('password_change'):
158 158 user_data['force_password_change'] = True
159 159 return UserModel().create_or_update(**user_data)
160 160 except Exception:
161 161 log.error(traceback.format_exc())
162 162 raise
163 163
164 164 def update_user(self, user, skip_attrs=None, **kwargs):
165 165 from rhodecode.lib.auth import get_crypt_password
166 166
167 167 user = self._get_user(user)
168 168 if user.username == User.DEFAULT_USER:
169 169 raise DefaultUserException(
170 170 "You can't edit this user (`%(username)s`) since it's "
171 171 "crucial for entire application" % {
172 172 'username': user.username})
173 173
174 174 # first store only defaults
175 175 user_attrs = {
176 176 'updating_user_id': user.user_id,
177 177 'username': user.username,
178 178 'password': user.password,
179 179 'email': user.email,
180 180 'firstname': user.name,
181 181 'lastname': user.lastname,
182 182 'description': user.description,
183 183 'active': user.active,
184 184 'admin': user.admin,
185 185 'extern_name': user.extern_name,
186 186 'extern_type': user.extern_type,
187 187 'language': user.user_data.get('language')
188 188 }
189 189
190 190 # in case there's new_password, that comes from form, use it to
191 191 # store password
192 192 if kwargs.get('new_password'):
193 193 kwargs['password'] = kwargs['new_password']
194 194
195 195 # cleanups, my_account password change form
196 196 kwargs.pop('current_password', None)
197 197 kwargs.pop('new_password', None)
198 198
199 199 # cleanups, user edit password change form
200 200 kwargs.pop('password_confirmation', None)
201 201 kwargs.pop('password_change', None)
202 202
203 203 # create repo group on user creation
204 204 kwargs.pop('create_repo_group', None)
205 205
206 206 # legacy forms send name, which is the firstname
207 207 firstname = kwargs.pop('name', None)
208 208 if firstname:
209 209 kwargs['firstname'] = firstname
210 210
211 211 for k, v in kwargs.items():
212 212 # skip if we don't want to update this
213 213 if skip_attrs and k in skip_attrs:
214 214 continue
215 215
216 216 user_attrs[k] = v
217 217
218 218 try:
219 219 return self.create_or_update(**user_attrs)
220 220 except Exception:
221 221 log.error(traceback.format_exc())
222 222 raise
223 223
224 224 def create_or_update(
225 225 self, username, password, email, firstname='', lastname='',
226 226 active=True, admin=False, extern_type=None, extern_name=None,
227 227 cur_user=None, plugin=None, force_password_change=False,
228 228 allow_to_create_user=True, create_repo_group=None,
229 229 updating_user_id=None, language=None, description='',
230 230 strict_creation_check=True):
231 231 """
232 232 Creates a new instance if not found, or updates current one
233 233
234 234 :param username:
235 235 :param password:
236 236 :param email:
237 237 :param firstname:
238 238 :param lastname:
239 239 :param active:
240 240 :param admin:
241 241 :param extern_type:
242 242 :param extern_name:
243 243 :param cur_user:
244 244 :param plugin: optional plugin this method was called from
245 245 :param force_password_change: toggles new or existing user flag
246 246 for password change
247 247 :param allow_to_create_user: Defines if the method can actually create
248 248 new users
249 249 :param create_repo_group: Defines if the method should also
250 250 create an repo group with user name, and owner
251 251 :param updating_user_id: if we set it up this is the user we want to
252 252 update this allows to editing username.
253 253 :param language: language of user from interface.
254 254 :param description: user description
255 255 :param strict_creation_check: checks for allowed creation license wise etc.
256 256
257 257 :returns: new User object with injected `is_new_user` attribute.
258 258 """
259 259
260 260 if not cur_user:
261 261 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
262 262
263 263 from rhodecode.lib.auth import (
264 264 get_crypt_password, check_password, generate_auth_token)
265 265 from rhodecode.lib.hooks_base import (
266 266 log_create_user, check_allowed_create_user)
267 267
268 268 def _password_change(new_user, password):
269 269 old_password = new_user.password or ''
270 270 # empty password
271 271 if not old_password:
272 272 return False
273 273
274 274 # password check is only needed for RhodeCode internal auth calls
275 275 # in case it's a plugin we don't care
276 276 if not plugin:
277 277
278 278 # first check if we gave crypted password back, and if it
279 279 # matches it's not password change
280 280 if new_user.password == password:
281 281 return False
282 282
283 283 password_match = check_password(password, old_password)
284 284 if not password_match:
285 285 return True
286 286
287 287 return False
288 288
289 289 # read settings on default personal repo group creation
290 290 if create_repo_group is None:
291 291 default_create_repo_group = RepoGroupModel()\
292 292 .get_default_create_personal_repo_group()
293 293 create_repo_group = default_create_repo_group
294 294
295 295 user_data = {
296 296 'username': username,
297 297 'password': password,
298 298 'email': email,
299 299 'firstname': firstname,
300 300 'lastname': lastname,
301 301 'active': active,
302 302 'admin': admin
303 303 }
304 304
305 305 if updating_user_id:
306 306 log.debug('Checking for existing account in RhodeCode '
307 307 'database with user_id `%s` ', updating_user_id)
308 308 user = User.get(updating_user_id)
309 309 else:
310 310 log.debug('Checking for existing account in RhodeCode '
311 311 'database with username `%s` ', username)
312 312 user = User.get_by_username(username, case_insensitive=True)
313 313
314 314 if user is None:
315 315 # we check internal flag if this method is actually allowed to
316 316 # create new user
317 317 if not allow_to_create_user:
318 318 msg = ('Method wants to create new user, but it is not '
319 319 'allowed to do so')
320 320 log.warning(msg)
321 321 raise NotAllowedToCreateUserError(msg)
322 322
323 323 log.debug('Creating new user %s', username)
324 324
325 325 # only if we create user that is active
326 326 new_active_user = active
327 327 if new_active_user and strict_creation_check:
328 328 # raises UserCreationError if it's not allowed for any reason to
329 329 # create new active user, this also executes pre-create hooks
330 330 check_allowed_create_user(user_data, cur_user, strict_check=True)
331 331 events.trigger(events.UserPreCreate(user_data))
332 332 new_user = User()
333 333 edit = False
334 334 else:
335 335 log.debug('updating user `%s`', username)
336 336 events.trigger(events.UserPreUpdate(user, user_data))
337 337 new_user = user
338 338 edit = True
339 339
340 340 # we're not allowed to edit default user
341 341 if user.username == User.DEFAULT_USER:
342 342 raise DefaultUserException(
343 343 "You can't edit this user (`%(username)s`) since it's "
344 344 "crucial for entire application"
345 345 % {'username': user.username})
346 346
347 347 # inject special attribute that will tell us if User is new or old
348 348 new_user.is_new_user = not edit
349 349 # for users that didn's specify auth type, we use RhodeCode built in
350 350 from rhodecode.authentication.plugins import auth_rhodecode
351 351 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
352 352 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
353 353
354 354 try:
355 355 new_user.username = username
356 356 new_user.admin = admin
357 357 new_user.email = email
358 358 new_user.active = active
359 359 new_user.extern_name = safe_unicode(extern_name)
360 360 new_user.extern_type = safe_unicode(extern_type)
361 361 new_user.name = firstname
362 362 new_user.lastname = lastname
363 363 new_user.description = description
364 364
365 365 # set password only if creating an user or password is changed
366 366 if not edit or _password_change(new_user, password):
367 367 reason = 'new password' if edit else 'new user'
368 368 log.debug('Updating password reason=>%s', reason)
369 369 new_user.password = get_crypt_password(password) if password else None
370 370
371 371 if force_password_change:
372 372 new_user.update_userdata(force_password_change=True)
373 373 if language:
374 374 new_user.update_userdata(language=language)
375 375 new_user.update_userdata(notification_status=True)
376 376
377 377 self.sa.add(new_user)
378 378
379 379 if not edit and create_repo_group:
380 380 RepoGroupModel().create_personal_repo_group(
381 381 new_user, commit_early=False)
382 382
383 383 if not edit:
384 384 # add the RSS token
385 385 self.add_auth_token(
386 386 user=username, lifetime_minutes=-1,
387 387 role=self.auth_token_role.ROLE_FEED,
388 388 description=u'Generated feed token')
389 389
390 390 kwargs = new_user.get_dict()
391 391 # backward compat, require api_keys present
392 392 kwargs['api_keys'] = kwargs['auth_tokens']
393 393 log_create_user(created_by=cur_user, **kwargs)
394 394 events.trigger(events.UserPostCreate(user_data))
395 395 return new_user
396 396 except (DatabaseError,):
397 397 log.error(traceback.format_exc())
398 398 raise
399 399
400 400 def create_registration(self, form_data,
401 401 extern_name='rhodecode', extern_type='rhodecode'):
402 402 from rhodecode.model.notification import NotificationModel
403 403 from rhodecode.model.notification import EmailNotificationModel
404 404
405 405 try:
406 406 form_data['admin'] = False
407 407 form_data['extern_name'] = extern_name
408 408 form_data['extern_type'] = extern_type
409 409 new_user = self.create(form_data)
410 410
411 411 self.sa.add(new_user)
412 412 self.sa.flush()
413 413
414 414 user_data = new_user.get_dict()
415 415 kwargs = {
416 416 # use SQLALCHEMY safe dump of user data
417 417 'user': AttributeDict(user_data),
418 418 'date': datetime.datetime.now()
419 419 }
420 420 notification_type = EmailNotificationModel.TYPE_REGISTRATION
421 421 # pre-generate the subject for notification itself
422 422 (subject,
423 423 _h, _e, # we don't care about those
424 424 body_plaintext) = EmailNotificationModel().render_email(
425 425 notification_type, **kwargs)
426 426
427 427 # create notification objects, and emails
428 428 NotificationModel().create(
429 429 created_by=new_user,
430 430 notification_subject=subject,
431 431 notification_body=body_plaintext,
432 432 notification_type=notification_type,
433 433 recipients=None, # all admins
434 434 email_kwargs=kwargs,
435 435 )
436 436
437 437 return new_user
438 438 except Exception:
439 439 log.error(traceback.format_exc())
440 440 raise
441 441
442 442 def _handle_user_repos(self, username, repositories, handle_mode=None):
443 443 _superadmin = self.cls.get_first_super_admin()
444 444 left_overs = True
445 445
446 446 from rhodecode.model.repo import RepoModel
447 447
448 448 if handle_mode == 'detach':
449 449 for obj in repositories:
450 450 obj.user = _superadmin
451 451 # set description we know why we super admin now owns
452 452 # additional repositories that were orphaned !
453 453 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
454 454 self.sa.add(obj)
455 455 left_overs = False
456 456 elif handle_mode == 'delete':
457 457 for obj in repositories:
458 458 RepoModel().delete(obj, forks='detach')
459 459 left_overs = False
460 460
461 461 # if nothing is done we have left overs left
462 462 return left_overs
463 463
464 464 def _handle_user_repo_groups(self, username, repository_groups,
465 465 handle_mode=None):
466 466 _superadmin = self.cls.get_first_super_admin()
467 467 left_overs = True
468 468
469 469 from rhodecode.model.repo_group import RepoGroupModel
470 470
471 471 if handle_mode == 'detach':
472 472 for r in repository_groups:
473 473 r.user = _superadmin
474 474 # set description we know why we super admin now owns
475 475 # additional repositories that were orphaned !
476 476 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
477 477 r.personal = False
478 478 self.sa.add(r)
479 479 left_overs = False
480 480 elif handle_mode == 'delete':
481 481 for r in repository_groups:
482 482 RepoGroupModel().delete(r)
483 483 left_overs = False
484 484
485 485 # if nothing is done we have left overs left
486 486 return left_overs
487 487
488 488 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
489 489 _superadmin = self.cls.get_first_super_admin()
490 490 left_overs = True
491 491
492 492 from rhodecode.model.user_group import UserGroupModel
493 493
494 494 if handle_mode == 'detach':
495 495 for r in user_groups:
496 496 for user_user_group_to_perm in r.user_user_group_to_perm:
497 497 if user_user_group_to_perm.user.username == username:
498 498 user_user_group_to_perm.user = _superadmin
499 499 r.user = _superadmin
500 500 # set description we know why we super admin now owns
501 501 # additional repositories that were orphaned !
502 502 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
503 503 self.sa.add(r)
504 504 left_overs = False
505 505 elif handle_mode == 'delete':
506 506 for r in user_groups:
507 507 UserGroupModel().delete(r)
508 508 left_overs = False
509 509
510 510 # if nothing is done we have left overs left
511 511 return left_overs
512 512
513 513 def _handle_user_artifacts(self, username, artifacts, handle_mode=None):
514 514 _superadmin = self.cls.get_first_super_admin()
515 515 left_overs = True
516 516
517 517 if handle_mode == 'detach':
518 518 for a in artifacts:
519 519 a.upload_user = _superadmin
520 520 # set description we know why we super admin now owns
521 521 # additional artifacts that were orphaned !
522 522 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
523 523 self.sa.add(a)
524 524 left_overs = False
525 525 elif handle_mode == 'delete':
526 526 from rhodecode.apps.file_store import utils as store_utils
527 527 storage = store_utils.get_file_storage(self.request.registry.settings)
528 528 for a in artifacts:
529 529 file_uid = a.file_uid
530 530 storage.delete(file_uid)
531 531 self.sa.delete(a)
532 532
533 533 left_overs = False
534 534
535 535 # if nothing is done we have left overs left
536 536 return left_overs
537 537
538 538 def delete(self, user, cur_user=None, handle_repos=None,
539 539 handle_repo_groups=None, handle_user_groups=None, handle_artifacts=None):
540 540 from rhodecode.lib.hooks_base import log_delete_user
541 541
542 542 if not cur_user:
543 543 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
544 544 user = self._get_user(user)
545 545
546 546 try:
547 547 if user.username == User.DEFAULT_USER:
548 548 raise DefaultUserException(
549 549 u"You can't remove this user since it's"
550 550 u" crucial for entire application")
551 551
552 552 left_overs = self._handle_user_repos(
553 553 user.username, user.repositories, handle_repos)
554 554 if left_overs and user.repositories:
555 555 repos = [x.repo_name for x in user.repositories]
556 556 raise UserOwnsReposException(
557 557 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
558 558 u'removed. Switch owners or remove those repositories:%(list_repos)s'
559 559 % {'username': user.username, 'len_repos': len(repos),
560 560 'list_repos': ', '.join(repos)})
561 561
562 562 left_overs = self._handle_user_repo_groups(
563 563 user.username, user.repository_groups, handle_repo_groups)
564 564 if left_overs and user.repository_groups:
565 565 repo_groups = [x.group_name for x in user.repository_groups]
566 566 raise UserOwnsRepoGroupsException(
567 567 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
568 568 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
569 569 % {'username': user.username, 'len_repo_groups': len(repo_groups),
570 570 'list_repo_groups': ', '.join(repo_groups)})
571 571
572 572 left_overs = self._handle_user_user_groups(
573 573 user.username, user.user_groups, handle_user_groups)
574 574 if left_overs and user.user_groups:
575 575 user_groups = [x.users_group_name for x in user.user_groups]
576 576 raise UserOwnsUserGroupsException(
577 577 u'user "%s" still owns %s user groups and cannot be '
578 578 u'removed. Switch owners or remove those user groups:%s'
579 579 % (user.username, len(user_groups), ', '.join(user_groups)))
580 580
581 581 left_overs = self._handle_user_artifacts(
582 582 user.username, user.artifacts, handle_artifacts)
583 583 if left_overs and user.artifacts:
584 584 artifacts = [x.file_uid for x in user.artifacts]
585 585 raise UserOwnsArtifactsException(
586 586 u'user "%s" still owns %s artifacts and cannot be '
587 587 u'removed. Switch owners or remove those artifacts:%s'
588 588 % (user.username, len(artifacts), ', '.join(artifacts)))
589 589
590 590 user_data = user.get_dict() # fetch user data before expire
591 591
592 592 # we might change the user data with detach/delete, make sure
593 593 # the object is marked as expired before actually deleting !
594 594 self.sa.expire(user)
595 595 self.sa.delete(user)
596 596
597 597 log_delete_user(deleted_by=cur_user, **user_data)
598 598 except Exception:
599 599 log.error(traceback.format_exc())
600 600 raise
601 601
602 602 def reset_password_link(self, data, pwd_reset_url):
603 603 from rhodecode.lib.celerylib import tasks, run_task
604 604 from rhodecode.model.notification import EmailNotificationModel
605 605 user_email = data['email']
606 606 try:
607 607 user = User.get_by_email(user_email)
608 608 if user:
609 609 log.debug('password reset user found %s', user)
610 610
611 611 email_kwargs = {
612 612 'password_reset_url': pwd_reset_url,
613 613 'user': user,
614 614 'email': user_email,
615 'date': datetime.datetime.now()
615 'date': datetime.datetime.now(),
616 'first_admin_email': User.get_first_super_admin().email
616 617 }
617 618
618 619 (subject, headers, email_body,
619 620 email_body_plaintext) = EmailNotificationModel().render_email(
620 621 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
621 622
622 623 recipients = [user_email]
623 624
624 625 action_logger_generic(
625 626 'sending password reset email to user: {}'.format(
626 627 user), namespace='security.password_reset')
627 628
628 629 run_task(tasks.send_email, recipients, subject,
629 630 email_body_plaintext, email_body)
630 631
631 632 else:
632 633 log.debug("password reset email %s not found", user_email)
633 634 except Exception:
634 635 log.error(traceback.format_exc())
635 636 return False
636 637
637 638 return True
638 639
639 640 def reset_password(self, data):
640 641 from rhodecode.lib.celerylib import tasks, run_task
641 642 from rhodecode.model.notification import EmailNotificationModel
642 643 from rhodecode.lib import auth
643 644 user_email = data['email']
644 645 pre_db = True
645 646 try:
646 647 user = User.get_by_email(user_email)
647 648 new_passwd = auth.PasswordGenerator().gen_password(
648 649 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
649 650 if user:
650 651 user.password = auth.get_crypt_password(new_passwd)
651 652 # also force this user to reset his password !
652 653 user.update_userdata(force_password_change=True)
653 654
654 655 Session().add(user)
655 656
656 657 # now delete the token in question
657 658 UserApiKeys = AuthTokenModel.cls
658 659 UserApiKeys().query().filter(
659 660 UserApiKeys.api_key == data['token']).delete()
660 661
661 662 Session().commit()
662 663 log.info('successfully reset password for `%s`', user_email)
663 664
664 665 if new_passwd is None:
665 666 raise Exception('unable to generate new password')
666 667
667 668 pre_db = False
668 669
669 670 email_kwargs = {
670 671 'new_password': new_passwd,
671 672 'user': user,
672 673 'email': user_email,
673 'date': datetime.datetime.now()
674 'date': datetime.datetime.now(),
675 'first_admin_email': User.get_first_super_admin().email
674 676 }
675 677
676 678 (subject, headers, email_body,
677 679 email_body_plaintext) = EmailNotificationModel().render_email(
678 680 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
679 681 **email_kwargs)
680 682
681 683 recipients = [user_email]
682 684
683 685 action_logger_generic(
684 686 'sent new password to user: {} with email: {}'.format(
685 687 user, user_email), namespace='security.password_reset')
686 688
687 689 run_task(tasks.send_email, recipients, subject,
688 690 email_body_plaintext, email_body)
689 691
690 692 except Exception:
691 693 log.error('Failed to update user password')
692 694 log.error(traceback.format_exc())
693 695 if pre_db:
694 696 # we rollback only if local db stuff fails. If it goes into
695 697 # run_task, we're pass rollback state this wouldn't work then
696 698 Session().rollback()
697 699
698 700 return True
699 701
700 702 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
701 703 """
702 704 Fetches auth_user by user_id,or api_key if present.
703 705 Fills auth_user attributes with those taken from database.
704 706 Additionally set's is_authenitated if lookup fails
705 707 present in database
706 708
707 709 :param auth_user: instance of user to set attributes
708 710 :param user_id: user id to fetch by
709 711 :param api_key: api key to fetch by
710 712 :param username: username to fetch by
711 713 """
712 714 def token_obfuscate(token):
713 715 if token:
714 716 return token[:4] + "****"
715 717
716 718 if user_id is None and api_key is None and username is None:
717 719 raise Exception('You need to pass user_id, api_key or username')
718 720
719 721 log.debug(
720 722 'AuthUser: fill data execution based on: '
721 723 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
722 724 try:
723 725 dbuser = None
724 726 if user_id:
725 727 dbuser = self.get(user_id)
726 728 elif api_key:
727 729 dbuser = self.get_by_auth_token(api_key)
728 730 elif username:
729 731 dbuser = self.get_by_username(username)
730 732
731 733 if not dbuser:
732 734 log.warning(
733 735 'Unable to lookup user by id:%s api_key:%s username:%s',
734 736 user_id, token_obfuscate(api_key), username)
735 737 return False
736 738 if not dbuser.active:
737 739 log.debug('User `%s:%s` is inactive, skipping fill data',
738 740 username, user_id)
739 741 return False
740 742
741 743 log.debug('AuthUser: filling found user:%s data', dbuser)
742 744
743 745 attrs = {
744 746 'user_id': dbuser.user_id,
745 747 'username': dbuser.username,
746 748 'name': dbuser.name,
747 749 'first_name': dbuser.first_name,
748 750 'firstname': dbuser.firstname,
749 751 'last_name': dbuser.last_name,
750 752 'lastname': dbuser.lastname,
751 753 'admin': dbuser.admin,
752 754 'active': dbuser.active,
753 755
754 756 'email': dbuser.email,
755 757 'emails': dbuser.emails_cached(),
756 758 'short_contact': dbuser.short_contact,
757 759 'full_contact': dbuser.full_contact,
758 760 'full_name': dbuser.full_name,
759 761 'full_name_or_username': dbuser.full_name_or_username,
760 762
761 763 '_api_key': dbuser._api_key,
762 764 '_user_data': dbuser._user_data,
763 765
764 766 'created_on': dbuser.created_on,
765 767 'extern_name': dbuser.extern_name,
766 768 'extern_type': dbuser.extern_type,
767 769
768 770 'inherit_default_permissions': dbuser.inherit_default_permissions,
769 771
770 772 'language': dbuser.language,
771 773 'last_activity': dbuser.last_activity,
772 774 'last_login': dbuser.last_login,
773 775 'password': dbuser.password,
774 776 }
775 777 auth_user.__dict__.update(attrs)
776 778 except Exception:
777 779 log.error(traceback.format_exc())
778 780 auth_user.is_authenticated = False
779 781 return False
780 782
781 783 return True
782 784
783 785 def has_perm(self, user, perm):
784 786 perm = self._get_perm(perm)
785 787 user = self._get_user(user)
786 788
787 789 return UserToPerm.query().filter(UserToPerm.user == user)\
788 790 .filter(UserToPerm.permission == perm).scalar() is not None
789 791
790 792 def grant_perm(self, user, perm):
791 793 """
792 794 Grant user global permissions
793 795
794 796 :param user:
795 797 :param perm:
796 798 """
797 799 user = self._get_user(user)
798 800 perm = self._get_perm(perm)
799 801 # if this permission is already granted skip it
800 802 _perm = UserToPerm.query()\
801 803 .filter(UserToPerm.user == user)\
802 804 .filter(UserToPerm.permission == perm)\
803 805 .scalar()
804 806 if _perm:
805 807 return
806 808 new = UserToPerm()
807 809 new.user = user
808 810 new.permission = perm
809 811 self.sa.add(new)
810 812 return new
811 813
812 814 def revoke_perm(self, user, perm):
813 815 """
814 816 Revoke users global permissions
815 817
816 818 :param user:
817 819 :param perm:
818 820 """
819 821 user = self._get_user(user)
820 822 perm = self._get_perm(perm)
821 823
822 824 obj = UserToPerm.query()\
823 825 .filter(UserToPerm.user == user)\
824 826 .filter(UserToPerm.permission == perm)\
825 827 .scalar()
826 828 if obj:
827 829 self.sa.delete(obj)
828 830
829 831 def add_extra_email(self, user, email):
830 832 """
831 833 Adds email address to UserEmailMap
832 834
833 835 :param user:
834 836 :param email:
835 837 """
836 838
837 839 user = self._get_user(user)
838 840
839 841 obj = UserEmailMap()
840 842 obj.user = user
841 843 obj.email = email
842 844 self.sa.add(obj)
843 845 return obj
844 846
845 847 def delete_extra_email(self, user, email_id):
846 848 """
847 849 Removes email address from UserEmailMap
848 850
849 851 :param user:
850 852 :param email_id:
851 853 """
852 854 user = self._get_user(user)
853 855 obj = UserEmailMap.query().get(email_id)
854 856 if obj and obj.user_id == user.user_id:
855 857 self.sa.delete(obj)
856 858
857 859 def parse_ip_range(self, ip_range):
858 860 ip_list = []
859 861
860 862 def make_unique(value):
861 863 seen = []
862 864 return [c for c in value if not (c in seen or seen.append(c))]
863 865
864 866 # firsts split by commas
865 867 for ip_range in ip_range.split(','):
866 868 if not ip_range:
867 869 continue
868 870 ip_range = ip_range.strip()
869 871 if '-' in ip_range:
870 872 start_ip, end_ip = ip_range.split('-', 1)
871 873 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
872 874 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
873 875 parsed_ip_range = []
874 876
875 877 for index in xrange(int(start_ip), int(end_ip) + 1):
876 878 new_ip = ipaddress.ip_address(index)
877 879 parsed_ip_range.append(str(new_ip))
878 880 ip_list.extend(parsed_ip_range)
879 881 else:
880 882 ip_list.append(ip_range)
881 883
882 884 return make_unique(ip_list)
883 885
884 886 def add_extra_ip(self, user, ip, description=None):
885 887 """
886 888 Adds ip address to UserIpMap
887 889
888 890 :param user:
889 891 :param ip:
890 892 """
891 893
892 894 user = self._get_user(user)
893 895 obj = UserIpMap()
894 896 obj.user = user
895 897 obj.ip_addr = ip
896 898 obj.description = description
897 899 self.sa.add(obj)
898 900 return obj
899 901
900 902 auth_token_role = AuthTokenModel.cls
901 903
902 904 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
903 905 scope_callback=None):
904 906 """
905 907 Add AuthToken for user.
906 908
907 909 :param user: username/user_id
908 910 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
909 911 :param role: one of AuthTokenModel.cls.ROLE_*
910 912 :param description: optional string description
911 913 """
912 914
913 915 token = AuthTokenModel().create(
914 916 user, description, lifetime_minutes, role)
915 917 if scope_callback and callable(scope_callback):
916 918 # call the callback if we provide, used to attach scope for EE edition
917 919 scope_callback(token)
918 920 return token
919 921
920 922 def delete_extra_ip(self, user, ip_id):
921 923 """
922 924 Removes ip address from UserIpMap
923 925
924 926 :param user:
925 927 :param ip_id:
926 928 """
927 929 user = self._get_user(user)
928 930 obj = UserIpMap.query().get(ip_id)
929 931 if obj and obj.user_id == user.user_id:
930 932 self.sa.delete(obj)
931 933
932 934 def get_accounts_in_creation_order(self, current_user=None):
933 935 """
934 936 Get accounts in order of creation for deactivation for license limits
935 937
936 938 pick currently logged in user, and append to the list in position 0
937 939 pick all super-admins in order of creation date and add it to the list
938 940 pick all other accounts in order of creation and add it to the list.
939 941
940 942 Based on that list, the last accounts can be disabled as they are
941 943 created at the end and don't include any of the super admins as well
942 944 as the current user.
943 945
944 946 :param current_user: optionally current user running this operation
945 947 """
946 948
947 949 if not current_user:
948 950 current_user = get_current_rhodecode_user()
949 951 active_super_admins = [
950 952 x.user_id for x in User.query()
951 953 .filter(User.user_id != current_user.user_id)
952 954 .filter(User.active == true())
953 955 .filter(User.admin == true())
954 956 .order_by(User.created_on.asc())]
955 957
956 958 active_regular_users = [
957 959 x.user_id for x in User.query()
958 960 .filter(User.user_id != current_user.user_id)
959 961 .filter(User.active == true())
960 962 .filter(User.admin == false())
961 963 .order_by(User.created_on.asc())]
962 964
963 965 list_of_accounts = [current_user.user_id]
964 966 list_of_accounts += active_super_admins
965 967 list_of_accounts += active_regular_users
966 968
967 969 return list_of_accounts
968 970
969 971 def deactivate_last_users(self, expected_users, current_user=None):
970 972 """
971 973 Deactivate accounts that are over the license limits.
972 974 Algorithm of which accounts to disabled is based on the formula:
973 975
974 976 Get current user, then super admins in creation order, then regular
975 977 active users in creation order.
976 978
977 979 Using that list we mark all accounts from the end of it as inactive.
978 980 This way we block only latest created accounts.
979 981
980 982 :param expected_users: list of users in special order, we deactivate
981 983 the end N amount of users from that list
982 984 """
983 985
984 986 list_of_accounts = self.get_accounts_in_creation_order(
985 987 current_user=current_user)
986 988
987 989 for acc_id in list_of_accounts[expected_users + 1:]:
988 990 user = User.get(acc_id)
989 991 log.info('Deactivating account %s for license unlock', user)
990 992 user.active = False
991 993 Session().add(user)
992 994 Session().commit()
993 995
994 996 return
995 997
996 998 def get_user_log(self, user, filter_term):
997 999 user_log = UserLog.query()\
998 1000 .filter(or_(UserLog.user_id == user.user_id,
999 1001 UserLog.username == user.username))\
1000 1002 .options(joinedload(UserLog.user))\
1001 1003 .options(joinedload(UserLog.repository))\
1002 1004 .order_by(UserLog.action_date.desc())
1003 1005
1004 1006 user_log = user_log_filter(user_log, filter_term)
1005 1007 return user_log
@@ -1,385 +1,387 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('favicon', '/favicon.ico', []);
16 16 pyroutes.register('robots', '/robots.txt', []);
17 17 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
18 18 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
19 19 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
20 20 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
21 21 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
22 22 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
23 23 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
24 24 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
25 25 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
26 26 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
27 27 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
28 28 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
29 29 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
30 30 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
32 32 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
33 33 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
34 34 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
35 35 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
36 36 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
37 37 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
38 38 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
39 39 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
40 40 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
41 41 pyroutes.register('admin_home', '/_admin', []);
42 42 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
43 43 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
44 44 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
45 45 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
46 46 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
47 47 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
48 48 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
49 49 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
50 50 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
51 51 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
52 52 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
53 53 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
54 54 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
55 55 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
56 56 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
57 57 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
58 58 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
59 59 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
60 60 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
61 61 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
62 62 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
63 63 pyroutes.register('admin_settings', '/_admin/settings', []);
64 64 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
65 65 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
66 66 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
67 67 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
68 68 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
69 69 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
70 70 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
71 71 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
72 72 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
73 73 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
74 74 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
75 75 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
76 76 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
77 77 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
78 78 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
79 79 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
80 80 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
81 81 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
82 82 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
83 83 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
84 84 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
85 85 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
86 86 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
87 87 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
88 88 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
89 89 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
90 90 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
91 91 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
92 92 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
93 93 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
94 94 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
95 95 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
96 96 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
97 97 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
98 98 pyroutes.register('users', '/_admin/users', []);
99 99 pyroutes.register('users_data', '/_admin/users_data', []);
100 100 pyroutes.register('users_create', '/_admin/users/create', []);
101 101 pyroutes.register('users_new', '/_admin/users/new', []);
102 102 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
103 103 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
104 104 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
105 105 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
106 106 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
107 107 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
108 108 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
109 109 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
110 110 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
111 111 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
112 112 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
113 113 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
114 114 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
115 115 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
116 116 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
117 117 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
118 118 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
119 119 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
120 120 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
121 121 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
122 122 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
123 123 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
124 124 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
125 125 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
126 126 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
127 127 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
128 128 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
129 129 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
130 130 pyroutes.register('user_groups', '/_admin/user_groups', []);
131 131 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
132 132 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
133 133 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
134 134 pyroutes.register('repos', '/_admin/repos', []);
135 135 pyroutes.register('repo_new', '/_admin/repos/new', []);
136 136 pyroutes.register('repo_create', '/_admin/repos/create', []);
137 137 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
138 138 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
139 139 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
140 140 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
141 141 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
142 142 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
143 143 pyroutes.register('channelstream_proxy', '/_channelstream', []);
144 144 pyroutes.register('upload_file', '/_file_store/upload', []);
145 145 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
146 146 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
147 147 pyroutes.register('logout', '/_admin/logout', []);
148 148 pyroutes.register('reset_password', '/_admin/password_reset', []);
149 149 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
150 150 pyroutes.register('home', '/', []);
151 151 pyroutes.register('user_autocomplete_data', '/_users', []);
152 152 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
153 153 pyroutes.register('repo_list_data', '/_repos', []);
154 154 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
155 155 pyroutes.register('goto_switcher_data', '/_goto_data', []);
156 156 pyroutes.register('markup_preview', '/_markup_preview', []);
157 157 pyroutes.register('file_preview', '/_file_preview', []);
158 158 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
159 159 pyroutes.register('journal', '/_admin/journal', []);
160 160 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
161 161 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
162 162 pyroutes.register('journal_public', '/_admin/public_journal', []);
163 163 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
164 164 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
165 165 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
166 166 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
167 167 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
168 168 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
169 169 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
170 170 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
171 171 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
172 172 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
173 173 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
174 174 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
175 175 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
176 176 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
177 177 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
178 178 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
179 179 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
180 180 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
181 181 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
182 182 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
183 183 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
184 184 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
185 185 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
186 186 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
187 187 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
188 188 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
189 189 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
190 190 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
191 191 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
192 192 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
193 193 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
194 194 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
195 195 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
196 196 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 197 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
198 198 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
199 199 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 200 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 201 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
202 202 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
203 203 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
204 204 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
205 205 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 206 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
207 207 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 208 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
209 209 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
210 210 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
211 211 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
212 212 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
213 213 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
214 214 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
215 215 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
216 216 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
217 217 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
218 218 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
219 219 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
220 220 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
221 221 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
222 222 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
223 223 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
224 224 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
225 225 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
226 226 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
227 227 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
228 228 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
229 229 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
230 230 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
231 231 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
232 232 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
233 233 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
234 234 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
235 235 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
236 236 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
237 237 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
238 238 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
239 239 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
240 240 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
241 241 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
242 242 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
243 243 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
244 244 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
245 245 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
246 246 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
247 247 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
248 248 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
249 249 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
250 250 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
251 251 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
252 252 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
253 253 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
254 254 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
255 255 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
256 256 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
257 257 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
258 258 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
259 259 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
260 260 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
261 261 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
262 262 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
263 263 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
264 264 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
265 265 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
266 266 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
267 267 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
268 268 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
269 269 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
270 270 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
271 271 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
272 272 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
273 273 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
274 274 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
275 275 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
276 276 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
277 277 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
278 278 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
279 279 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
280 280 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
281 281 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
282 282 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
283 283 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
284 284 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
285 285 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
286 286 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
287 287 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
288 288 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
289 289 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
290 290 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
291 291 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
292 292 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
293 293 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
294 294 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
295 295 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
296 296 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
297 297 pyroutes.register('search', '/_admin/search', []);
298 298 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
299 299 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
300 300 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
301 301 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
302 302 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
303 303 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
304 304 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
305 305 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
306 306 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
307 307 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
308 308 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
309 309 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
310 310 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
311 311 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
312 312 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
313 313 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
314 314 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
315 315 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
316 316 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
317 317 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
318 318 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
319 319 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
320 320 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
321 321 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
322 322 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
323 323 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
324 324 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
325 325 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
326 326 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
327 327 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
328 328 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
329 329 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
330 330 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
331 331 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
332 332 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
333 333 pyroutes.register('gists_show', '/_admin/gists', []);
334 334 pyroutes.register('gists_new', '/_admin/gists/new', []);
335 335 pyroutes.register('gists_create', '/_admin/gists/create', []);
336 336 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
337 337 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
338 338 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
339 339 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
340 340 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
341 341 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
342 342 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
343 343 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
344 344 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
345 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
346 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
345 347 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
346 348 pyroutes.register('apiv2', '/_admin/api', []);
347 349 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
348 350 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
349 351 pyroutes.register('login', '/_admin/login', []);
350 352 pyroutes.register('register', '/_admin/register', []);
351 353 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
352 354 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
353 355 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
354 356 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
355 357 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
356 358 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
357 359 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
358 360 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
359 361 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
360 362 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
361 363 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
362 364 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
363 365 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
364 366 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
365 367 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
366 368 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
367 369 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
368 370 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
369 371 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
370 372 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
371 373 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
372 374 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
373 375 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
374 376 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
375 377 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
376 378 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
377 379 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
378 380 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
379 381 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
380 382 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
381 383 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
382 384 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
383 385 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
384 386 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
385 387 }
@@ -1,79 +1,80 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Debug Style')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${_('Style')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_nav()">
16 16 ${self.menu_items(active='debug_style')}
17 17 </%def>
18 18
19 19
20 20 <%def name="main()">
21 21 <div id="style-page">
22 22 ${self.real_main()}
23 23 </div>
24 24 </%def>
25 25
26 26 <%def name="real_main()">
27 27 <div class="box">
28 28 <div class="title">
29 29 ${self.breadcrumbs()}
30 30 </div>
31 31
32 32 <div class='sidebar-col-wrapper'>
33 33 ##main
34 34 ${self.sidebar()}
35 35
36 36 <div class="main-content">
37 37 <h2>Examples of styled elements</h2>
38 38 <p>Taken based on the examples from Bootstrap, form elements based
39 39 on our current markup.</p>
40 40 <p>
41 41 The objective of this section is to have a comprehensive style guide which out
42 42 lines any and all elements used throughout the application, as a reference for
43 43 both existing developers and as a training tool for future hires.
44 44 </p>
45 45 </div>
46 46 </div>
47 47 </div>
48 48 </%def>
49 49
50 50
51 51 <%def name="sidebar()">
52 52 <div class="sidebar">
53 53 <ul class="nav nav-pills nav-stacked">
54 54 <li class="${'active' if c.active=='index' else ''}"><a href="${h.route_path('debug_style_home')}">${_('Index')}</a></li>
55 <li class="${'active' if c.active=='emails' else ''}"><a href="${h.route_path('debug_style_template', t_path='emails.html')}">${_('Emails')}</a></li>
55 56 <li class="${'active' if c.active=='typography' else ''}"><a href="${h.route_path('debug_style_template', t_path='typography.html')}">${_('Typography')}</a></li>
56 57 <li class="${'active' if c.active=='forms' else ''}"><a href="${h.route_path('debug_style_template', t_path='forms.html')}">${_('Forms')}</a></li>
57 58 <li class="${'active' if c.active=='buttons' else ''}"><a href="${h.route_path('debug_style_template', t_path='buttons.html')}">${_('Buttons')}</a></li>
58 59 <li class="${'active' if c.active=='labels' else ''}"><a href="${h.route_path('debug_style_template', t_path='labels.html')}">${_('Labels')}</a></li>
59 60 <li class="${'active' if c.active=='alerts' else ''}"><a href="${h.route_path('debug_style_template', t_path='alerts.html')}">${_('Alerts')}</a></li>
60 61 <li class="${'active' if c.active=='tables' else ''}"><a href="${h.route_path('debug_style_template', t_path='tables.html')}">${_('Tables')}</a></li>
61 62 <li class="${'active' if c.active=='tables-wide' else ''}"><a href="${h.route_path('debug_style_template', t_path='tables-wide.html')}">${_('Tables wide')}</a></li>
62 63 <li class="${'active' if c.active=='collapsable-content' else ''}"><a href="${h.route_path('debug_style_template', t_path='collapsable-content.html')}">${_('Collapsable Content')}</a></li>
63 64 <li class="${'active' if c.active=='icons' else ''}"><a href="${h.route_path('debug_style_template', t_path='icons.html')}">${_('Icons')}</a></li>
64 65 <li class="${'active' if c.active=='layout-form-sidebar' else ''}"><a href="${h.route_path('debug_style_template', t_path='layout-form-sidebar.html')}">${_('Layout form with sidebar')}</a></li>
65 66 <li class="${'active' if c.active=='login' else ''}"><a href="${h.route_path('debug_style_template', t_path='login.html')}">${_('Login')}</a></li>
66 67 <li class="${'active' if c.active=='login2' else ''}"><a href="${h.route_path('debug_style_template', t_path='login2.html')}">${_('Login 2')}</a></li>
67 68 <li class="${'active' if c.active=='code-block' else ''}"><a href="${h.route_path('debug_style_template', t_path='code-block.html')}">${_('Code blocks')}</a></li>
68 69
69 70 <li class="divider"><strong>Experimental</strong></li>
70 71 <li class="${'active' if c.active=='panels' else ''}"><a href="${h.route_path('debug_style_template', t_path='panels.html')}">${_('Panels')}</a></li>
71 72
72 73 <li class="divider"><strong>Depreciated</strong></li>
73 74 <li class="${'active' if c.active=='form-elements' else ''}"><a href="${h.route_path('debug_style_template', t_path='form-elements.html')}">${_('Form elements')}</a></li>
74 75 <li class="${'active' if c.active=='form-elements-small' else ''}"><a href="${h.route_path('debug_style_template', t_path='form-elements-small.html')}">${_('Form elements small')}</a></li>
75 76 <li class="${'active' if c.active=='form-inline' else ''}"><a href="${h.route_path('debug_style_template', t_path='form-inline.html')}">${_('Form inline elements')}</a></li>
76 77 <li class="${'active' if c.active=='form-vertical' else ''}"><a href="${h.route_path('debug_style_template', t_path='form-vertical.html')}">${_('Form vertical')}</a></li>
77 78 </ul>
78 79 </div>
79 80 </%def> No newline at end of file
@@ -1,142 +1,525 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 ## helpers
4 4 <%def name="tag_button(text, tag_type=None)">
5 5 <%
6 6 color_scheme = {
7 7 'default': 'border:1px solid #979797;color:#666666;background-color:#f9f9f9',
8 8 'approved': 'border:1px solid #0ac878;color:#0ac878;background-color:#f9f9f9',
9 9 'rejected': 'border:1px solid #e85e4d;color:#e85e4d;background-color:#f9f9f9',
10 10 'under_review': 'border:1px solid #ffc854;color:#ffc854;background-color:#f9f9f9',
11 11 }
12
13 css_style = ';'.join([
14 'display:inline',
15 'border-radius:2px',
16 'font-size:12px',
17 'padding:.2em',
18 ])
19
12 20 %>
13 <pre style="display:inline;border-radius:2px;font-size:12px;padding:.2em;${color_scheme.get(tag_type, color_scheme['default'])}">${text}</pre>
21 <pre style="${css_style}; ${color_scheme.get(tag_type, color_scheme['default'])}">${text}</pre>
14 22 </%def>
15 23
16 24 <%def name="status_text(text, tag_type=None)">
17 25 <%
18 26 color_scheme = {
19 27 'default': 'color:#666666',
20 28 'approved': 'color:#0ac878',
21 29 'rejected': 'color:#e85e4d',
22 30 'under_review': 'color:#ffc854',
23 31 }
24 32 %>
25 33 <span style="font-weight:bold;font-size:12px;padding:.2em;${color_scheme.get(tag_type, color_scheme['default'])}">${text}</span>
26 34 </%def>
27 35
36 <%def name="gravatar_img(email, size=16)">
37 <%
38 css_style = ';'.join([
39 'padding: 0',
40 'margin: -4px 0',
41 'border-radius: 50%',
42 'box-sizing: content-box',
43 'display: inline',
44 'line-height: 1em',
45 'min-width: 16px',
46 'min-height: 16px',
47 ])
48 %>
49
50 <img alt="gravatar" style="${css_style}" src="${h.gravatar_url(email, size)}" height="${size}" width="${size}">
51 </%def>
52
53 <%def name="link_css()">\
54 <%
55 css_style = ';'.join([
56 'color:#427cc9',
57 'text-decoration:none',
58 'cursor:pointer'
59 ])
60 %>\
61 ${css_style}\
62 </%def>
63
28 64 ## Constants
29 65 <%
30 66 text_regular = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;"
31 67 text_monospace = "'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;"
32 68
33 69 %>
34 70
35 71 ## headers we additionally can set for email
36 72 <%def name="headers()" filter="n,trim"></%def>
37 73
38 <%def name="plaintext_footer()">
39 ${_('This is a notification from RhodeCode. %(instance_url)s') % {'instance_url': instance_url}}
74 <%def name="plaintext_footer()" filter="trim">
75 ${_('This is a notification from RhodeCode.')} ${instance_url}
40 76 </%def>
41 77
42 78 <%def name="body_plaintext()" filter="n,trim">
43 79 ## this example is not called itself but overridden in each template
44 80 ## the plaintext_footer should be at the bottom of both html and text emails
45 81 ${self.plaintext_footer()}
46 82 </%def>
47 83
48 84 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
49 85 <html xmlns="http://www.w3.org/1999/xhtml">
50 86 <head>
51 87 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
52 88 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
53 89 <title>${self.subject()}</title>
54 90 <style type="text/css">
55 91 /* Based on The MailChimp Reset INLINE: Yes. */
56 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
57 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; font-family: ${text_regular|n}}
92 #outlook a {
93 padding: 0;
94 }
95
96 /* Force Outlook to provide a "view in browser" menu link. */
97 body {
98 width: 100% !important;
99 -webkit-text-size-adjust: 100%;
100 -ms-text-size-adjust: 100%;
101 margin: 0;
102 padding: 0;
103 font-family: ${text_regular|n}
104 }
105
58 106 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
59 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
60 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
107 .ExternalClass {
108 width: 100%;
109 }
110
111 /* Force Hotmail to display emails at full width */
112 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {
113 line-height: 100%;
114 }
115
61 116 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
62 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
117 #backgroundTable {
118 margin: 0;
119 padding: 0;
120 line-height: 100% !important;
121 }
122
63 123 /* End reset */
64 124
65 125 /* defaults for images*/
66 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
67 a img {border:none;}
68 .image_fix {display:block;}
126 img {
127 outline: none;
128 text-decoration: none;
129 -ms-interpolation-mode: bicubic;
130 }
131
132 a img {
133 border: none;
134 }
135
136 .image_fix {
137 display: block;
138 }
139
140 body {
141 line-height: 1.2em;
142 }
143
144 p {
145 margin: 0 0 20px;
146 }
147
148 h1, h2, h3, h4, h5, h6 {
149 color: #323232 !important;
150 }
151
152 a {
153 color: #427cc9;
154 text-decoration: none;
155 outline: none;
156 cursor: pointer;
157 }
158
159 a:focus {
160 outline: none;
161 }
69 162
70 body {line-height:1.2em;}
71 p {margin: 0 0 20px;}
72 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
73 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
74 a:focus {outline:none;}
75 a:hover {color: #305b91;}
76 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
77 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
78 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
79 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
80 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
81 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
82 input:focus {outline: 1px solid #979797}
163 a:hover {
164 color: #305b91;
165 }
166
167 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
168 color: #427cc9 !important;
169 text-decoration: none !important;
170 }
171
172 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {
173 color: #305b91 !important;
174 }
175
176 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {
177 color: #305b91 !important;
178 }
179
180 table {
181 font-size: 13px;
182 border-collapse: collapse;
183 mso-table-lspace: 0pt;
184 mso-table-rspace: 0pt;
185 }
186
187 table td {
188 padding: .65em 1em .65em 0;
189 border-collapse: collapse;
190 vertical-align: top;
191 text-align: left;
192 }
193
194 input {
195 display: inline;
196 border-radius: 2px;
197 border: 1px solid #dbd9da;
198 padding: .5em;
199 }
200
201 input:focus {
202 outline: 1px solid #979797
203 }
204
83 205 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
84 206 /* Put your iPhone 4g styles in here */
85 207 }
86 208
87 209 /* Android targeting */
88 210 @media only screen and (-webkit-device-pixel-ratio:.75){
89 211 /* Put CSS for low density (ldpi) Android layouts in here */
90 212 }
91 213 @media only screen and (-webkit-device-pixel-ratio:1){
92 214 /* Put CSS for medium density (mdpi) Android layouts in here */
93 215 }
94 216 @media only screen and (-webkit-device-pixel-ratio:1.5){
95 217 /* Put CSS for high density (hdpi) Android layouts in here */
96 218 }
97 219 /* end Android targeting */
98 220
221 /** MARKDOWN styling **/
222 div.markdown-block {
223 clear: both;
224 overflow: hidden;
225 margin: 0;
226 padding: 3px 5px 3px
227 }
228
229 div.markdown-block h1, div.markdown-block h2, div.markdown-block h3, div.markdown-block h4, div.markdown-block h5, div.markdown-block h6 {
230 border-bottom: none !important;
231 padding: 0 !important;
232 overflow: visible !important
233 }
234
235 div.markdown-block h1, div.markdown-block h2 {
236 border-bottom: 1px #e6e5e5 solid !important
237 }
238
239 div.markdown-block h1 {
240 font-size: 32px;
241 margin: 15px 0 15px 0 !important;
242 padding-bottom: 5px !important
243 }
244
245 div.markdown-block h2 {
246 font-size: 24px !important;
247 margin: 34px 0 10px 0 !important;
248 padding-top: 15px !important;
249 padding-bottom: 8px !important
250 }
251
252 div.markdown-block h3 {
253 font-size: 18px !important;
254 margin: 30px 0 8px 0 !important;
255 padding-bottom: 2px !important
256 }
257
258 div.markdown-block h4 {
259 font-size: 13px !important;
260 margin: 18px 0 3px 0 !important
261 }
262
263 div.markdown-block h5 {
264 font-size: 12px !important;
265 margin: 15px 0 3px 0 !important
266 }
267
268 div.markdown-block h6 {
269 font-size: 12px;
270 color: #777777;
271 margin: 15px 0 3px 0 !important
272 }
273
274 div.markdown-block hr {
275 border: 0;
276 color: #e6e5e5;
277 background-color: #e6e5e5;
278 height: 3px;
279 margin-bottom: 13px
280 }
281
282 div.markdown-block ol, div.markdown-block ul, div.markdown-block p, div.markdown-block blockquote, div.markdown-block dl, div.markdown-block li, div.markdown-block table {
283 margin: 3px 0 13px 0 !important;
284 color: #424242 !important;
285 font-size: 13px !important;
286 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
287 font-weight: normal !important;
288 overflow: visible !important;
289 line-height: 140% !important
290 }
291
292 div.markdown-block pre {
293 margin: 3px 0 13px 0 !important;
294 padding: .5em;
295 color: #424242 !important;
296 font-size: 13px !important;
297 overflow: visible !important;
298 line-height: 140% !important;
299 background-color: #F5F5F5
300 }
301
302 div.markdown-block img {
303 border-style: none;
304 background-color: #fff;
305 padding-right: 20px;
306 max-width: 100%
307 }
308
309 div.markdown-block strong {
310 font-weight: 600;
311 margin: 0
312 }
313
314 div.markdown-block ul.checkbox, div.markdown-block ol.checkbox {
315 padding-left: 20px !important;
316 margin-top: 0 !important;
317 margin-bottom: 18px !important
318 }
319
320 div.markdown-block ul, div.markdown-block ol {
321 padding-left: 30px !important;
322 margin-top: 0 !important;
323 margin-bottom: 18px !important
324 }
325
326 div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
327 list-style: none !important;
328 margin: 6px !important;
329 padding: 0 !important
330 }
331
332 div.markdown-block ul li, div.markdown-block ol li {
333 list-style: disc !important;
334 margin: 6px !important;
335 padding: 0 !important
336 }
337
338 div.markdown-block ol li {
339 list-style: decimal !important
340 }
341
342 div.markdown-block #message {
343 -webkit-border-radius: 2px;
344 -moz-border-radius: 2px;
345 border-radius: 2px;
346 border: 1px solid #dbd9da;
347 display: block;
348 width: 100%;
349 height: 60px;
350 margin: 6px 0
351 }
352
353 div.markdown-block button, div.markdown-block #ws {
354 font-size: 13px;
355 padding: 4px 6px;
356 -webkit-border-radius: 2px;
357 -moz-border-radius: 2px;
358 border-radius: 2px;
359 border: 1px solid #dbd9da;
360 background-color: #eeeeee
361 }
362
363 div.markdown-block code, div.markdown-block pre, div.markdown-block #ws, div.markdown-block #message {
364 font-family: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
365 font-size: 11px;
366 -webkit-border-radius: 2px;
367 -moz-border-radius: 2px;
368 border-radius: 2px;
369 background-color: white;
370 color: #7E7F7F
371 }
372
373 div.markdown-block code {
374 border: 1px solid #eeeeee;
375 margin: 0 2px;
376 padding: 0 5px
377 }
378
379 div.markdown-block pre {
380 border: 1px solid #dbd9da;
381 overflow: auto;
382 padding: .5em;
383 background-color: #F5F5F5
384 }
385
386 div.markdown-block pre > code {
387 border: 0;
388 margin: 0;
389 padding: 0
390 }
391
392 div.rst-block {
393 clear: both;
394 overflow: hidden;
395 margin: 0;
396 padding: 3px 5px 3px
397 }
398
399 div.rst-block h2 {
400 font-weight: normal
401 }
402
403 div.rst-block h1, div.rst-block h2, div.rst-block h3, div.rst-block h4, div.rst-block h5, div.rst-block h6 {
404 border-bottom: 0 !important;
405 margin: 0 !important;
406 padding: 0 !important;
407 line-height: 1.5em !important
408 }
409
410 div.rst-block h1:first-child {
411 padding-top: .25em !important
412 }
413
414 div.rst-block h2, div.rst-block h3 {
415 margin: 1em 0 !important
416 }
417
418 div.rst-block h1, div.rst-block h2 {
419 border-bottom: 1px #e6e5e5 solid !important
420 }
421
422 div.rst-block h2 {
423 margin-top: 1.5em !important;
424 padding-top: .5em !important
425 }
426
427 div.rst-block p {
428 color: black !important;
429 margin: 1em 0 !important;
430 line-height: 1.5em !important
431 }
432
433 div.rst-block ul {
434 list-style: disc !important;
435 margin: 1em 0 1em 2em !important;
436 clear: both
437 }
438
439 div.rst-block ol {
440 list-style: decimal;
441 margin: 1em 0 1em 2em !important
442 }
443
444 div.rst-block pre, div.rst-block code {
445 font: 12px "Bitstream Vera Sans Mono", "Courier", monospace
446 }
447
448 div.rst-block code {
449 font-size: 12px !important;
450 background-color: ghostWhite !important;
451 color: #444 !important;
452 padding: 0 .2em !important;
453 border: 1px solid #dedede !important
454 }
455
456 div.rst-block pre code {
457 padding: 0 !important;
458 font-size: 12px !important;
459 background-color: #eee !important;
460 border: none !important
461 }
462
463 div.rst-block pre {
464 margin: 1em 0;
465 padding: 15px;
466 border: 1px solid #eeeeee;
467 -webkit-border-radius: 2px;
468 -moz-border-radius: 2px;
469 border-radius: 2px;
470 overflow: auto;
471 font-size: 12px;
472 color: #444;
473 background-color: #F5F5F5
474 }
475
476
99 477 </style>
100 478
101 479 <!-- Targeting Windows Mobile -->
102 480 <!--[if IEMobile 7]>
103 481 <style type="text/css">
104 482
105 483 </style>
106 484 <![endif]-->
107 485
108 486 <!--[if gte mso 9]>
109 487 <style>
110 488 /* Target Outlook 2007 and 2010 */
111 489 </style>
112 490 <![endif]-->
113 491 </head>
114 492 <body>
115 493 <!-- 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. -->
116 494 <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">
117 495 <tr>
118 496 <td valign="top" style="padding:0;">
119 497 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
120 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
498 <tr>
499 <td style="width:100%;padding:10px 15px;background-color:#202020" valign="top">
121 500 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
122 501 ${_('RhodeCode')}
123 502 % if rhodecode_instance_name:
124 503 - ${rhodecode_instance_name}
125 504 % endif
126 505 </a>
127 </td></tr>
128 <tr><td style="padding:15px;" valign="top">${self.body()}</td></tr>
506 </td>
507 </tr>
508 <tr>
509 <td style="padding:15px;" valign="top">${self.body()}</td>
510 </tr>
129 511 </table>
130 512 </td>
131 513 </tr>
132 514 </table>
133 515 <!-- End of wrapper table -->
134 516
135 517 <div style="clear: both"></div>
136 <p>
137 <a style="margin-top:15px;margin-left:1%;font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace} " href="${instance_url}">
138 ${self.plaintext_footer()}
518 <div style="margin-left:1%;font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace}">
519 ${_('This is a notification from RhodeCode.')}
520 <a style="font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace}" href="${instance_url}">
521 ${instance_url}
139 522 </a>
140 </p>
523 </div>
141 524 </body>
142 525 </html>
@@ -1,108 +1,161 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 'user': h.person(user),
9 'user': '@'+h.person(user),
10 10 'repo_name': repo_name,
11 'commit_id': h.show_id(commit),
12 11 'status': status_change,
13 12 'comment_file': comment_file,
14 13 'comment_line': comment_line,
15 14 'comment_type': comment_type,
15
16 'commit_id': h.show_id(commit),
16 17 }
17 18 %>
18 ${_('[mention]') if mention else ''} \
19
19 20
20 21 % if comment_file:
21 ${_('{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)} ${_('in the {repo_name} repository').format(**data) |n}
22 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)} ${_('in the `{repo_name}` repository').format(**data) |n}
22 23 % else:
23 24 % if status_change:
24 ${_('[status: {status}] {user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the {repo_name} repository').format(**data) |n}
25 ${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
25 26 % else:
26 ${_('{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the {repo_name} repository').format(**data) |n}
27 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
27 28 % endif
28 29 % endif
29 30
30 31 </%def>
31 32
32 33 ## PLAINTEXT VERSION OF BODY
33 34 <%def name="body_plaintext()" filter="n,trim">
34 35 <%
35 36 data = {
36 37 'user': h.person(user),
37 38 'repo_name': repo_name,
38 'commit_id': h.show_id(commit),
39 39 'status': status_change,
40 40 'comment_file': comment_file,
41 41 'comment_line': comment_line,
42 42 'comment_type': comment_type,
43
44 'commit_id': h.show_id(commit),
43 45 }
44 46 %>
45 ${self.subject()}
46 47
47 48 * ${_('Comment link')}: ${commit_comment_url}
48 49
50 %if status_change:
51 * ${_('Commit status')}: ${_('Status was changed to')}: *${status_change}*
52
53 %endif
49 54 * ${_('Commit')}: ${h.show_id(commit)}
50 55
56 * ${_('Commit message')}: ${commit.message}
57
51 58 %if comment_file:
52 59 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
60
53 61 %endif
62 % if comment_type == 'todo':
63 ${_('`TODO` comment')}:
64 % else:
65 ${_('`Note` comment')}:
66 % endif
67
68 ${comment_body |n, trim}
54 69
55 70 ---
56
57 %if status_change:
58 ${_('Commit status was changed to')}: *${status_change}*
59 %endif
60
61 ${comment_body|n}
62
63 71 ${self.plaintext_footer()}
64 72 </%def>
65 73
66 74
67 75 <%
68 76 data = {
69 77 'user': h.person(user),
70 'repo': commit_target_repo,
71 'repo_name': repo_name,
72 'commit_id': h.show_id(commit),
73 78 'comment_file': comment_file,
74 79 'comment_line': comment_line,
75 80 'comment_type': comment_type,
81 'renderer_type': renderer_type or 'plain',
82
83 'repo': commit_target_repo_url,
84 'repo_name': repo_name,
85 'commit_id': h.show_id(commit),
76 86 }
77 87 %>
78 <table style="text-align:left;vertical-align:middle;">
79 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
88
89 <table style="text-align:left;vertical-align:middle;width: 100%">
90 <tr>
91 <td style="width:100%;border-bottom:1px solid #dbd9da;">
80 92
93 <h4 style="margin: 0">
94 <div style="margin-bottom: 4px; color:#7E7F7F">
95 @${h.person(user.username)}
96 </div>
97 ${_('left a')}
98 <a href="${commit_comment_url}" style="${base.link_css()}">
81 99 % if comment_file:
82 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)}</a> ${_('in the {repo} repository').format(**data) |n}</h4>
100 ${_('{comment_type} on file `{comment_file}` in commit.').format(**data)}
83 101 % else:
84 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n}</a> ${_('in the {repo} repository').format(**data) |n}</h4>
102 ${_('{comment_type} on commit.').format(**data) |n}
85 103 % endif
86 </td></tr>
104 </a>
105 <div style="margin-top: 10px"></div>
106 ${_('Commit')} <code>${data['commit_id']}</code> ${_('of repository')}: ${data['repo_name']}
107 </h4>
108
109 </td>
110 </tr>
87 111
88 <tr><td style="padding-right:20px;padding-top:15px;">${_('Commit')}</td><td style="padding-top:15px;"><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${h.show_id(commit)}</a></td></tr>
89 <tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td></tr>
112 </table>
113
114 <table style="text-align:left;vertical-align:middle;width: 100%">
115
116 ## spacing def
117 <tr>
118 <td style="width: 130px"></td>
119 <td></td>
120 </tr>
90 121
91 122 % if status_change:
92 123 <tr>
93 <td style="padding-right:20px;">${_('Status')}</td>
124 <td style="padding-right:20px;">${_('Commit Status')}:</td>
94 125 <td>
95 ${_('The commit status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}
126 ${_('Status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}
96 127 </td>
97 128 </tr>
98 129 % endif
130
99 131 <tr>
100 <td style="padding-right:20px;">
132 <td style="padding-right:20px;">${_('Commit')}:</td>
133 <td>
134 <a href="${commit_comment_url}" style="${base.link_css()}">${h.show_id(commit)}</a>
135 </td>
136 </tr>
137 <tr>
138 <td style="padding-right:20px;">${_('Commit message')}:</td>
139 <td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td>
140 </tr>
141
142 % if comment_file:
143 <tr>
144 <td style="padding-right:20px;">${_('File')}:</td>
145 <td><a href="${commit_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
146 </tr>
147 % endif
148
149 <tr style="background-image: linear-gradient(to right, black 33%, rgba(255,255,255,0) 0%);background-position: bottom;background-size: 3px 1px;background-repeat: repeat-x;">
150 <td colspan="2" style="padding-right:20px;">
101 151 % if comment_type == 'todo':
102 ${(_('TODO comment on line: {comment_line}') if comment_file else _('TODO comment')).format(**data)}
152 ${_('`TODO` comment')}:
103 153 % else:
104 ${(_('Note comment on line: {comment_line}') if comment_file else _('Note comment')).format(**data)}
154 ${_('`Note` comment')}:
105 155 % endif
106 156 </td>
107 <td style="line-height:1.2em;white-space:pre-wrap">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td></tr>
157 </tr>
158
159 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
160 </tr>
108 161 </table>
@@ -1,13 +1,20 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 4
4 5 <%def name="subject()" filter="n,trim,whitespace_filter">
5 6 RhodeCode test email: ${h.format_date(date)}
6 7 </%def>
7 8
8 9 ## plain text version of the email. Empty by default
9 10 <%def name="body_plaintext()" filter="n,trim">
10 Test Email from RhodeCode version: ${rhodecode_version}, sent by: ${user}
11 Test Email from RhodeCode version: ${rhodecode_version}
12 Email sent by: ${h.person(user)}
13
14 ---
15 ${self.plaintext_footer()}
11 16 </%def>
12 17
13 ${body_plaintext()} No newline at end of file
18 Test Email from RhodeCode version: ${rhodecode_version}
19 <br/><br/>
20 Email sent by: <strong>${h.person(user)}</strong>
@@ -1,21 +1,21 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 4 <%def name="subject()" filter="n,trim,whitespace_filter">
5 5 </%def>
6 6
7 7
8 8 ## plain text version of the email. Empty by default
9 9 <%def name="body_plaintext()" filter="n,trim">
10 10 ${body}
11 11
12 ---
12 13 ${self.plaintext_footer()}
13 14 </%def>
14 15
15 16 ## BODY GOES BELOW
16 17 <table style="text-align:left;vertical-align:top;">
17 <tr><td style="padding-right:20px;padding-top:15px;white-space:pre-wrap">${body}</td></tr>
18 <tr>
19 <td style="padding-right:20px;padding-top:15px;white-space:pre-wrap">${body}</td>
20 </tr>
18 21 </table>
19 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;display:block;color:#666666;text-decoration:none;" href="${instance_url}">
20 ${self.plaintext_footer()}
21 </a></p> No newline at end of file
@@ -1,33 +1,37 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 4
4 5 <%def name="subject()" filter="n,trim,whitespace_filter">
5 6 RhodeCode Password reset
6 7 </%def>
7 8
8 9 ## plain text version of the email. Empty by default
9 10 <%def name="body_plaintext()" filter="n,trim">
10 Hi ${user.username},
11 Hello ${user.username},
11 12
12 There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
13 On ${h.format_date(date)} there was a request to reset your password using the email address `${email}`
13 14
14 *If you didn't do this, please contact your RhodeCode administrator.*
15 *If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}*
15 16
16 17 You can continue, and generate new password by clicking following URL:
17 18 ${password_reset_url}
18 19
19 20 This link will be active for 10 minutes.
21
22 ---
20 23 ${self.plaintext_footer()}
21 24 </%def>
22 25
23 26 ## BODY GOES BELOW
24 27 <p>
25 28 Hello ${user.username},
26 29 </p><p>
27 There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
28 <br/>
29 <strong>If you did not request a password reset, please contact your RhodeCode administrator.</strong>
30 On ${h.format_date(date)} there was a request to reset your password using the email address `${email}`
31 <br/><br/>
32 <strong>If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}.</strong>
30 33 </p><p>
31 <a href="${password_reset_url}">${_('Generate new password here')}.</a>
32 This link will be active for 10 minutes.
34 You can continue, and generate new password by clicking following URL:<br/><br/>
35 <a href="${password_reset_url}" style="${base.link_css()}">${password_reset_url}</a>
36 <br/><br/>This link will be active for 10 minutes.
33 37 </p>
@@ -1,29 +1,31 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 4
4 5 <%def name="subject()" filter="n,trim,whitespace_filter">
5 6 Your new RhodeCode password
6 7 </%def>
7 8
8 9 ## plain text version of the email. Empty by default
9 10 <%def name="body_plaintext()" filter="n,trim">
10 Hi ${user.username},
11 Hello ${user.username},
11 12
12 Below is your new access password for RhodeCode.
13 Below is your new access password for RhodeCode requested via password reset link.
13 14
14 *If you didn't do this, please contact your RhodeCode administrator.*
15 *If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}.*
15 16
16 password: ${new_password}
17 new password: ${new_password}
17 18
19 ---
18 20 ${self.plaintext_footer()}
19 21 </%def>
20 22
21 23 ## BODY GOES BELOW
22 24 <p>
23 25 Hello ${user.username},
24 26 </p><p>
25 Below is your new access password for RhodeCode.
26 <br/>
27 <strong>If you didn't request a new password, please contact your RhodeCode administrator.</strong>
27 Below is your new access password for RhodeCode requested via password reset link.
28 <br/><br/>
29 <strong>If you did not request a password reset, please contact your RhodeCode administrator at: ${first_admin_email}.</strong>
28 30 </p>
29 <p>password: <pre>${new_password}</pre>
31 <p>new password: <code>${new_password}</code>
@@ -1,114 +1,191 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 'user': h.person(user),
10 'pr_title': pull_request.title,
11 'pr_id': pull_request.pull_request_id,
9 'user': '@'+h.person(user),
10 'repo_name': repo_name,
12 11 'status': status_change,
13 12 'comment_file': comment_file,
14 13 'comment_line': comment_line,
15 14 'comment_type': comment_type,
15
16 'pr_title': pull_request.title,
17 'pr_id': pull_request.pull_request_id,
16 18 }
17 19 %>
18 20
19 ${(_('[mention]') if mention else '')} \
20 21
21 22 % if comment_file:
22 ${_('{user} left a {comment_type} on file `{comment_file}` in pull request #{pr_id} "{pr_title}"').format(**data) |n}
23 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data) |n}
23 24 % else:
24 25 % if status_change:
25 ${_('[status: {status}] {user} left a {comment_type} on pull request #{pr_id} "{pr_title}"').format(**data) |n}
26 ${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
26 27 % else:
27 ${_('{user} left a {comment_type} on pull request #{pr_id} "{pr_title}"').format(**data) |n}
28 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
28 29 % endif
29 30 % endif
31
30 32 </%def>
31 33
32 34 ## PLAINTEXT VERSION OF BODY
33 35 <%def name="body_plaintext()" filter="n,trim">
34 36 <%
35 37 data = {
36 38 'user': h.person(user),
37 'pr_title': pull_request.title,
38 'pr_id': pull_request.pull_request_id,
39 'repo_name': repo_name,
39 40 'status': status_change,
40 41 'comment_file': comment_file,
41 42 'comment_line': comment_line,
42 43 'comment_type': comment_type,
44
45 'pr_title': pull_request.title,
46 'pr_id': pull_request.pull_request_id,
47 'source_ref_type': pull_request.source_ref_parts.type,
48 'source_ref_name': pull_request.source_ref_parts.name,
49 'target_ref_type': pull_request.target_ref_parts.type,
50 'target_ref_name': pull_request.target_ref_parts.name,
51 'source_repo': pull_request_source_repo.repo_name,
52 'target_repo': pull_request_target_repo.repo_name,
53 'source_repo_url': pull_request_source_repo_url,
54 'target_repo_url': pull_request_target_repo_url,
43 55 }
44 56 %>
45 ${self.subject()}
57
58 ${h.literal(_('Pull request !{pr_id}: `{pr_title}`').format(**data))}
59
60 * ${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))}
46 61
47 62 * ${_('Comment link')}: ${pr_comment_url}
48 63
49 * ${_('Source repository')}: ${pr_source_repo_url}
64 %if status_change and not closing_pr:
65 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
50 66
67 %elif status_change and closing_pr:
68 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
69
70 %endif
51 71 %if comment_file:
52 * ${_('File: {comment_file} on line {comment_line}').format(comment_file=comment_file, comment_line=comment_line)}
72 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
73
53 74 %endif
75 % if comment_type == 'todo':
76 ${_('`TODO` comment')}:
77 % else:
78 ${_('`Note` comment')}:
79 % endif
80
81 ${comment_body |n, trim}
54 82
55 83 ---
56
57 %if status_change and not closing_pr:
58 ${_('{user} submitted pull request #{pr_id} status: *{status}*').format(**data)}
59 %elif status_change and closing_pr:
60 ${_('{user} submitted pull request #{pr_id} status: *{status} and closed*').format(**data)}
61 %endif
62
63 ${comment_body |n}
64
65 84 ${self.plaintext_footer()}
66 85 </%def>
67 86
68 87
69 88 <%
70 89 data = {
71 90 'user': h.person(user),
72 'pr_title': pull_request.title,
73 'pr_id': pull_request.pull_request_id,
74 'status': status_change,
75 91 'comment_file': comment_file,
76 92 'comment_line': comment_line,
77 93 'comment_type': comment_type,
94 'renderer_type': renderer_type or 'plain',
95
96 'pr_title': pull_request.title,
97 'pr_id': pull_request.pull_request_id,
98 'status': status_change,
99 'source_ref_type': pull_request.source_ref_parts.type,
100 'source_ref_name': pull_request.source_ref_parts.name,
101 'target_ref_type': pull_request.target_ref_parts.type,
102 'target_ref_name': pull_request.target_ref_parts.name,
103 'source_repo': pull_request_source_repo.repo_name,
104 'target_repo': pull_request_target_repo.repo_name,
105 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
106 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
78 107 }
79 108 %>
80 <table style="text-align:left;vertical-align:middle;">
81 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
109
110 <table style="text-align:left;vertical-align:middle;width: 100%">
111 <tr>
112 <td style="width:100%;border-bottom:1px solid #dbd9da;">
82 113
114 <h4 style="margin: 0">
115 <div style="margin-bottom: 4px; color:#7E7F7F">
116 @${h.person(user.username)}
117 </div>
118 ${_('left a')}
119 <a href="${pr_comment_url}" style="${base.link_css()}">
83 120 % if comment_file:
84 <h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('{user} left a {comment_type} on file `{comment_file}` in pull request #{pr_id} "{pr_title}"').format(**data) |n}</a></h4>
121 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
85 122 % else:
86 <h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('{user} left a {comment_type} on pull request #{pr_id} "{pr_title}"').format(**data) |n}</a></h4>
123 ${_('{comment_type} on pull request.').format(**data) |n}
87 124 % endif
125 </a>
126 <div style="margin-top: 10px"></div>
127 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
128 </h4>
129
130 </td>
131 </tr>
88 132
89 </td></tr>
90 <tr><td style="padding-right:20px;padding-top:15px;">${_('Source')}</td><td style="padding-top:15px;"><a style="color:#427cc9;text-decoration:none;cursor:pointer" href="${pr_source_repo_url}">${pr_source_repo.repo_name}</a></td></tr>
133 </table>
134
135 <table style="text-align:left;vertical-align:middle;width: 100%">
136
137 ## spacing def
138 <tr>
139 <td style="width: 130px"></td>
140 <td></td>
141 </tr>
91 142
92 143 % if status_change:
93 144 <tr>
94 <td style="padding-right:20px;">${_('Status')}</td>
145 <td style="padding-right:20px;">${_('Review Status')}:</td>
95 146 <td>
96 147 % if closing_pr:
97 148 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
98 149 % else:
99 150 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
100 151 % endif
101 152 </td>
102 153 </tr>
103 154 % endif
155
104 156 <tr>
105 <td style="padding-right:20px;">
157 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
158 <td style="line-height:20px;">
159 ${base.tag_button('{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name))} ${_('of')} ${data['source_repo_url']}
160 &rarr;
161 ${base.tag_button('{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name))} ${_('of')} ${data['target_repo_url']}
162 </td>
163 </tr>
164 <tr>
165 <td style="padding-right:20px;">${_('Pull request')}:</td>
166 <td>
167 <a href="${pull_request_url}" style="${base.link_css()}">
168 !${pull_request.pull_request_id}
169 </a>
170 </td>
171 </tr>
172 % if comment_file:
173 <tr>
174 <td style="padding-right:20px;">${_('File')}:</td>
175 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
176 </tr>
177 % endif
178
179 <tr style="background-image: linear-gradient(to right, black 33%, rgba(255,255,255,0) 0%);background-position: bottom;background-size: 3px 1px;background-repeat: repeat-x;">
180 <td colspan="2" style="padding-right:20px;">
106 181 % if comment_type == 'todo':
107 ${(_('TODO comment on line: {comment_line}') if comment_file else _('TODO comment')).format(**data)}
182 ${_('`TODO` comment')}:
108 183 % else:
109 ${(_('Note comment on line: {comment_line}') if comment_file else _('Note comment')).format(**data)}
184 ${_('`Note` comment')}:
110 185 % endif
111 186 </td>
112 <td style="line-height:1.2em;white-space:pre-wrap">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td>
187 </tr>
188
189 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
113 190 </tr>
114 191 </table>
@@ -1,85 +1,144 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 ## EMAIL SUBJECT
5 6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 7 <%
7 8 data = {
8 'user': h.person(user),
9 'user': '@'+h.person(user),
9 10 'pr_id': pull_request.pull_request_id,
10 11 'pr_title': pull_request.title,
11 12 }
12 13 %>
13 14
14 ${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s"') % data |n}
15 ${_('{user} requested a pull request review. !{pr_id}: "{pr_title}"').format(**data) |n}
15 16 </%def>
16 17
17
18 ## PLAINTEXT VERSION OF BODY
18 19 <%def name="body_plaintext()" filter="n,trim">
19 20 <%
20 21 data = {
21 22 'user': h.person(user),
22 23 'pr_id': pull_request.pull_request_id,
23 24 'pr_title': pull_request.title,
24 25 'source_ref_type': pull_request.source_ref_parts.type,
25 26 'source_ref_name': pull_request.source_ref_parts.name,
26 27 'target_ref_type': pull_request.target_ref_parts.type,
27 28 'target_ref_name': pull_request.target_ref_parts.name,
28 'repo_url': pull_request_source_repo_url
29 'repo_url': pull_request_source_repo_url,
30 'source_repo': pull_request_source_repo.repo_name,
31 'target_repo': pull_request_target_repo.repo_name,
32 'source_repo_url': pull_request_source_repo_url,
33 'target_repo_url': pull_request_target_repo_url,
29 34 }
30 35 %>
31 ${self.subject()}
32 36
37 ${h.literal(_('Pull request !{pr_id}: `{pr_title}`').format(**data))}
33 38
34 ${h.literal(_('Pull request from %(source_ref_type)s:%(source_ref_name)s of %(repo_url)s into %(target_ref_type)s:%(target_ref_name)s') % data)}
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))}
35 40
36
37 * ${_('Link')}: ${pull_request_url}
41 * ${_('Pull Request link')}: ${pull_request_url}
38 42
39 43 * ${_('Title')}: ${pull_request.title}
40 44
41 45 * ${_('Description')}:
42 46
43 ${pull_request.description}
47 ${pull_request.description | trim}
44 48
45 49
46 50 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
47 51
48 52 % for commit_id, message in pull_request_commits:
49 53 - ${h.short_id(commit_id)}
50 54 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
51 55
52 56 % endfor
53 57
58 ---
54 59 ${self.plaintext_footer()}
55 60 </%def>
56 61 <%
57 62 data = {
58 63 'user': h.person(user),
59 64 'pr_id': pull_request.pull_request_id,
60 65 'pr_title': pull_request.title,
61 66 'source_ref_type': pull_request.source_ref_parts.type,
62 67 'source_ref_name': pull_request.source_ref_parts.name,
63 68 'target_ref_type': pull_request.target_ref_parts.type,
64 69 'target_ref_name': pull_request.target_ref_parts.name,
65 70 'repo_url': pull_request_source_repo_url,
71 'source_repo': pull_request_source_repo.repo_name,
72 'target_repo': pull_request_target_repo.repo_name,
66 73 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
67 '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),
68 75 }
69 76 %>
70 <table style="text-align:left;vertical-align:middle;">
71 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;"><h4><a href="${pull_request_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s".') % data }</a></h4></td></tr>
72 <tr><td style="padding-right:20px;padding-top:15px;">${_('Title')}</td><td style="padding-top:15px;">${pull_request.title}</td></tr>
73 <tr><td style="padding-right:20px;">${_('Source')}</td><td>${base.tag_button(pull_request.source_ref_parts.name)} ${h.literal(_('%(source_ref_type)s of %(source_repo_url)s') % data)}</td></tr>
74 <tr><td style="padding-right:20px;">${_('Target')}</td><td>${base.tag_button(pull_request.target_ref_parts.name)} ${h.literal(_('%(target_ref_type)s of %(target_repo_url)s') % data)}</td></tr>
75 <tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${pull_request.description}</td></tr>
76 <tr><td style="padding-right:20px;">${_ungettext('%(num)s Commit', '%(num)s Commits', len(pull_request_commits)) % {'num': len(pull_request_commits)}}</td>
77 <td><ol style="margin:0 0 0 1em;padding:0;text-align:left;">
77
78 <table style="text-align:left;vertical-align:middle;width: 100%">
79 <tr>
80 <td style="width:100%;border-bottom:1px solid #dbd9da;">
81
82 <h4 style="margin: 0">
83 <div style="margin-bottom: 4px; color:#7E7F7F">
84 @${h.person(user.username)}
85 </div>
86 ${_('requested a')}
87 <a href="${pull_request_url}" style="${base.link_css()}">
88 ${_('pull request review.').format(**data) }
89 </a>
90 <div style="margin-top: 10px"></div>
91 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
92 </h4>
93
94 </td>
95 </tr>
96
97 </table>
98
99 <table style="text-align:left;vertical-align:middle;width: 100%">
100 ## spacing def
101 <tr>
102 <td style="width: 130px"></td>
103 <td></td>
104 </tr>
105
106 <tr>
107 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
108 <td style="line-height:20px;">
109 ${base.tag_button('{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name))} ${_('of')} ${data['source_repo_url']}
110 &rarr;
111 ${base.tag_button('{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name))} ${_('of')} ${data['target_repo_url']}
112 </td>
113 </tr>
114
115 <tr>
116 <td style="padding-right:20px;">${_('Pull request')}:</td>
117 <td>
118 <a href="${pull_request_url}" style="${base.link_css()}">
119 !${pull_request.pull_request_id}
120 </a>
121 </td>
122 </tr>
123 <tr>
124 <td style="padding-right:20px;">${_('Description')}:</td>
125 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
126 </tr>
127 <tr>
128 <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>
130 </tr>
131
132 <tr>
133 <td colspan="2">
134 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
78 135 % for commit_id, message in pull_request_commits:
79 <li style="margin:0 0 1em;"><pre style="margin:0 0 .5em">${h.short_id(commit_id)}</pre>
136 <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>
80 138 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
81 139 </li>
82 140 % endfor
83 </ol></td>
141 </ol>
142 </td>
84 143 </tr>
85 144 </table>
@@ -1,21 +1,22 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 4 <%def name="subject()" filter="n,trim,whitespace_filter">
5 5 Test "Subject" ${_('hello "world"')|n}
6 6 </%def>
7 7
8 8 <%def name="headers()" filter="n,trim">
9 9 X=Y
10 10 </%def>
11 11
12 12 ## plain text version of the email. Empty by default
13 13 <%def name="body_plaintext()" filter="n,trim">
14 14 Email Plaintext Body
15 15 </%def>
16 16
17 17 ## BODY GOES BELOW
18 <b>Email Body</b>
19
20 ${h.short_id('0' * 40)}
21 ${_('Translation')} No newline at end of file
18 <strong>Email Body</strong>
19 <br/>
20 <br/>
21 `h.short_id()`: ${h.short_id('0' * 40)}<br/>
22 ${_('Translation String')}<br/>
@@ -1,27 +1,59 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 4
4 5 <%def name="subject()" filter="n,trim,whitespace_filter">
5 6 RhodeCode new user registration: ${user.username}
6 7 </%def>
7 8
8 9 <%def name="body_plaintext()" filter="n,trim">
9 10
10 11 A new user `${user.username}` has registered on ${h.format_date(date)}
11 12
12 13 - Username: ${user.username}
13 14 - Full Name: ${user.first_name} ${user.last_name}
14 15 - Email: ${user.email}
15 16 - Profile link: ${h.route_url('user_profile', username=user.username)}
16 17
18 ---
17 19 ${self.plaintext_footer()}
18 20 </%def>
19 21
20 ## BODY GOES BELOW
21 <table style="text-align:left;vertical-align:middle;">
22 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;"><h4><a href="${h.route_url('user_profile', username=user.username)}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('New user %(user)s has registered on %(date)s') % {'user': user.username, 'date': h.format_date(date)}}</a></h4></td></tr>
23 <tr><td style="padding-right:20px;padding-top:20px;">${_('Username')}</td><td style="line-height:1;padding-top:20px;"><img style="margin-bottom:-5px;text-align:left;border:1px solid #dbd9da" src="${h.gravatar_url(user.email, 16)}" height="16" width="16">&nbsp;${user.username}</td></tr>
24 <tr><td style="padding-right:20px;">${_('Full Name')}</td><td>${user.first_name} ${user.last_name}</td></tr>
25 <tr><td style="padding-right:20px;">${_('Email')}</td><td>${user.email}</td></tr>
26 <tr><td style="padding-right:20px;">${_('Profile')}</td><td><a href="${h.route_url('user_profile', username=user.username)}">${h.route_url('user_profile', username=user.username)}</a></td></tr>
27 </table> No newline at end of file
22
23 <table style="text-align:left;vertical-align:middle;width: 100%">
24 <tr>
25 <td style="width:100%;border-bottom:1px solid #dbd9da;">
26 <h4 style="margin: 0">
27 <a href="${h.route_url('user_profile', username=user.username)}" style="${base.link_css()}">
28 ${_('New user {user} has registered on {date}').format(user=user.username, date=h.format_date(date))}
29 </a>
30 </h4>
31 </td>
32 </tr>
33 </table>
34
35 <table style="text-align:left;vertical-align:middle;width: 100%">
36 ## spacing def
37 <tr>
38 <td style="width: 130px"></td>
39 <td></td>
40 </tr>
41 <tr>
42 <td style="padding-right:20px;padding-top:20px;">${_('Username')}:</td>
43 <td style="line-height:1;padding-top:20px;">${user.username}</td>
44 </tr>
45 <tr>
46 <td style="padding-right:20px;">${_('Full Name')}:</td>
47 <td>${user.first_name} ${user.last_name}</td>
48 </tr>
49 <tr>
50 <td style="padding-right:20px;">${_('Email')}:</td>
51 <td>${user.email}</td>
52 </tr>
53 <tr>
54 <td style="padding-right:20px;">${_('Profile')}:</td>
55 <td>
56 <a href="${h.route_url('user_profile', username=user.username)}">${h.route_url('user_profile', username=user.username)}</a>
57 </td>
58 </tr>
59 </table>
@@ -1,143 +1,138 b''
1 import collections
2 1 # -*- coding: utf-8 -*-
3 2
4 3 # Copyright (C) 2010-2019 RhodeCode GmbH
5 4 #
6 5 # This program is free software: you can redistribute it and/or modify
7 6 # it under the terms of the GNU Affero General Public License, version 3
8 7 # (only), as published by the Free Software Foundation.
9 8 #
10 9 # This program is distributed in the hope that it will be useful,
11 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 12 # GNU General Public License for more details.
14 13 #
15 14 # You should have received a copy of the GNU Affero General Public License
16 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 16 #
18 17 # This program is dual-licensed. If you wish to learn more about the
19 18 # RhodeCode Enterprise Edition, including its added features, Support services,
20 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
21 20
22 21 import pytest
22 import collections
23 23
24 24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 25 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.model.db import User
26 27 from rhodecode.model.notification import EmailNotificationModel
27 28
28 29
29 30 def test_get_template_obj(app, request_stub):
30 31 template = EmailNotificationModel().get_renderer(
31 32 EmailNotificationModel.TYPE_TEST, request_stub)
32 33 assert isinstance(template, PyramidPartialRenderer)
33 34
34 35
35 36 def test_render_email(app, http_host_only_stub):
36 37 kwargs = {}
37 38 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
38 39 EmailNotificationModel.TYPE_TEST, **kwargs)
39 40
40 41 # subject
41 42 assert subject == 'Test "Subject" hello "world"'
42 43
43 44 # headers
44 45 assert headers == 'X=Y'
45 46
46 47 # body plaintext
47 48 assert body_plaintext == 'Email Plaintext Body'
48 49
49 50 # body
50 notification_footer = 'This is a notification from RhodeCode. http://%s/' \
51 % http_host_only_stub
52 assert notification_footer in body
51 notification_footer1 = 'This is a notification from RhodeCode.'
52 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
53 assert notification_footer1 in body
54 assert notification_footer2 in body
53 55 assert 'Email Body' in body
54 56
55 57
56 58 def test_render_pr_email(app, user_admin):
57
58 ref = collections.namedtuple('Ref',
59 'name, type')(
60 'fxies123', 'book'
61 )
59 ref = collections.namedtuple(
60 'Ref', 'name, type')('fxies123', 'book')
62 61
63 62 pr = collections.namedtuple('PullRequest',
64 63 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
65 64 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
66 65
67 source_repo = target_repo = collections.namedtuple('Repo',
68 'type, repo_name')(
69 'hg', 'pull_request_1')
66 source_repo = target_repo = collections.namedtuple(
67 'Repo', 'type, repo_name')('hg', 'pull_request_1')
70 68
71 69 kwargs = {
72 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
70 'user': User.get_first_super_admin(),
73 71 'pull_request': pr,
74 72 'pull_request_commits': [],
75 73
76 74 'pull_request_target_repo': target_repo,
77 75 'pull_request_target_repo_url': 'x',
78 76
79 77 'pull_request_source_repo': source_repo,
80 78 'pull_request_source_repo_url': 'x',
81 79
82 80 'pull_request_url': 'http://localhost/pr1',
83 81 }
84 82
85 83 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
86 84 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
87 85
88 86 # subject
89 assert subject == 'Marcin Kuzminski wants you to review pull request #200: "Example Pull Request"'
87 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
90 88
91 89
92 90 @pytest.mark.parametrize('mention', [
93 91 True,
94 92 False
95 93 ])
96 94 @pytest.mark.parametrize('email_type', [
97 95 EmailNotificationModel.TYPE_COMMIT_COMMENT,
98 96 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
99 97 ])
100 98 def test_render_comment_subject_no_newlines(app, mention, email_type):
101 ref = collections.namedtuple('Ref',
102 'name, type')(
103 'fxies123', 'book'
104 )
99 ref = collections.namedtuple(
100 'Ref', 'name, type')('fxies123', 'book')
105 101
106 102 pr = collections.namedtuple('PullRequest',
107 103 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
108 104 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
109 105
110 source_repo = target_repo = collections.namedtuple('Repo',
111 'type, repo_name')(
112 'hg', 'pull_request_1')
106 source_repo = target_repo = collections.namedtuple(
107 'Repo', 'type, repo_name')('hg', 'pull_request_1')
113 108
114 109 kwargs = {
115 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
110 'user': User.get_first_super_admin(),
116 111 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
117 112 'status_change': 'approved',
118 'commit_target_repo': AttributeDict(),
113 'commit_target_repo_url': 'http://foo.example.com/#comment1',
119 114 'repo_name': 'test-repo',
120 115 'comment_file': 'test-file.py',
121 116 'comment_line': 'n100',
122 117 'comment_type': 'note',
123 118 'commit_comment_url': 'http://comment-url',
124 119 'instance_url': 'http://rc-instance',
125 120 'comment_body': 'hello world',
126 121 'mention': mention,
127 122
128 123 'pr_comment_url': 'http://comment-url',
129 'pr_source_repo': AttributeDict(repo_name='foobar'),
130 'pr_source_repo_url': 'http://soirce-repo/url',
131 124 'pull_request': pr,
132 125 'pull_request_commits': [],
133 126
134 127 'pull_request_target_repo': target_repo,
135 128 'pull_request_target_repo_url': 'x',
136 129
137 130 'pull_request_source_repo': source_repo,
138 131 'pull_request_source_repo_url': 'x',
132
133 'pull_request_url': 'http://code.rc.com/_pr/123'
139 134 }
140 135 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
141 136 email_type, **kwargs)
142 137
143 138 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now