##// END OF EJS Templates
pull-requests: fixed a case when template marker was used in description field....
milka -
r4631:21211d2f stable
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,1658 +1,1679 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib
39 import urllib
40
40
41 base_url = {
41 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'repo_commits': '/{repo_name}/commits',
44 'repo_commits': '/{repo_name}/commits',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 }[name].format(**kwargs)
59 }[name].format(**kwargs)
60
60
61 if params:
61 if params:
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
63 return base_url
63 return base_url
64
64
65
65
66 @pytest.mark.usefixtures('app', 'autologin_user')
66 @pytest.mark.usefixtures('app', 'autologin_user')
67 @pytest.mark.backends("git", "hg")
67 @pytest.mark.backends("git", "hg")
68 class TestPullrequestsView(object):
68 class TestPullrequestsView(object):
69
69
70 def test_index(self, backend):
70 def test_index(self, backend):
71 self.app.get(route_path(
71 self.app.get(route_path(
72 'pullrequest_new',
72 'pullrequest_new',
73 repo_name=backend.repo_name))
73 repo_name=backend.repo_name))
74
74
75 def test_option_menu_create_pull_request_exists(self, backend):
75 def test_option_menu_create_pull_request_exists(self, backend):
76 repo_name = backend.repo_name
76 repo_name = backend.repo_name
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78
78
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 'pullrequest_new', repo_name=repo_name)
80 'pullrequest_new', repo_name=repo_name)
81 response.mustcontain(create_pr_link)
81 response.mustcontain(create_pr_link)
82
82
83 def test_create_pr_form_with_raw_commit_id(self, backend):
83 def test_create_pr_form_with_raw_commit_id(self, backend):
84 repo = backend.repo
84 repo = backend.repo
85
85
86 self.app.get(
86 self.app.get(
87 route_path('pullrequest_new', repo_name=repo.repo_name,
87 route_path('pullrequest_new', repo_name=repo.repo_name,
88 commit=repo.get_commit().raw_id),
88 commit=repo.get_commit().raw_id),
89 status=200)
89 status=200)
90
90
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 pull_request = pr_util.create_pull_request(
94 pull_request = pr_util.create_pull_request(
95 mergeable=pr_merge_enabled, enable_notifications=False)
95 mergeable=pr_merge_enabled, enable_notifications=False)
96
96
97 response = self.app.get(route_path(
97 response = self.app.get(route_path(
98 'pullrequest_show',
98 'pullrequest_show',
99 repo_name=pull_request.target_repo.scm_instance().name,
99 repo_name=pull_request.target_repo.scm_instance().name,
100 pull_request_id=pull_request.pull_request_id,
100 pull_request_id=pull_request.pull_request_id,
101 params={'range-diff': range_diff}))
101 params={'range-diff': range_diff}))
102
102
103 for commit_id in pull_request.revisions:
103 for commit_id in pull_request.revisions:
104 response.mustcontain(commit_id)
104 response.mustcontain(commit_id)
105
105
106 response.mustcontain(pull_request.target_ref_parts.type)
106 response.mustcontain(pull_request.target_ref_parts.type)
107 response.mustcontain(pull_request.target_ref_parts.name)
107 response.mustcontain(pull_request.target_ref_parts.name)
108
108
109 response.mustcontain('class="pull-request-merge"')
109 response.mustcontain('class="pull-request-merge"')
110
110
111 if pr_merge_enabled:
111 if pr_merge_enabled:
112 response.mustcontain('Pull request reviewer approval is pending')
112 response.mustcontain('Pull request reviewer approval is pending')
113 else:
113 else:
114 response.mustcontain('Server-side pull request merging is disabled.')
114 response.mustcontain('Server-side pull request merging is disabled.')
115
115
116 if range_diff == "1":
116 if range_diff == "1":
117 response.mustcontain('Turn off: Show the diff as commit range')
117 response.mustcontain('Turn off: Show the diff as commit range')
118
118
119 def test_show_versions_of_pr(self, backend, csrf_token):
119 def test_show_versions_of_pr(self, backend, csrf_token):
120 commits = [
120 commits = [
121 {'message': 'initial-commit',
121 {'message': 'initial-commit',
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123
123
124 {'message': 'commit-1',
124 {'message': 'commit-1',
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 # Above is the initial version of PR that changes a single line
126 # Above is the initial version of PR that changes a single line
127
127
128 # from now on we'll add 3x commit adding a nother line on each step
128 # from now on we'll add 3x commit adding a nother line on each step
129 {'message': 'commit-2',
129 {'message': 'commit-2',
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131
131
132 {'message': 'commit-3',
132 {'message': 'commit-3',
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134
134
135 {'message': 'commit-4',
135 {'message': 'commit-4',
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 ]
137 ]
138
138
139 commit_ids = backend.create_master_repo(commits)
139 commit_ids = backend.create_master_repo(commits)
140 target = backend.create_repo(heads=['initial-commit'])
140 target = backend.create_repo(heads=['initial-commit'])
141 source = backend.create_repo(heads=['commit-1'])
141 source = backend.create_repo(heads=['commit-1'])
142 source_repo_name = source.repo_name
142 source_repo_name = source.repo_name
143 target_repo_name = target.repo_name
143 target_repo_name = target.repo_name
144
144
145 target_ref = 'branch:{branch}:{commit_id}'.format(
145 target_ref = 'branch:{branch}:{commit_id}'.format(
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 source_ref = 'branch:{branch}:{commit_id}'.format(
147 source_ref = 'branch:{branch}:{commit_id}'.format(
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149
149
150 response = self.app.post(
150 response = self.app.post(
151 route_path('pullrequest_create', repo_name=source.repo_name),
151 route_path('pullrequest_create', repo_name=source.repo_name),
152 [
152 [
153 ('source_repo', source_repo_name),
153 ('source_repo', source_repo_name),
154 ('source_ref', source_ref),
154 ('source_ref', source_ref),
155 ('target_repo', target_repo_name),
155 ('target_repo', target_repo_name),
156 ('target_ref', target_ref),
156 ('target_ref', target_ref),
157 ('common_ancestor', commit_ids['initial-commit']),
157 ('common_ancestor', commit_ids['initial-commit']),
158 ('pullrequest_title', 'Title'),
158 ('pullrequest_title', 'Title'),
159 ('pullrequest_desc', 'Description'),
159 ('pullrequest_desc', 'Description'),
160 ('description_renderer', 'markdown'),
160 ('description_renderer', 'markdown'),
161 ('__start__', 'review_members:sequence'),
161 ('__start__', 'review_members:sequence'),
162 ('__start__', 'reviewer:mapping'),
162 ('__start__', 'reviewer:mapping'),
163 ('user_id', '1'),
163 ('user_id', '1'),
164 ('__start__', 'reasons:sequence'),
164 ('__start__', 'reasons:sequence'),
165 ('reason', 'Some reason'),
165 ('reason', 'Some reason'),
166 ('__end__', 'reasons:sequence'),
166 ('__end__', 'reasons:sequence'),
167 ('__start__', 'rules:sequence'),
167 ('__start__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
169 ('mandatory', 'False'),
169 ('mandatory', 'False'),
170 ('__end__', 'reviewer:mapping'),
170 ('__end__', 'reviewer:mapping'),
171 ('__end__', 'review_members:sequence'),
171 ('__end__', 'review_members:sequence'),
172 ('__start__', 'revisions:sequence'),
172 ('__start__', 'revisions:sequence'),
173 ('revisions', commit_ids['commit-1']),
173 ('revisions', commit_ids['commit-1']),
174 ('__end__', 'revisions:sequence'),
174 ('__end__', 'revisions:sequence'),
175 ('user', ''),
175 ('user', ''),
176 ('csrf_token', csrf_token),
176 ('csrf_token', csrf_token),
177 ],
177 ],
178 status=302)
178 status=302)
179
179
180 location = response.headers['Location']
180 location = response.headers['Location']
181
181
182 pull_request_id = location.rsplit('/', 1)[1]
182 pull_request_id = location.rsplit('/', 1)[1]
183 assert pull_request_id != 'new'
183 assert pull_request_id != 'new'
184 pull_request = PullRequest.get(int(pull_request_id))
184 pull_request = PullRequest.get(int(pull_request_id))
185
185
186 pull_request_id = pull_request.pull_request_id
186 pull_request_id = pull_request.pull_request_id
187
187
188 # Show initial version of PR
188 # Show initial version of PR
189 response = self.app.get(
189 response = self.app.get(
190 route_path('pullrequest_show',
190 route_path('pullrequest_show',
191 repo_name=target_repo_name,
191 repo_name=target_repo_name,
192 pull_request_id=pull_request_id))
192 pull_request_id=pull_request_id))
193
193
194 response.mustcontain('commit-1')
194 response.mustcontain('commit-1')
195 response.mustcontain(no=['commit-2'])
195 response.mustcontain(no=['commit-2'])
196 response.mustcontain(no=['commit-3'])
196 response.mustcontain(no=['commit-3'])
197 response.mustcontain(no=['commit-4'])
197 response.mustcontain(no=['commit-4'])
198
198
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 response.mustcontain(no=['LINE3'])
200 response.mustcontain(no=['LINE3'])
201 response.mustcontain(no=['LINE4'])
201 response.mustcontain(no=['LINE4'])
202 response.mustcontain(no=['LINE5'])
202 response.mustcontain(no=['LINE5'])
203
203
204 # update PR #1
204 # update PR #1
205 source_repo = Repository.get_by_repo_name(source_repo_name)
205 source_repo = Repository.get_by_repo_name(source_repo_name)
206 backend.pull_heads(source_repo, heads=['commit-2'])
206 backend.pull_heads(source_repo, heads=['commit-2'])
207 response = self.app.post(
207 response = self.app.post(
208 route_path('pullrequest_update',
208 route_path('pullrequest_update',
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
211
211
212 # update PR #2
212 # update PR #2
213 source_repo = Repository.get_by_repo_name(source_repo_name)
213 source_repo = Repository.get_by_repo_name(source_repo_name)
214 backend.pull_heads(source_repo, heads=['commit-3'])
214 backend.pull_heads(source_repo, heads=['commit-3'])
215 response = self.app.post(
215 response = self.app.post(
216 route_path('pullrequest_update',
216 route_path('pullrequest_update',
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
219
219
220 # update PR #3
220 # update PR #3
221 source_repo = Repository.get_by_repo_name(source_repo_name)
221 source_repo = Repository.get_by_repo_name(source_repo_name)
222 backend.pull_heads(source_repo, heads=['commit-4'])
222 backend.pull_heads(source_repo, heads=['commit-4'])
223 response = self.app.post(
223 response = self.app.post(
224 route_path('pullrequest_update',
224 route_path('pullrequest_update',
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
227
227
228 # Show final version !
228 # Show final version !
229 response = self.app.get(
229 response = self.app.get(
230 route_path('pullrequest_show',
230 route_path('pullrequest_show',
231 repo_name=target_repo_name,
231 repo_name=target_repo_name,
232 pull_request_id=pull_request_id))
232 pull_request_id=pull_request_id))
233
233
234 # 3 updates, and the latest == 4
234 # 3 updates, and the latest == 4
235 response.mustcontain('4 versions available for this pull request')
235 response.mustcontain('4 versions available for this pull request')
236 response.mustcontain(no=['rhodecode diff rendering error'])
236 response.mustcontain(no=['rhodecode diff rendering error'])
237
237
238 # initial show must have 3 commits, and 3 adds
238 # initial show must have 3 commits, and 3 adds
239 response.mustcontain('commit-1')
239 response.mustcontain('commit-1')
240 response.mustcontain('commit-2')
240 response.mustcontain('commit-2')
241 response.mustcontain('commit-3')
241 response.mustcontain('commit-3')
242 response.mustcontain('commit-4')
242 response.mustcontain('commit-4')
243
243
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248
248
249 # fetch versions
249 # fetch versions
250 pr = PullRequest.get(pull_request_id)
250 pr = PullRequest.get(pull_request_id)
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 assert len(versions) == 3
252 assert len(versions) == 3
253
253
254 # show v1,v2,v3,v4
254 # show v1,v2,v3,v4
255 def cb_line(text):
255 def cb_line(text):
256 return 'cb-addition"></span><span>{}</span>'.format(text)
256 return 'cb-addition"></span><span>{}</span>'.format(text)
257
257
258 def cb_context(text):
258 def cb_context(text):
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 '</span><span>{}</span></span>'.format(text)
260 '</span><span>{}</span></span>'.format(text)
261
261
262 commit_tests = {
262 commit_tests = {
263 # in response, not in response
263 # in response, not in response
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 }
268 }
269 diff_tests = {
269 diff_tests = {
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 }
274 }
275 for idx, ver in enumerate(versions, 1):
275 for idx, ver in enumerate(versions, 1):
276
276
277 response = self.app.get(
277 response = self.app.get(
278 route_path('pullrequest_show',
278 route_path('pullrequest_show',
279 repo_name=target_repo_name,
279 repo_name=target_repo_name,
280 pull_request_id=pull_request_id,
280 pull_request_id=pull_request_id,
281 params={'version': ver}))
281 params={'version': ver}))
282
282
283 response.mustcontain(no=['rhodecode diff rendering error'])
283 response.mustcontain(no=['rhodecode diff rendering error'])
284 response.mustcontain('Showing changes at v{}'.format(idx))
284 response.mustcontain('Showing changes at v{}'.format(idx))
285
285
286 yes, no = commit_tests[idx]
286 yes, no = commit_tests[idx]
287 for y in yes:
287 for y in yes:
288 response.mustcontain(y)
288 response.mustcontain(y)
289 for n in no:
289 for n in no:
290 response.mustcontain(no=n)
290 response.mustcontain(no=n)
291
291
292 yes, no = diff_tests[idx]
292 yes, no = diff_tests[idx]
293 for y in yes:
293 for y in yes:
294 response.mustcontain(cb_line(y))
294 response.mustcontain(cb_line(y))
295 for n in no:
295 for n in no:
296 response.mustcontain(no=n)
296 response.mustcontain(no=n)
297
297
298 # show diff between versions
298 # show diff between versions
299 diff_compare_tests = {
299 diff_compare_tests = {
300 1: (['LINE3'], ['LINE1', 'LINE2']),
300 1: (['LINE3'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 }
303 }
304 for idx, ver in enumerate(versions, 1):
304 for idx, ver in enumerate(versions, 1):
305 adds, context = diff_compare_tests[idx]
305 adds, context = diff_compare_tests[idx]
306
306
307 to_ver = ver+1
307 to_ver = ver+1
308 if idx == 3:
308 if idx == 3:
309 to_ver = 'latest'
309 to_ver = 'latest'
310
310
311 response = self.app.get(
311 response = self.app.get(
312 route_path('pullrequest_show',
312 route_path('pullrequest_show',
313 repo_name=target_repo_name,
313 repo_name=target_repo_name,
314 pull_request_id=pull_request_id,
314 pull_request_id=pull_request_id,
315 params={'from_version': versions[0], 'version': to_ver}))
315 params={'from_version': versions[0], 'version': to_ver}))
316
316
317 response.mustcontain(no=['rhodecode diff rendering error'])
317 response.mustcontain(no=['rhodecode diff rendering error'])
318
318
319 for a in adds:
319 for a in adds:
320 response.mustcontain(cb_line(a))
320 response.mustcontain(cb_line(a))
321 for c in context:
321 for c in context:
322 response.mustcontain(cb_context(c))
322 response.mustcontain(cb_context(c))
323
323
324 # test version v2 -> v3
324 # test version v2 -> v3
325 response = self.app.get(
325 response = self.app.get(
326 route_path('pullrequest_show',
326 route_path('pullrequest_show',
327 repo_name=target_repo_name,
327 repo_name=target_repo_name,
328 pull_request_id=pull_request_id,
328 pull_request_id=pull_request_id,
329 params={'from_version': versions[1], 'version': versions[2]}))
329 params={'from_version': versions[1], 'version': versions[2]}))
330
330
331 response.mustcontain(cb_context('LINE1'))
331 response.mustcontain(cb_context('LINE1'))
332 response.mustcontain(cb_context('LINE2'))
332 response.mustcontain(cb_context('LINE2'))
333 response.mustcontain(cb_context('LINE3'))
333 response.mustcontain(cb_context('LINE3'))
334 response.mustcontain(cb_line('LINE4'))
334 response.mustcontain(cb_line('LINE4'))
335
335
336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
337 # Logout
337 # Logout
338 response = self.app.post(
338 response = self.app.post(
339 h.route_path('logout'),
339 h.route_path('logout'),
340 params={'csrf_token': csrf_token})
340 params={'csrf_token': csrf_token})
341 # Login as regular user
341 # Login as regular user
342 response = self.app.post(h.route_path('login'),
342 response = self.app.post(h.route_path('login'),
343 {'username': TEST_USER_REGULAR_LOGIN,
343 {'username': TEST_USER_REGULAR_LOGIN,
344 'password': 'test12'})
344 'password': 'test12'})
345
345
346 pull_request = pr_util.create_pull_request(
346 pull_request = pr_util.create_pull_request(
347 author=TEST_USER_REGULAR_LOGIN)
347 author=TEST_USER_REGULAR_LOGIN)
348
348
349 response = self.app.get(route_path(
349 response = self.app.get(route_path(
350 'pullrequest_show',
350 'pullrequest_show',
351 repo_name=pull_request.target_repo.scm_instance().name,
351 repo_name=pull_request.target_repo.scm_instance().name,
352 pull_request_id=pull_request.pull_request_id))
352 pull_request_id=pull_request.pull_request_id))
353
353
354 response.mustcontain('Server-side pull request merging is disabled.')
354 response.mustcontain('Server-side pull request merging is disabled.')
355
355
356 assert_response = response.assert_response()
356 assert_response = response.assert_response()
357 # for regular user without a merge permissions, we don't see it
357 # for regular user without a merge permissions, we don't see it
358 assert_response.no_element_exists('#close-pull-request-action')
358 assert_response.no_element_exists('#close-pull-request-action')
359
359
360 user_util.grant_user_permission_to_repo(
360 user_util.grant_user_permission_to_repo(
361 pull_request.target_repo,
361 pull_request.target_repo,
362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
363 'repository.write')
363 'repository.write')
364 response = self.app.get(route_path(
364 response = self.app.get(route_path(
365 'pullrequest_show',
365 'pullrequest_show',
366 repo_name=pull_request.target_repo.scm_instance().name,
366 repo_name=pull_request.target_repo.scm_instance().name,
367 pull_request_id=pull_request.pull_request_id))
367 pull_request_id=pull_request.pull_request_id))
368
368
369 response.mustcontain('Server-side pull request merging is disabled.')
369 response.mustcontain('Server-side pull request merging is disabled.')
370
370
371 assert_response = response.assert_response()
371 assert_response = response.assert_response()
372 # now regular user has a merge permissions, we have CLOSE button
372 # now regular user has a merge permissions, we have CLOSE button
373 assert_response.one_element_exists('#close-pull-request-action')
373 assert_response.one_element_exists('#close-pull-request-action')
374
374
375 def test_show_invalid_commit_id(self, pr_util):
375 def test_show_invalid_commit_id(self, pr_util):
376 # Simulating invalid revisions which will cause a lookup error
376 # Simulating invalid revisions which will cause a lookup error
377 pull_request = pr_util.create_pull_request()
377 pull_request = pr_util.create_pull_request()
378 pull_request.revisions = ['invalid']
378 pull_request.revisions = ['invalid']
379 Session().add(pull_request)
379 Session().add(pull_request)
380 Session().commit()
380 Session().commit()
381
381
382 response = self.app.get(route_path(
382 response = self.app.get(route_path(
383 'pullrequest_show',
383 'pullrequest_show',
384 repo_name=pull_request.target_repo.scm_instance().name,
384 repo_name=pull_request.target_repo.scm_instance().name,
385 pull_request_id=pull_request.pull_request_id))
385 pull_request_id=pull_request.pull_request_id))
386
386
387 for commit_id in pull_request.revisions:
387 for commit_id in pull_request.revisions:
388 response.mustcontain(commit_id)
388 response.mustcontain(commit_id)
389
389
390 def test_show_invalid_source_reference(self, pr_util):
390 def test_show_invalid_source_reference(self, pr_util):
391 pull_request = pr_util.create_pull_request()
391 pull_request = pr_util.create_pull_request()
392 pull_request.source_ref = 'branch:b:invalid'
392 pull_request.source_ref = 'branch:b:invalid'
393 Session().add(pull_request)
393 Session().add(pull_request)
394 Session().commit()
394 Session().commit()
395
395
396 self.app.get(route_path(
396 self.app.get(route_path(
397 'pullrequest_show',
397 'pullrequest_show',
398 repo_name=pull_request.target_repo.scm_instance().name,
398 repo_name=pull_request.target_repo.scm_instance().name,
399 pull_request_id=pull_request.pull_request_id))
399 pull_request_id=pull_request.pull_request_id))
400
400
401 def test_edit_title_description(self, pr_util, csrf_token):
401 def test_edit_title_description(self, pr_util, csrf_token):
402 pull_request = pr_util.create_pull_request()
402 pull_request = pr_util.create_pull_request()
403 pull_request_id = pull_request.pull_request_id
403 pull_request_id = pull_request.pull_request_id
404
404
405 response = self.app.post(
405 response = self.app.post(
406 route_path('pullrequest_update',
406 route_path('pullrequest_update',
407 repo_name=pull_request.target_repo.repo_name,
407 repo_name=pull_request.target_repo.repo_name,
408 pull_request_id=pull_request_id),
408 pull_request_id=pull_request_id),
409 params={
409 params={
410 'edit_pull_request': 'true',
410 'edit_pull_request': 'true',
411 'title': 'New title',
411 'title': 'New title',
412 'description': 'New description',
412 'description': 'New description',
413 'csrf_token': csrf_token})
413 'csrf_token': csrf_token})
414
414
415 assert_session_flash(
415 assert_session_flash(
416 response, u'Pull request title & description updated.',
416 response, u'Pull request title & description updated.',
417 category='success')
417 category='success')
418
418
419 pull_request = PullRequest.get(pull_request_id)
419 pull_request = PullRequest.get(pull_request_id)
420 assert pull_request.title == 'New title'
420 assert pull_request.title == 'New title'
421 assert pull_request.description == 'New description'
421 assert pull_request.description == 'New description'
422
422
423 def test_edit_title_description(self, pr_util, csrf_token):
424 pull_request = pr_util.create_pull_request()
425 pull_request_id = pull_request.pull_request_id
426
427 response = self.app.post(
428 route_path('pullrequest_update',
429 repo_name=pull_request.target_repo.repo_name,
430 pull_request_id=pull_request_id),
431 params={
432 'edit_pull_request': 'true',
433 'title': 'New title {} {2} {foo}',
434 'description': 'New description',
435 'csrf_token': csrf_token})
436
437 assert_session_flash(
438 response, u'Pull request title & description updated.',
439 category='success')
440
441 pull_request = PullRequest.get(pull_request_id)
442 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
443
423 def test_edit_title_description_closed(self, pr_util, csrf_token):
444 def test_edit_title_description_closed(self, pr_util, csrf_token):
424 pull_request = pr_util.create_pull_request()
445 pull_request = pr_util.create_pull_request()
425 pull_request_id = pull_request.pull_request_id
446 pull_request_id = pull_request.pull_request_id
426 repo_name = pull_request.target_repo.repo_name
447 repo_name = pull_request.target_repo.repo_name
427 pr_util.close()
448 pr_util.close()
428
449
429 response = self.app.post(
450 response = self.app.post(
430 route_path('pullrequest_update',
451 route_path('pullrequest_update',
431 repo_name=repo_name, pull_request_id=pull_request_id),
452 repo_name=repo_name, pull_request_id=pull_request_id),
432 params={
453 params={
433 'edit_pull_request': 'true',
454 'edit_pull_request': 'true',
434 'title': 'New title',
455 'title': 'New title',
435 'description': 'New description',
456 'description': 'New description',
436 'csrf_token': csrf_token}, status=200)
457 'csrf_token': csrf_token}, status=200)
437 assert_session_flash(
458 assert_session_flash(
438 response, u'Cannot update closed pull requests.',
459 response, u'Cannot update closed pull requests.',
439 category='error')
460 category='error')
440
461
441 def test_update_invalid_source_reference(self, pr_util, csrf_token):
462 def test_update_invalid_source_reference(self, pr_util, csrf_token):
442 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
463 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
443
464
444 pull_request = pr_util.create_pull_request()
465 pull_request = pr_util.create_pull_request()
445 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
466 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
446 Session().add(pull_request)
467 Session().add(pull_request)
447 Session().commit()
468 Session().commit()
448
469
449 pull_request_id = pull_request.pull_request_id
470 pull_request_id = pull_request.pull_request_id
450
471
451 response = self.app.post(
472 response = self.app.post(
452 route_path('pullrequest_update',
473 route_path('pullrequest_update',
453 repo_name=pull_request.target_repo.repo_name,
474 repo_name=pull_request.target_repo.repo_name,
454 pull_request_id=pull_request_id),
475 pull_request_id=pull_request_id),
455 params={'update_commits': 'true', 'csrf_token': csrf_token})
476 params={'update_commits': 'true', 'csrf_token': csrf_token})
456
477
457 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
478 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
458 UpdateFailureReason.MISSING_SOURCE_REF])
479 UpdateFailureReason.MISSING_SOURCE_REF])
459 assert_session_flash(response, expected_msg, category='error')
480 assert_session_flash(response, expected_msg, category='error')
460
481
461 def test_missing_target_reference(self, pr_util, csrf_token):
482 def test_missing_target_reference(self, pr_util, csrf_token):
462 from rhodecode.lib.vcs.backends.base import MergeFailureReason
483 from rhodecode.lib.vcs.backends.base import MergeFailureReason
463 pull_request = pr_util.create_pull_request(
484 pull_request = pr_util.create_pull_request(
464 approved=True, mergeable=True)
485 approved=True, mergeable=True)
465 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
486 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
466 pull_request.target_ref = unicode_reference
487 pull_request.target_ref = unicode_reference
467 Session().add(pull_request)
488 Session().add(pull_request)
468 Session().commit()
489 Session().commit()
469
490
470 pull_request_id = pull_request.pull_request_id
491 pull_request_id = pull_request.pull_request_id
471 pull_request_url = route_path(
492 pull_request_url = route_path(
472 'pullrequest_show',
493 'pullrequest_show',
473 repo_name=pull_request.target_repo.repo_name,
494 repo_name=pull_request.target_repo.repo_name,
474 pull_request_id=pull_request_id)
495 pull_request_id=pull_request_id)
475
496
476 response = self.app.get(pull_request_url)
497 response = self.app.get(pull_request_url)
477 target_ref_id = 'invalid-branch'
498 target_ref_id = 'invalid-branch'
478 merge_resp = MergeResponse(
499 merge_resp = MergeResponse(
479 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
500 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
480 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
501 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
481 response.assert_response().element_contains(
502 response.assert_response().element_contains(
482 'div[data-role="merge-message"]', merge_resp.merge_status_message)
503 'div[data-role="merge-message"]', merge_resp.merge_status_message)
483
504
484 def test_comment_and_close_pull_request_custom_message_approved(
505 def test_comment_and_close_pull_request_custom_message_approved(
485 self, pr_util, csrf_token, xhr_header):
506 self, pr_util, csrf_token, xhr_header):
486
507
487 pull_request = pr_util.create_pull_request(approved=True)
508 pull_request = pr_util.create_pull_request(approved=True)
488 pull_request_id = pull_request.pull_request_id
509 pull_request_id = pull_request.pull_request_id
489 author = pull_request.user_id
510 author = pull_request.user_id
490 repo = pull_request.target_repo.repo_id
511 repo = pull_request.target_repo.repo_id
491
512
492 self.app.post(
513 self.app.post(
493 route_path('pullrequest_comment_create',
514 route_path('pullrequest_comment_create',
494 repo_name=pull_request.target_repo.scm_instance().name,
515 repo_name=pull_request.target_repo.scm_instance().name,
495 pull_request_id=pull_request_id),
516 pull_request_id=pull_request_id),
496 params={
517 params={
497 'close_pull_request': '1',
518 'close_pull_request': '1',
498 'text': 'Closing a PR',
519 'text': 'Closing a PR',
499 'csrf_token': csrf_token},
520 'csrf_token': csrf_token},
500 extra_environ=xhr_header,)
521 extra_environ=xhr_header,)
501
522
502 journal = UserLog.query()\
523 journal = UserLog.query()\
503 .filter(UserLog.user_id == author)\
524 .filter(UserLog.user_id == author)\
504 .filter(UserLog.repository_id == repo) \
525 .filter(UserLog.repository_id == repo) \
505 .order_by(UserLog.user_log_id.asc()) \
526 .order_by(UserLog.user_log_id.asc()) \
506 .all()
527 .all()
507 assert journal[-1].action == 'repo.pull_request.close'
528 assert journal[-1].action == 'repo.pull_request.close'
508
529
509 pull_request = PullRequest.get(pull_request_id)
530 pull_request = PullRequest.get(pull_request_id)
510 assert pull_request.is_closed()
531 assert pull_request.is_closed()
511
532
512 status = ChangesetStatusModel().get_status(
533 status = ChangesetStatusModel().get_status(
513 pull_request.source_repo, pull_request=pull_request)
534 pull_request.source_repo, pull_request=pull_request)
514 assert status == ChangesetStatus.STATUS_APPROVED
535 assert status == ChangesetStatus.STATUS_APPROVED
515 comments = ChangesetComment().query() \
536 comments = ChangesetComment().query() \
516 .filter(ChangesetComment.pull_request == pull_request) \
537 .filter(ChangesetComment.pull_request == pull_request) \
517 .order_by(ChangesetComment.comment_id.asc())\
538 .order_by(ChangesetComment.comment_id.asc())\
518 .all()
539 .all()
519 assert comments[-1].text == 'Closing a PR'
540 assert comments[-1].text == 'Closing a PR'
520
541
521 def test_comment_force_close_pull_request_rejected(
542 def test_comment_force_close_pull_request_rejected(
522 self, pr_util, csrf_token, xhr_header):
543 self, pr_util, csrf_token, xhr_header):
523 pull_request = pr_util.create_pull_request()
544 pull_request = pr_util.create_pull_request()
524 pull_request_id = pull_request.pull_request_id
545 pull_request_id = pull_request.pull_request_id
525 PullRequestModel().update_reviewers(
546 PullRequestModel().update_reviewers(
526 pull_request_id, [
547 pull_request_id, [
527 (1, ['reason'], False, 'reviewer', []),
548 (1, ['reason'], False, 'reviewer', []),
528 (2, ['reason2'], False, 'reviewer', [])],
549 (2, ['reason2'], False, 'reviewer', [])],
529 pull_request.author)
550 pull_request.author)
530 author = pull_request.user_id
551 author = pull_request.user_id
531 repo = pull_request.target_repo.repo_id
552 repo = pull_request.target_repo.repo_id
532
553
533 self.app.post(
554 self.app.post(
534 route_path('pullrequest_comment_create',
555 route_path('pullrequest_comment_create',
535 repo_name=pull_request.target_repo.scm_instance().name,
556 repo_name=pull_request.target_repo.scm_instance().name,
536 pull_request_id=pull_request_id),
557 pull_request_id=pull_request_id),
537 params={
558 params={
538 'close_pull_request': '1',
559 'close_pull_request': '1',
539 'csrf_token': csrf_token},
560 'csrf_token': csrf_token},
540 extra_environ=xhr_header)
561 extra_environ=xhr_header)
541
562
542 pull_request = PullRequest.get(pull_request_id)
563 pull_request = PullRequest.get(pull_request_id)
543
564
544 journal = UserLog.query()\
565 journal = UserLog.query()\
545 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
566 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
546 .order_by(UserLog.user_log_id.asc()) \
567 .order_by(UserLog.user_log_id.asc()) \
547 .all()
568 .all()
548 assert journal[-1].action == 'repo.pull_request.close'
569 assert journal[-1].action == 'repo.pull_request.close'
549
570
550 # check only the latest status, not the review status
571 # check only the latest status, not the review status
551 status = ChangesetStatusModel().get_status(
572 status = ChangesetStatusModel().get_status(
552 pull_request.source_repo, pull_request=pull_request)
573 pull_request.source_repo, pull_request=pull_request)
553 assert status == ChangesetStatus.STATUS_REJECTED
574 assert status == ChangesetStatus.STATUS_REJECTED
554
575
555 def test_comment_and_close_pull_request(
576 def test_comment_and_close_pull_request(
556 self, pr_util, csrf_token, xhr_header):
577 self, pr_util, csrf_token, xhr_header):
557 pull_request = pr_util.create_pull_request()
578 pull_request = pr_util.create_pull_request()
558 pull_request_id = pull_request.pull_request_id
579 pull_request_id = pull_request.pull_request_id
559
580
560 response = self.app.post(
581 response = self.app.post(
561 route_path('pullrequest_comment_create',
582 route_path('pullrequest_comment_create',
562 repo_name=pull_request.target_repo.scm_instance().name,
583 repo_name=pull_request.target_repo.scm_instance().name,
563 pull_request_id=pull_request.pull_request_id),
584 pull_request_id=pull_request.pull_request_id),
564 params={
585 params={
565 'close_pull_request': 'true',
586 'close_pull_request': 'true',
566 'csrf_token': csrf_token},
587 'csrf_token': csrf_token},
567 extra_environ=xhr_header)
588 extra_environ=xhr_header)
568
589
569 assert response.json
590 assert response.json
570
591
571 pull_request = PullRequest.get(pull_request_id)
592 pull_request = PullRequest.get(pull_request_id)
572 assert pull_request.is_closed()
593 assert pull_request.is_closed()
573
594
574 # check only the latest status, not the review status
595 # check only the latest status, not the review status
575 status = ChangesetStatusModel().get_status(
596 status = ChangesetStatusModel().get_status(
576 pull_request.source_repo, pull_request=pull_request)
597 pull_request.source_repo, pull_request=pull_request)
577 assert status == ChangesetStatus.STATUS_REJECTED
598 assert status == ChangesetStatus.STATUS_REJECTED
578
599
579 def test_comment_and_close_pull_request_try_edit_comment(
600 def test_comment_and_close_pull_request_try_edit_comment(
580 self, pr_util, csrf_token, xhr_header
601 self, pr_util, csrf_token, xhr_header
581 ):
602 ):
582 pull_request = pr_util.create_pull_request()
603 pull_request = pr_util.create_pull_request()
583 pull_request_id = pull_request.pull_request_id
604 pull_request_id = pull_request.pull_request_id
584 target_scm = pull_request.target_repo.scm_instance()
605 target_scm = pull_request.target_repo.scm_instance()
585 target_scm_name = target_scm.name
606 target_scm_name = target_scm.name
586
607
587 response = self.app.post(
608 response = self.app.post(
588 route_path(
609 route_path(
589 'pullrequest_comment_create',
610 'pullrequest_comment_create',
590 repo_name=target_scm_name,
611 repo_name=target_scm_name,
591 pull_request_id=pull_request_id,
612 pull_request_id=pull_request_id,
592 ),
613 ),
593 params={
614 params={
594 'close_pull_request': 'true',
615 'close_pull_request': 'true',
595 'csrf_token': csrf_token,
616 'csrf_token': csrf_token,
596 },
617 },
597 extra_environ=xhr_header)
618 extra_environ=xhr_header)
598
619
599 assert response.json
620 assert response.json
600
621
601 pull_request = PullRequest.get(pull_request_id)
622 pull_request = PullRequest.get(pull_request_id)
602 target_scm = pull_request.target_repo.scm_instance()
623 target_scm = pull_request.target_repo.scm_instance()
603 target_scm_name = target_scm.name
624 target_scm_name = target_scm.name
604 assert pull_request.is_closed()
625 assert pull_request.is_closed()
605
626
606 # check only the latest status, not the review status
627 # check only the latest status, not the review status
607 status = ChangesetStatusModel().get_status(
628 status = ChangesetStatusModel().get_status(
608 pull_request.source_repo, pull_request=pull_request)
629 pull_request.source_repo, pull_request=pull_request)
609 assert status == ChangesetStatus.STATUS_REJECTED
630 assert status == ChangesetStatus.STATUS_REJECTED
610
631
611 for comment_id in response.json.keys():
632 for comment_id in response.json.keys():
612 test_text = 'test'
633 test_text = 'test'
613 response = self.app.post(
634 response = self.app.post(
614 route_path(
635 route_path(
615 'pullrequest_comment_edit',
636 'pullrequest_comment_edit',
616 repo_name=target_scm_name,
637 repo_name=target_scm_name,
617 pull_request_id=pull_request_id,
638 pull_request_id=pull_request_id,
618 comment_id=comment_id,
639 comment_id=comment_id,
619 ),
640 ),
620 extra_environ=xhr_header,
641 extra_environ=xhr_header,
621 params={
642 params={
622 'csrf_token': csrf_token,
643 'csrf_token': csrf_token,
623 'text': test_text,
644 'text': test_text,
624 },
645 },
625 status=403,
646 status=403,
626 )
647 )
627 assert response.status_int == 403
648 assert response.status_int == 403
628
649
629 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
650 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
630 pull_request = pr_util.create_pull_request()
651 pull_request = pr_util.create_pull_request()
631 target_scm = pull_request.target_repo.scm_instance()
652 target_scm = pull_request.target_repo.scm_instance()
632 target_scm_name = target_scm.name
653 target_scm_name = target_scm.name
633
654
634 response = self.app.post(
655 response = self.app.post(
635 route_path(
656 route_path(
636 'pullrequest_comment_create',
657 'pullrequest_comment_create',
637 repo_name=target_scm_name,
658 repo_name=target_scm_name,
638 pull_request_id=pull_request.pull_request_id),
659 pull_request_id=pull_request.pull_request_id),
639 params={
660 params={
640 'csrf_token': csrf_token,
661 'csrf_token': csrf_token,
641 'text': 'init',
662 'text': 'init',
642 },
663 },
643 extra_environ=xhr_header,
664 extra_environ=xhr_header,
644 )
665 )
645 assert response.json
666 assert response.json
646
667
647 for comment_id in response.json.keys():
668 for comment_id in response.json.keys():
648 assert comment_id
669 assert comment_id
649 test_text = 'test'
670 test_text = 'test'
650 self.app.post(
671 self.app.post(
651 route_path(
672 route_path(
652 'pullrequest_comment_edit',
673 'pullrequest_comment_edit',
653 repo_name=target_scm_name,
674 repo_name=target_scm_name,
654 pull_request_id=pull_request.pull_request_id,
675 pull_request_id=pull_request.pull_request_id,
655 comment_id=comment_id,
676 comment_id=comment_id,
656 ),
677 ),
657 extra_environ=xhr_header,
678 extra_environ=xhr_header,
658 params={
679 params={
659 'csrf_token': csrf_token,
680 'csrf_token': csrf_token,
660 'text': test_text,
681 'text': test_text,
661 'version': '0',
682 'version': '0',
662 },
683 },
663
684
664 )
685 )
665 text_form_db = ChangesetComment.query().filter(
686 text_form_db = ChangesetComment.query().filter(
666 ChangesetComment.comment_id == comment_id).first().text
687 ChangesetComment.comment_id == comment_id).first().text
667 assert test_text == text_form_db
688 assert test_text == text_form_db
668
689
669 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
690 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
670 pull_request = pr_util.create_pull_request()
691 pull_request = pr_util.create_pull_request()
671 target_scm = pull_request.target_repo.scm_instance()
692 target_scm = pull_request.target_repo.scm_instance()
672 target_scm_name = target_scm.name
693 target_scm_name = target_scm.name
673
694
674 response = self.app.post(
695 response = self.app.post(
675 route_path(
696 route_path(
676 'pullrequest_comment_create',
697 'pullrequest_comment_create',
677 repo_name=target_scm_name,
698 repo_name=target_scm_name,
678 pull_request_id=pull_request.pull_request_id),
699 pull_request_id=pull_request.pull_request_id),
679 params={
700 params={
680 'csrf_token': csrf_token,
701 'csrf_token': csrf_token,
681 'text': 'init',
702 'text': 'init',
682 },
703 },
683 extra_environ=xhr_header,
704 extra_environ=xhr_header,
684 )
705 )
685 assert response.json
706 assert response.json
686
707
687 for comment_id in response.json.keys():
708 for comment_id in response.json.keys():
688 test_text = 'init'
709 test_text = 'init'
689 response = self.app.post(
710 response = self.app.post(
690 route_path(
711 route_path(
691 'pullrequest_comment_edit',
712 'pullrequest_comment_edit',
692 repo_name=target_scm_name,
713 repo_name=target_scm_name,
693 pull_request_id=pull_request.pull_request_id,
714 pull_request_id=pull_request.pull_request_id,
694 comment_id=comment_id,
715 comment_id=comment_id,
695 ),
716 ),
696 extra_environ=xhr_header,
717 extra_environ=xhr_header,
697 params={
718 params={
698 'csrf_token': csrf_token,
719 'csrf_token': csrf_token,
699 'text': test_text,
720 'text': test_text,
700 'version': '0',
721 'version': '0',
701 },
722 },
702 status=404,
723 status=404,
703
724
704 )
725 )
705 assert response.status_int == 404
726 assert response.status_int == 404
706
727
707 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
728 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
708 pull_request = pr_util.create_pull_request()
729 pull_request = pr_util.create_pull_request()
709 target_scm = pull_request.target_repo.scm_instance()
730 target_scm = pull_request.target_repo.scm_instance()
710 target_scm_name = target_scm.name
731 target_scm_name = target_scm.name
711
732
712 response = self.app.post(
733 response = self.app.post(
713 route_path(
734 route_path(
714 'pullrequest_comment_create',
735 'pullrequest_comment_create',
715 repo_name=target_scm_name,
736 repo_name=target_scm_name,
716 pull_request_id=pull_request.pull_request_id),
737 pull_request_id=pull_request.pull_request_id),
717 params={
738 params={
718 'csrf_token': csrf_token,
739 'csrf_token': csrf_token,
719 'text': 'init',
740 'text': 'init',
720 },
741 },
721 extra_environ=xhr_header,
742 extra_environ=xhr_header,
722 )
743 )
723 assert response.json
744 assert response.json
724 for comment_id in response.json.keys():
745 for comment_id in response.json.keys():
725 test_text = 'test'
746 test_text = 'test'
726 self.app.post(
747 self.app.post(
727 route_path(
748 route_path(
728 'pullrequest_comment_edit',
749 'pullrequest_comment_edit',
729 repo_name=target_scm_name,
750 repo_name=target_scm_name,
730 pull_request_id=pull_request.pull_request_id,
751 pull_request_id=pull_request.pull_request_id,
731 comment_id=comment_id,
752 comment_id=comment_id,
732 ),
753 ),
733 extra_environ=xhr_header,
754 extra_environ=xhr_header,
734 params={
755 params={
735 'csrf_token': csrf_token,
756 'csrf_token': csrf_token,
736 'text': test_text,
757 'text': test_text,
737 'version': '0',
758 'version': '0',
738 },
759 },
739
760
740 )
761 )
741 test_text_v2 = 'test_v2'
762 test_text_v2 = 'test_v2'
742 response = self.app.post(
763 response = self.app.post(
743 route_path(
764 route_path(
744 'pullrequest_comment_edit',
765 'pullrequest_comment_edit',
745 repo_name=target_scm_name,
766 repo_name=target_scm_name,
746 pull_request_id=pull_request.pull_request_id,
767 pull_request_id=pull_request.pull_request_id,
747 comment_id=comment_id,
768 comment_id=comment_id,
748 ),
769 ),
749 extra_environ=xhr_header,
770 extra_environ=xhr_header,
750 params={
771 params={
751 'csrf_token': csrf_token,
772 'csrf_token': csrf_token,
752 'text': test_text_v2,
773 'text': test_text_v2,
753 'version': '0',
774 'version': '0',
754 },
775 },
755 status=409,
776 status=409,
756 )
777 )
757 assert response.status_int == 409
778 assert response.status_int == 409
758
779
759 text_form_db = ChangesetComment.query().filter(
780 text_form_db = ChangesetComment.query().filter(
760 ChangesetComment.comment_id == comment_id).first().text
781 ChangesetComment.comment_id == comment_id).first().text
761
782
762 assert test_text == text_form_db
783 assert test_text == text_form_db
763 assert test_text_v2 != text_form_db
784 assert test_text_v2 != text_form_db
764
785
765 def test_comment_and_comment_edit_permissions_forbidden(
786 def test_comment_and_comment_edit_permissions_forbidden(
766 self, autologin_regular_user, user_regular, user_admin, pr_util,
787 self, autologin_regular_user, user_regular, user_admin, pr_util,
767 csrf_token, xhr_header):
788 csrf_token, xhr_header):
768 pull_request = pr_util.create_pull_request(
789 pull_request = pr_util.create_pull_request(
769 author=user_admin.username, enable_notifications=False)
790 author=user_admin.username, enable_notifications=False)
770 comment = CommentsModel().create(
791 comment = CommentsModel().create(
771 text='test',
792 text='test',
772 repo=pull_request.target_repo.scm_instance().name,
793 repo=pull_request.target_repo.scm_instance().name,
773 user=user_admin,
794 user=user_admin,
774 pull_request=pull_request,
795 pull_request=pull_request,
775 )
796 )
776 response = self.app.post(
797 response = self.app.post(
777 route_path(
798 route_path(
778 'pullrequest_comment_edit',
799 'pullrequest_comment_edit',
779 repo_name=pull_request.target_repo.scm_instance().name,
800 repo_name=pull_request.target_repo.scm_instance().name,
780 pull_request_id=pull_request.pull_request_id,
801 pull_request_id=pull_request.pull_request_id,
781 comment_id=comment.comment_id,
802 comment_id=comment.comment_id,
782 ),
803 ),
783 extra_environ=xhr_header,
804 extra_environ=xhr_header,
784 params={
805 params={
785 'csrf_token': csrf_token,
806 'csrf_token': csrf_token,
786 'text': 'test_text',
807 'text': 'test_text',
787 },
808 },
788 status=403,
809 status=403,
789 )
810 )
790 assert response.status_int == 403
811 assert response.status_int == 403
791
812
792 def test_create_pull_request(self, backend, csrf_token):
813 def test_create_pull_request(self, backend, csrf_token):
793 commits = [
814 commits = [
794 {'message': 'ancestor'},
815 {'message': 'ancestor'},
795 {'message': 'change'},
816 {'message': 'change'},
796 {'message': 'change2'},
817 {'message': 'change2'},
797 ]
818 ]
798 commit_ids = backend.create_master_repo(commits)
819 commit_ids = backend.create_master_repo(commits)
799 target = backend.create_repo(heads=['ancestor'])
820 target = backend.create_repo(heads=['ancestor'])
800 source = backend.create_repo(heads=['change2'])
821 source = backend.create_repo(heads=['change2'])
801
822
802 response = self.app.post(
823 response = self.app.post(
803 route_path('pullrequest_create', repo_name=source.repo_name),
824 route_path('pullrequest_create', repo_name=source.repo_name),
804 [
825 [
805 ('source_repo', source.repo_name),
826 ('source_repo', source.repo_name),
806 ('source_ref', 'branch:default:' + commit_ids['change2']),
827 ('source_ref', 'branch:default:' + commit_ids['change2']),
807 ('target_repo', target.repo_name),
828 ('target_repo', target.repo_name),
808 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
829 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
809 ('common_ancestor', commit_ids['ancestor']),
830 ('common_ancestor', commit_ids['ancestor']),
810 ('pullrequest_title', 'Title'),
831 ('pullrequest_title', 'Title'),
811 ('pullrequest_desc', 'Description'),
832 ('pullrequest_desc', 'Description'),
812 ('description_renderer', 'markdown'),
833 ('description_renderer', 'markdown'),
813 ('__start__', 'review_members:sequence'),
834 ('__start__', 'review_members:sequence'),
814 ('__start__', 'reviewer:mapping'),
835 ('__start__', 'reviewer:mapping'),
815 ('user_id', '1'),
836 ('user_id', '1'),
816 ('__start__', 'reasons:sequence'),
837 ('__start__', 'reasons:sequence'),
817 ('reason', 'Some reason'),
838 ('reason', 'Some reason'),
818 ('__end__', 'reasons:sequence'),
839 ('__end__', 'reasons:sequence'),
819 ('__start__', 'rules:sequence'),
840 ('__start__', 'rules:sequence'),
820 ('__end__', 'rules:sequence'),
841 ('__end__', 'rules:sequence'),
821 ('mandatory', 'False'),
842 ('mandatory', 'False'),
822 ('__end__', 'reviewer:mapping'),
843 ('__end__', 'reviewer:mapping'),
823 ('__end__', 'review_members:sequence'),
844 ('__end__', 'review_members:sequence'),
824 ('__start__', 'revisions:sequence'),
845 ('__start__', 'revisions:sequence'),
825 ('revisions', commit_ids['change']),
846 ('revisions', commit_ids['change']),
826 ('revisions', commit_ids['change2']),
847 ('revisions', commit_ids['change2']),
827 ('__end__', 'revisions:sequence'),
848 ('__end__', 'revisions:sequence'),
828 ('user', ''),
849 ('user', ''),
829 ('csrf_token', csrf_token),
850 ('csrf_token', csrf_token),
830 ],
851 ],
831 status=302)
852 status=302)
832
853
833 location = response.headers['Location']
854 location = response.headers['Location']
834 pull_request_id = location.rsplit('/', 1)[1]
855 pull_request_id = location.rsplit('/', 1)[1]
835 assert pull_request_id != 'new'
856 assert pull_request_id != 'new'
836 pull_request = PullRequest.get(int(pull_request_id))
857 pull_request = PullRequest.get(int(pull_request_id))
837
858
838 # check that we have now both revisions
859 # check that we have now both revisions
839 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
860 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
840 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
861 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
841 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
862 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
842 assert pull_request.target_ref == expected_target_ref
863 assert pull_request.target_ref == expected_target_ref
843
864
844 def test_reviewer_notifications(self, backend, csrf_token):
865 def test_reviewer_notifications(self, backend, csrf_token):
845 # We have to use the app.post for this test so it will create the
866 # We have to use the app.post for this test so it will create the
846 # notifications properly with the new PR
867 # notifications properly with the new PR
847 commits = [
868 commits = [
848 {'message': 'ancestor',
869 {'message': 'ancestor',
849 'added': [FileNode('file_A', content='content_of_ancestor')]},
870 'added': [FileNode('file_A', content='content_of_ancestor')]},
850 {'message': 'change',
871 {'message': 'change',
851 'added': [FileNode('file_a', content='content_of_change')]},
872 'added': [FileNode('file_a', content='content_of_change')]},
852 {'message': 'change-child'},
873 {'message': 'change-child'},
853 {'message': 'ancestor-child', 'parents': ['ancestor'],
874 {'message': 'ancestor-child', 'parents': ['ancestor'],
854 'added': [
875 'added': [
855 FileNode('file_B', content='content_of_ancestor_child')]},
876 FileNode('file_B', content='content_of_ancestor_child')]},
856 {'message': 'ancestor-child-2'},
877 {'message': 'ancestor-child-2'},
857 ]
878 ]
858 commit_ids = backend.create_master_repo(commits)
879 commit_ids = backend.create_master_repo(commits)
859 target = backend.create_repo(heads=['ancestor-child'])
880 target = backend.create_repo(heads=['ancestor-child'])
860 source = backend.create_repo(heads=['change'])
881 source = backend.create_repo(heads=['change'])
861
882
862 response = self.app.post(
883 response = self.app.post(
863 route_path('pullrequest_create', repo_name=source.repo_name),
884 route_path('pullrequest_create', repo_name=source.repo_name),
864 [
885 [
865 ('source_repo', source.repo_name),
886 ('source_repo', source.repo_name),
866 ('source_ref', 'branch:default:' + commit_ids['change']),
887 ('source_ref', 'branch:default:' + commit_ids['change']),
867 ('target_repo', target.repo_name),
888 ('target_repo', target.repo_name),
868 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
889 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
869 ('common_ancestor', commit_ids['ancestor']),
890 ('common_ancestor', commit_ids['ancestor']),
870 ('pullrequest_title', 'Title'),
891 ('pullrequest_title', 'Title'),
871 ('pullrequest_desc', 'Description'),
892 ('pullrequest_desc', 'Description'),
872 ('description_renderer', 'markdown'),
893 ('description_renderer', 'markdown'),
873 ('__start__', 'review_members:sequence'),
894 ('__start__', 'review_members:sequence'),
874 ('__start__', 'reviewer:mapping'),
895 ('__start__', 'reviewer:mapping'),
875 ('user_id', '2'),
896 ('user_id', '2'),
876 ('__start__', 'reasons:sequence'),
897 ('__start__', 'reasons:sequence'),
877 ('reason', 'Some reason'),
898 ('reason', 'Some reason'),
878 ('__end__', 'reasons:sequence'),
899 ('__end__', 'reasons:sequence'),
879 ('__start__', 'rules:sequence'),
900 ('__start__', 'rules:sequence'),
880 ('__end__', 'rules:sequence'),
901 ('__end__', 'rules:sequence'),
881 ('mandatory', 'False'),
902 ('mandatory', 'False'),
882 ('__end__', 'reviewer:mapping'),
903 ('__end__', 'reviewer:mapping'),
883 ('__end__', 'review_members:sequence'),
904 ('__end__', 'review_members:sequence'),
884 ('__start__', 'revisions:sequence'),
905 ('__start__', 'revisions:sequence'),
885 ('revisions', commit_ids['change']),
906 ('revisions', commit_ids['change']),
886 ('__end__', 'revisions:sequence'),
907 ('__end__', 'revisions:sequence'),
887 ('user', ''),
908 ('user', ''),
888 ('csrf_token', csrf_token),
909 ('csrf_token', csrf_token),
889 ],
910 ],
890 status=302)
911 status=302)
891
912
892 location = response.headers['Location']
913 location = response.headers['Location']
893
914
894 pull_request_id = location.rsplit('/', 1)[1]
915 pull_request_id = location.rsplit('/', 1)[1]
895 assert pull_request_id != 'new'
916 assert pull_request_id != 'new'
896 pull_request = PullRequest.get(int(pull_request_id))
917 pull_request = PullRequest.get(int(pull_request_id))
897
918
898 # Check that a notification was made
919 # Check that a notification was made
899 notifications = Notification.query()\
920 notifications = Notification.query()\
900 .filter(Notification.created_by == pull_request.author.user_id,
921 .filter(Notification.created_by == pull_request.author.user_id,
901 Notification.type_ == Notification.TYPE_PULL_REQUEST,
922 Notification.type_ == Notification.TYPE_PULL_REQUEST,
902 Notification.subject.contains(
923 Notification.subject.contains(
903 "requested a pull request review. !%s" % pull_request_id))
924 "requested a pull request review. !%s" % pull_request_id))
904 assert len(notifications.all()) == 1
925 assert len(notifications.all()) == 1
905
926
906 # Change reviewers and check that a notification was made
927 # Change reviewers and check that a notification was made
907 PullRequestModel().update_reviewers(
928 PullRequestModel().update_reviewers(
908 pull_request.pull_request_id, [
929 pull_request.pull_request_id, [
909 (1, [], False, 'reviewer', [])
930 (1, [], False, 'reviewer', [])
910 ],
931 ],
911 pull_request.author)
932 pull_request.author)
912 assert len(notifications.all()) == 2
933 assert len(notifications.all()) == 2
913
934
914 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
935 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
915 commits = [
936 commits = [
916 {'message': 'ancestor',
937 {'message': 'ancestor',
917 'added': [FileNode('file_A', content='content_of_ancestor')]},
938 'added': [FileNode('file_A', content='content_of_ancestor')]},
918 {'message': 'change',
939 {'message': 'change',
919 'added': [FileNode('file_a', content='content_of_change')]},
940 'added': [FileNode('file_a', content='content_of_change')]},
920 {'message': 'change-child'},
941 {'message': 'change-child'},
921 {'message': 'ancestor-child', 'parents': ['ancestor'],
942 {'message': 'ancestor-child', 'parents': ['ancestor'],
922 'added': [
943 'added': [
923 FileNode('file_B', content='content_of_ancestor_child')]},
944 FileNode('file_B', content='content_of_ancestor_child')]},
924 {'message': 'ancestor-child-2'},
945 {'message': 'ancestor-child-2'},
925 ]
946 ]
926 commit_ids = backend.create_master_repo(commits)
947 commit_ids = backend.create_master_repo(commits)
927 target = backend.create_repo(heads=['ancestor-child'])
948 target = backend.create_repo(heads=['ancestor-child'])
928 source = backend.create_repo(heads=['change'])
949 source = backend.create_repo(heads=['change'])
929
950
930 response = self.app.post(
951 response = self.app.post(
931 route_path('pullrequest_create', repo_name=source.repo_name),
952 route_path('pullrequest_create', repo_name=source.repo_name),
932 [
953 [
933 ('source_repo', source.repo_name),
954 ('source_repo', source.repo_name),
934 ('source_ref', 'branch:default:' + commit_ids['change']),
955 ('source_ref', 'branch:default:' + commit_ids['change']),
935 ('target_repo', target.repo_name),
956 ('target_repo', target.repo_name),
936 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
957 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
937 ('common_ancestor', commit_ids['ancestor']),
958 ('common_ancestor', commit_ids['ancestor']),
938 ('pullrequest_title', 'Title'),
959 ('pullrequest_title', 'Title'),
939 ('pullrequest_desc', 'Description'),
960 ('pullrequest_desc', 'Description'),
940 ('description_renderer', 'markdown'),
961 ('description_renderer', 'markdown'),
941 ('__start__', 'review_members:sequence'),
962 ('__start__', 'review_members:sequence'),
942 ('__start__', 'reviewer:mapping'),
963 ('__start__', 'reviewer:mapping'),
943 ('user_id', '1'),
964 ('user_id', '1'),
944 ('__start__', 'reasons:sequence'),
965 ('__start__', 'reasons:sequence'),
945 ('reason', 'Some reason'),
966 ('reason', 'Some reason'),
946 ('__end__', 'reasons:sequence'),
967 ('__end__', 'reasons:sequence'),
947 ('__start__', 'rules:sequence'),
968 ('__start__', 'rules:sequence'),
948 ('__end__', 'rules:sequence'),
969 ('__end__', 'rules:sequence'),
949 ('mandatory', 'False'),
970 ('mandatory', 'False'),
950 ('__end__', 'reviewer:mapping'),
971 ('__end__', 'reviewer:mapping'),
951 ('__end__', 'review_members:sequence'),
972 ('__end__', 'review_members:sequence'),
952 ('__start__', 'revisions:sequence'),
973 ('__start__', 'revisions:sequence'),
953 ('revisions', commit_ids['change']),
974 ('revisions', commit_ids['change']),
954 ('__end__', 'revisions:sequence'),
975 ('__end__', 'revisions:sequence'),
955 ('user', ''),
976 ('user', ''),
956 ('csrf_token', csrf_token),
977 ('csrf_token', csrf_token),
957 ],
978 ],
958 status=302)
979 status=302)
959
980
960 location = response.headers['Location']
981 location = response.headers['Location']
961
982
962 pull_request_id = location.rsplit('/', 1)[1]
983 pull_request_id = location.rsplit('/', 1)[1]
963 assert pull_request_id != 'new'
984 assert pull_request_id != 'new'
964 pull_request = PullRequest.get(int(pull_request_id))
985 pull_request = PullRequest.get(int(pull_request_id))
965
986
966 # target_ref has to point to the ancestor's commit_id in order to
987 # target_ref has to point to the ancestor's commit_id in order to
967 # show the correct diff
988 # show the correct diff
968 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
989 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
969 assert pull_request.target_ref == expected_target_ref
990 assert pull_request.target_ref == expected_target_ref
970
991
971 # Check generated diff contents
992 # Check generated diff contents
972 response = response.follow()
993 response = response.follow()
973 response.mustcontain(no=['content_of_ancestor'])
994 response.mustcontain(no=['content_of_ancestor'])
974 response.mustcontain(no=['content_of_ancestor-child'])
995 response.mustcontain(no=['content_of_ancestor-child'])
975 response.mustcontain('content_of_change')
996 response.mustcontain('content_of_change')
976
997
977 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
998 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
978 # Clear any previous calls to rcextensions
999 # Clear any previous calls to rcextensions
979 rhodecode.EXTENSIONS.calls.clear()
1000 rhodecode.EXTENSIONS.calls.clear()
980
1001
981 pull_request = pr_util.create_pull_request(
1002 pull_request = pr_util.create_pull_request(
982 approved=True, mergeable=True)
1003 approved=True, mergeable=True)
983 pull_request_id = pull_request.pull_request_id
1004 pull_request_id = pull_request.pull_request_id
984 repo_name = pull_request.target_repo.scm_instance().name,
1005 repo_name = pull_request.target_repo.scm_instance().name,
985
1006
986 url = route_path('pullrequest_merge',
1007 url = route_path('pullrequest_merge',
987 repo_name=str(repo_name[0]),
1008 repo_name=str(repo_name[0]),
988 pull_request_id=pull_request_id)
1009 pull_request_id=pull_request_id)
989 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1010 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
990
1011
991 pull_request = PullRequest.get(pull_request_id)
1012 pull_request = PullRequest.get(pull_request_id)
992
1013
993 assert response.status_int == 200
1014 assert response.status_int == 200
994 assert pull_request.is_closed()
1015 assert pull_request.is_closed()
995 assert_pull_request_status(
1016 assert_pull_request_status(
996 pull_request, ChangesetStatus.STATUS_APPROVED)
1017 pull_request, ChangesetStatus.STATUS_APPROVED)
997
1018
998 # Check the relevant log entries were added
1019 # Check the relevant log entries were added
999 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1020 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1000 actions = [log.action for log in user_logs]
1021 actions = [log.action for log in user_logs]
1001 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1022 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1002 expected_actions = [
1023 expected_actions = [
1003 u'repo.pull_request.close',
1024 u'repo.pull_request.close',
1004 u'repo.pull_request.merge',
1025 u'repo.pull_request.merge',
1005 u'repo.pull_request.comment.create'
1026 u'repo.pull_request.comment.create'
1006 ]
1027 ]
1007 assert actions == expected_actions
1028 assert actions == expected_actions
1008
1029
1009 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1030 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1010 actions = [log for log in user_logs]
1031 actions = [log for log in user_logs]
1011 assert actions[-1].action == 'user.push'
1032 assert actions[-1].action == 'user.push'
1012 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1033 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1013
1034
1014 # Check post_push rcextension was really executed
1035 # Check post_push rcextension was really executed
1015 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1036 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1016 assert len(push_calls) == 1
1037 assert len(push_calls) == 1
1017 unused_last_call_args, last_call_kwargs = push_calls[0]
1038 unused_last_call_args, last_call_kwargs = push_calls[0]
1018 assert last_call_kwargs['action'] == 'push'
1039 assert last_call_kwargs['action'] == 'push'
1019 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1040 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1020
1041
1021 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1042 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1022 pull_request = pr_util.create_pull_request(mergeable=False)
1043 pull_request = pr_util.create_pull_request(mergeable=False)
1023 pull_request_id = pull_request.pull_request_id
1044 pull_request_id = pull_request.pull_request_id
1024 pull_request = PullRequest.get(pull_request_id)
1045 pull_request = PullRequest.get(pull_request_id)
1025
1046
1026 response = self.app.post(
1047 response = self.app.post(
1027 route_path('pullrequest_merge',
1048 route_path('pullrequest_merge',
1028 repo_name=pull_request.target_repo.scm_instance().name,
1049 repo_name=pull_request.target_repo.scm_instance().name,
1029 pull_request_id=pull_request.pull_request_id),
1050 pull_request_id=pull_request.pull_request_id),
1030 params={'csrf_token': csrf_token}).follow()
1051 params={'csrf_token': csrf_token}).follow()
1031
1052
1032 assert response.status_int == 200
1053 assert response.status_int == 200
1033 response.mustcontain(
1054 response.mustcontain(
1034 'Merge is not currently possible because of below failed checks.')
1055 'Merge is not currently possible because of below failed checks.')
1035 response.mustcontain('Server-side pull request merging is disabled.')
1056 response.mustcontain('Server-side pull request merging is disabled.')
1036
1057
1037 @pytest.mark.skip_backends('svn')
1058 @pytest.mark.skip_backends('svn')
1038 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1059 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1039 pull_request = pr_util.create_pull_request(mergeable=True)
1060 pull_request = pr_util.create_pull_request(mergeable=True)
1040 pull_request_id = pull_request.pull_request_id
1061 pull_request_id = pull_request.pull_request_id
1041 repo_name = pull_request.target_repo.scm_instance().name
1062 repo_name = pull_request.target_repo.scm_instance().name
1042
1063
1043 response = self.app.post(
1064 response = self.app.post(
1044 route_path('pullrequest_merge',
1065 route_path('pullrequest_merge',
1045 repo_name=repo_name, pull_request_id=pull_request_id),
1066 repo_name=repo_name, pull_request_id=pull_request_id),
1046 params={'csrf_token': csrf_token}).follow()
1067 params={'csrf_token': csrf_token}).follow()
1047
1068
1048 assert response.status_int == 200
1069 assert response.status_int == 200
1049
1070
1050 response.mustcontain(
1071 response.mustcontain(
1051 'Merge is not currently possible because of below failed checks.')
1072 'Merge is not currently possible because of below failed checks.')
1052 response.mustcontain('Pull request reviewer approval is pending.')
1073 response.mustcontain('Pull request reviewer approval is pending.')
1053
1074
1054 def test_merge_pull_request_renders_failure_reason(
1075 def test_merge_pull_request_renders_failure_reason(
1055 self, user_regular, csrf_token, pr_util):
1076 self, user_regular, csrf_token, pr_util):
1056 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1077 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1057 pull_request_id = pull_request.pull_request_id
1078 pull_request_id = pull_request.pull_request_id
1058 repo_name = pull_request.target_repo.scm_instance().name
1079 repo_name = pull_request.target_repo.scm_instance().name
1059
1080
1060 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1081 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1061 MergeFailureReason.PUSH_FAILED,
1082 MergeFailureReason.PUSH_FAILED,
1062 metadata={'target': 'shadow repo',
1083 metadata={'target': 'shadow repo',
1063 'merge_commit': 'xxx'})
1084 'merge_commit': 'xxx'})
1064 model_patcher = mock.patch.multiple(
1085 model_patcher = mock.patch.multiple(
1065 PullRequestModel,
1086 PullRequestModel,
1066 merge_repo=mock.Mock(return_value=merge_resp),
1087 merge_repo=mock.Mock(return_value=merge_resp),
1067 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1088 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1068
1089
1069 with model_patcher:
1090 with model_patcher:
1070 response = self.app.post(
1091 response = self.app.post(
1071 route_path('pullrequest_merge',
1092 route_path('pullrequest_merge',
1072 repo_name=repo_name,
1093 repo_name=repo_name,
1073 pull_request_id=pull_request_id),
1094 pull_request_id=pull_request_id),
1074 params={'csrf_token': csrf_token}, status=302)
1095 params={'csrf_token': csrf_token}, status=302)
1075
1096
1076 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1097 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1077 metadata={'target': 'shadow repo',
1098 metadata={'target': 'shadow repo',
1078 'merge_commit': 'xxx'})
1099 'merge_commit': 'xxx'})
1079 assert_session_flash(response, merge_resp.merge_status_message)
1100 assert_session_flash(response, merge_resp.merge_status_message)
1080
1101
1081 def test_update_source_revision(self, backend, csrf_token):
1102 def test_update_source_revision(self, backend, csrf_token):
1082 commits = [
1103 commits = [
1083 {'message': 'ancestor'},
1104 {'message': 'ancestor'},
1084 {'message': 'change'},
1105 {'message': 'change'},
1085 {'message': 'change-2'},
1106 {'message': 'change-2'},
1086 ]
1107 ]
1087 commit_ids = backend.create_master_repo(commits)
1108 commit_ids = backend.create_master_repo(commits)
1088 target = backend.create_repo(heads=['ancestor'])
1109 target = backend.create_repo(heads=['ancestor'])
1089 source = backend.create_repo(heads=['change'])
1110 source = backend.create_repo(heads=['change'])
1090
1111
1091 # create pr from a in source to A in target
1112 # create pr from a in source to A in target
1092 pull_request = PullRequest()
1113 pull_request = PullRequest()
1093
1114
1094 pull_request.source_repo = source
1115 pull_request.source_repo = source
1095 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1116 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1096 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1117 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1097
1118
1098 pull_request.target_repo = target
1119 pull_request.target_repo = target
1099 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1120 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1100 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1121 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1101
1122
1102 pull_request.revisions = [commit_ids['change']]
1123 pull_request.revisions = [commit_ids['change']]
1103 pull_request.title = u"Test"
1124 pull_request.title = u"Test"
1104 pull_request.description = u"Description"
1125 pull_request.description = u"Description"
1105 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1126 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1106 pull_request.pull_request_state = PullRequest.STATE_CREATED
1127 pull_request.pull_request_state = PullRequest.STATE_CREATED
1107 Session().add(pull_request)
1128 Session().add(pull_request)
1108 Session().commit()
1129 Session().commit()
1109 pull_request_id = pull_request.pull_request_id
1130 pull_request_id = pull_request.pull_request_id
1110
1131
1111 # source has ancestor - change - change-2
1132 # source has ancestor - change - change-2
1112 backend.pull_heads(source, heads=['change-2'])
1133 backend.pull_heads(source, heads=['change-2'])
1113 target_repo_name = target.repo_name
1134 target_repo_name = target.repo_name
1114
1135
1115 # update PR
1136 # update PR
1116 self.app.post(
1137 self.app.post(
1117 route_path('pullrequest_update',
1138 route_path('pullrequest_update',
1118 repo_name=target_repo_name, pull_request_id=pull_request_id),
1139 repo_name=target_repo_name, pull_request_id=pull_request_id),
1119 params={'update_commits': 'true', 'csrf_token': csrf_token})
1140 params={'update_commits': 'true', 'csrf_token': csrf_token})
1120
1141
1121 response = self.app.get(
1142 response = self.app.get(
1122 route_path('pullrequest_show',
1143 route_path('pullrequest_show',
1123 repo_name=target_repo_name,
1144 repo_name=target_repo_name,
1124 pull_request_id=pull_request.pull_request_id))
1145 pull_request_id=pull_request.pull_request_id))
1125
1146
1126 assert response.status_int == 200
1147 assert response.status_int == 200
1127 response.mustcontain('Pull request updated to')
1148 response.mustcontain('Pull request updated to')
1128 response.mustcontain('with 1 added, 0 removed commits.')
1149 response.mustcontain('with 1 added, 0 removed commits.')
1129
1150
1130 # check that we have now both revisions
1151 # check that we have now both revisions
1131 pull_request = PullRequest.get(pull_request_id)
1152 pull_request = PullRequest.get(pull_request_id)
1132 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1153 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1133
1154
1134 def test_update_target_revision(self, backend, csrf_token):
1155 def test_update_target_revision(self, backend, csrf_token):
1135 commits = [
1156 commits = [
1136 {'message': 'ancestor'},
1157 {'message': 'ancestor'},
1137 {'message': 'change'},
1158 {'message': 'change'},
1138 {'message': 'ancestor-new', 'parents': ['ancestor']},
1159 {'message': 'ancestor-new', 'parents': ['ancestor']},
1139 {'message': 'change-rebased'},
1160 {'message': 'change-rebased'},
1140 ]
1161 ]
1141 commit_ids = backend.create_master_repo(commits)
1162 commit_ids = backend.create_master_repo(commits)
1142 target = backend.create_repo(heads=['ancestor'])
1163 target = backend.create_repo(heads=['ancestor'])
1143 source = backend.create_repo(heads=['change'])
1164 source = backend.create_repo(heads=['change'])
1144
1165
1145 # create pr from a in source to A in target
1166 # create pr from a in source to A in target
1146 pull_request = PullRequest()
1167 pull_request = PullRequest()
1147
1168
1148 pull_request.source_repo = source
1169 pull_request.source_repo = source
1149 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1170 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1150 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1171 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1151
1172
1152 pull_request.target_repo = target
1173 pull_request.target_repo = target
1153 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1174 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1154 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1175 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1155
1176
1156 pull_request.revisions = [commit_ids['change']]
1177 pull_request.revisions = [commit_ids['change']]
1157 pull_request.title = u"Test"
1178 pull_request.title = u"Test"
1158 pull_request.description = u"Description"
1179 pull_request.description = u"Description"
1159 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1180 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1160 pull_request.pull_request_state = PullRequest.STATE_CREATED
1181 pull_request.pull_request_state = PullRequest.STATE_CREATED
1161
1182
1162 Session().add(pull_request)
1183 Session().add(pull_request)
1163 Session().commit()
1184 Session().commit()
1164 pull_request_id = pull_request.pull_request_id
1185 pull_request_id = pull_request.pull_request_id
1165
1186
1166 # target has ancestor - ancestor-new
1187 # target has ancestor - ancestor-new
1167 # source has ancestor - ancestor-new - change-rebased
1188 # source has ancestor - ancestor-new - change-rebased
1168 backend.pull_heads(target, heads=['ancestor-new'])
1189 backend.pull_heads(target, heads=['ancestor-new'])
1169 backend.pull_heads(source, heads=['change-rebased'])
1190 backend.pull_heads(source, heads=['change-rebased'])
1170 target_repo_name = target.repo_name
1191 target_repo_name = target.repo_name
1171
1192
1172 # update PR
1193 # update PR
1173 url = route_path('pullrequest_update',
1194 url = route_path('pullrequest_update',
1174 repo_name=target_repo_name,
1195 repo_name=target_repo_name,
1175 pull_request_id=pull_request_id)
1196 pull_request_id=pull_request_id)
1176 self.app.post(url,
1197 self.app.post(url,
1177 params={'update_commits': 'true', 'csrf_token': csrf_token},
1198 params={'update_commits': 'true', 'csrf_token': csrf_token},
1178 status=200)
1199 status=200)
1179
1200
1180 # check that we have now both revisions
1201 # check that we have now both revisions
1181 pull_request = PullRequest.get(pull_request_id)
1202 pull_request = PullRequest.get(pull_request_id)
1182 assert pull_request.revisions == [commit_ids['change-rebased']]
1203 assert pull_request.revisions == [commit_ids['change-rebased']]
1183 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1204 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1184 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1205 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1185
1206
1186 response = self.app.get(
1207 response = self.app.get(
1187 route_path('pullrequest_show',
1208 route_path('pullrequest_show',
1188 repo_name=target_repo_name,
1209 repo_name=target_repo_name,
1189 pull_request_id=pull_request.pull_request_id))
1210 pull_request_id=pull_request.pull_request_id))
1190 assert response.status_int == 200
1211 assert response.status_int == 200
1191 response.mustcontain('Pull request updated to')
1212 response.mustcontain('Pull request updated to')
1192 response.mustcontain('with 1 added, 1 removed commits.')
1213 response.mustcontain('with 1 added, 1 removed commits.')
1193
1214
1194 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1215 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1195 backend = backend_git
1216 backend = backend_git
1196 commits = [
1217 commits = [
1197 {'message': 'master-commit-1'},
1218 {'message': 'master-commit-1'},
1198 {'message': 'master-commit-2-change-1'},
1219 {'message': 'master-commit-2-change-1'},
1199 {'message': 'master-commit-3-change-2'},
1220 {'message': 'master-commit-3-change-2'},
1200
1221
1201 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1222 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1202 {'message': 'feat-commit-2'},
1223 {'message': 'feat-commit-2'},
1203 ]
1224 ]
1204 commit_ids = backend.create_master_repo(commits)
1225 commit_ids = backend.create_master_repo(commits)
1205 target = backend.create_repo(heads=['master-commit-3-change-2'])
1226 target = backend.create_repo(heads=['master-commit-3-change-2'])
1206 source = backend.create_repo(heads=['feat-commit-2'])
1227 source = backend.create_repo(heads=['feat-commit-2'])
1207
1228
1208 # create pr from a in source to A in target
1229 # create pr from a in source to A in target
1209 pull_request = PullRequest()
1230 pull_request = PullRequest()
1210 pull_request.source_repo = source
1231 pull_request.source_repo = source
1211
1232
1212 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1233 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1213 branch=backend.default_branch_name,
1234 branch=backend.default_branch_name,
1214 commit_id=commit_ids['master-commit-3-change-2'])
1235 commit_id=commit_ids['master-commit-3-change-2'])
1215
1236
1216 pull_request.target_repo = target
1237 pull_request.target_repo = target
1217 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1238 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1218 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1239 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1219
1240
1220 pull_request.revisions = [
1241 pull_request.revisions = [
1221 commit_ids['feat-commit-1'],
1242 commit_ids['feat-commit-1'],
1222 commit_ids['feat-commit-2']
1243 commit_ids['feat-commit-2']
1223 ]
1244 ]
1224 pull_request.title = u"Test"
1245 pull_request.title = u"Test"
1225 pull_request.description = u"Description"
1246 pull_request.description = u"Description"
1226 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1247 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1227 pull_request.pull_request_state = PullRequest.STATE_CREATED
1248 pull_request.pull_request_state = PullRequest.STATE_CREATED
1228 Session().add(pull_request)
1249 Session().add(pull_request)
1229 Session().commit()
1250 Session().commit()
1230 pull_request_id = pull_request.pull_request_id
1251 pull_request_id = pull_request.pull_request_id
1231
1252
1232 # PR is created, now we simulate a force-push into target,
1253 # PR is created, now we simulate a force-push into target,
1233 # that drops a 2 last commits
1254 # that drops a 2 last commits
1234 vcsrepo = target.scm_instance()
1255 vcsrepo = target.scm_instance()
1235 vcsrepo.config.clear_section('hooks')
1256 vcsrepo.config.clear_section('hooks')
1236 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1257 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1237 target_repo_name = target.repo_name
1258 target_repo_name = target.repo_name
1238
1259
1239 # update PR
1260 # update PR
1240 url = route_path('pullrequest_update',
1261 url = route_path('pullrequest_update',
1241 repo_name=target_repo_name,
1262 repo_name=target_repo_name,
1242 pull_request_id=pull_request_id)
1263 pull_request_id=pull_request_id)
1243 self.app.post(url,
1264 self.app.post(url,
1244 params={'update_commits': 'true', 'csrf_token': csrf_token},
1265 params={'update_commits': 'true', 'csrf_token': csrf_token},
1245 status=200)
1266 status=200)
1246
1267
1247 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1268 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1248 assert response.status_int == 200
1269 assert response.status_int == 200
1249 response.mustcontain('Pull request updated to')
1270 response.mustcontain('Pull request updated to')
1250 response.mustcontain('with 0 added, 0 removed commits.')
1271 response.mustcontain('with 0 added, 0 removed commits.')
1251
1272
1252 def test_update_of_ancestor_reference(self, backend, csrf_token):
1273 def test_update_of_ancestor_reference(self, backend, csrf_token):
1253 commits = [
1274 commits = [
1254 {'message': 'ancestor'},
1275 {'message': 'ancestor'},
1255 {'message': 'change'},
1276 {'message': 'change'},
1256 {'message': 'change-2'},
1277 {'message': 'change-2'},
1257 {'message': 'ancestor-new', 'parents': ['ancestor']},
1278 {'message': 'ancestor-new', 'parents': ['ancestor']},
1258 {'message': 'change-rebased'},
1279 {'message': 'change-rebased'},
1259 ]
1280 ]
1260 commit_ids = backend.create_master_repo(commits)
1281 commit_ids = backend.create_master_repo(commits)
1261 target = backend.create_repo(heads=['ancestor'])
1282 target = backend.create_repo(heads=['ancestor'])
1262 source = backend.create_repo(heads=['change'])
1283 source = backend.create_repo(heads=['change'])
1263
1284
1264 # create pr from a in source to A in target
1285 # create pr from a in source to A in target
1265 pull_request = PullRequest()
1286 pull_request = PullRequest()
1266 pull_request.source_repo = source
1287 pull_request.source_repo = source
1267
1288
1268 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1289 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1269 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1290 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1270 pull_request.target_repo = target
1291 pull_request.target_repo = target
1271 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1292 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1272 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1293 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1273 pull_request.revisions = [commit_ids['change']]
1294 pull_request.revisions = [commit_ids['change']]
1274 pull_request.title = u"Test"
1295 pull_request.title = u"Test"
1275 pull_request.description = u"Description"
1296 pull_request.description = u"Description"
1276 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1297 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1277 pull_request.pull_request_state = PullRequest.STATE_CREATED
1298 pull_request.pull_request_state = PullRequest.STATE_CREATED
1278 Session().add(pull_request)
1299 Session().add(pull_request)
1279 Session().commit()
1300 Session().commit()
1280 pull_request_id = pull_request.pull_request_id
1301 pull_request_id = pull_request.pull_request_id
1281
1302
1282 # target has ancestor - ancestor-new
1303 # target has ancestor - ancestor-new
1283 # source has ancestor - ancestor-new - change-rebased
1304 # source has ancestor - ancestor-new - change-rebased
1284 backend.pull_heads(target, heads=['ancestor-new'])
1305 backend.pull_heads(target, heads=['ancestor-new'])
1285 backend.pull_heads(source, heads=['change-rebased'])
1306 backend.pull_heads(source, heads=['change-rebased'])
1286 target_repo_name = target.repo_name
1307 target_repo_name = target.repo_name
1287
1308
1288 # update PR
1309 # update PR
1289 self.app.post(
1310 self.app.post(
1290 route_path('pullrequest_update',
1311 route_path('pullrequest_update',
1291 repo_name=target_repo_name, pull_request_id=pull_request_id),
1312 repo_name=target_repo_name, pull_request_id=pull_request_id),
1292 params={'update_commits': 'true', 'csrf_token': csrf_token},
1313 params={'update_commits': 'true', 'csrf_token': csrf_token},
1293 status=200)
1314 status=200)
1294
1315
1295 # Expect the target reference to be updated correctly
1316 # Expect the target reference to be updated correctly
1296 pull_request = PullRequest.get(pull_request_id)
1317 pull_request = PullRequest.get(pull_request_id)
1297 assert pull_request.revisions == [commit_ids['change-rebased']]
1318 assert pull_request.revisions == [commit_ids['change-rebased']]
1298 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1319 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1299 branch=backend.default_branch_name,
1320 branch=backend.default_branch_name,
1300 commit_id=commit_ids['ancestor-new'])
1321 commit_id=commit_ids['ancestor-new'])
1301 assert pull_request.target_ref == expected_target_ref
1322 assert pull_request.target_ref == expected_target_ref
1302
1323
1303 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1324 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1304 branch_name = 'development'
1325 branch_name = 'development'
1305 commits = [
1326 commits = [
1306 {'message': 'initial-commit'},
1327 {'message': 'initial-commit'},
1307 {'message': 'old-feature'},
1328 {'message': 'old-feature'},
1308 {'message': 'new-feature', 'branch': branch_name},
1329 {'message': 'new-feature', 'branch': branch_name},
1309 ]
1330 ]
1310 repo = backend_git.create_repo(commits)
1331 repo = backend_git.create_repo(commits)
1311 repo_name = repo.repo_name
1332 repo_name = repo.repo_name
1312 commit_ids = backend_git.commit_ids
1333 commit_ids = backend_git.commit_ids
1313
1334
1314 pull_request = PullRequest()
1335 pull_request = PullRequest()
1315 pull_request.source_repo = repo
1336 pull_request.source_repo = repo
1316 pull_request.target_repo = repo
1337 pull_request.target_repo = repo
1317 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1338 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1318 branch=branch_name, commit_id=commit_ids['new-feature'])
1339 branch=branch_name, commit_id=commit_ids['new-feature'])
1319 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1340 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1320 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1341 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1321 pull_request.revisions = [commit_ids['new-feature']]
1342 pull_request.revisions = [commit_ids['new-feature']]
1322 pull_request.title = u"Test"
1343 pull_request.title = u"Test"
1323 pull_request.description = u"Description"
1344 pull_request.description = u"Description"
1324 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1345 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1325 pull_request.pull_request_state = PullRequest.STATE_CREATED
1346 pull_request.pull_request_state = PullRequest.STATE_CREATED
1326 Session().add(pull_request)
1347 Session().add(pull_request)
1327 Session().commit()
1348 Session().commit()
1328
1349
1329 pull_request_id = pull_request.pull_request_id
1350 pull_request_id = pull_request.pull_request_id
1330
1351
1331 vcs = repo.scm_instance()
1352 vcs = repo.scm_instance()
1332 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1353 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1333 # NOTE(marcink): run GC to ensure the commits are gone
1354 # NOTE(marcink): run GC to ensure the commits are gone
1334 vcs.run_gc()
1355 vcs.run_gc()
1335
1356
1336 response = self.app.get(route_path(
1357 response = self.app.get(route_path(
1337 'pullrequest_show',
1358 'pullrequest_show',
1338 repo_name=repo_name,
1359 repo_name=repo_name,
1339 pull_request_id=pull_request_id))
1360 pull_request_id=pull_request_id))
1340
1361
1341 assert response.status_int == 200
1362 assert response.status_int == 200
1342
1363
1343 response.assert_response().element_contains(
1364 response.assert_response().element_contains(
1344 '#changeset_compare_view_content .alert strong',
1365 '#changeset_compare_view_content .alert strong',
1345 'Missing commits')
1366 'Missing commits')
1346 response.assert_response().element_contains(
1367 response.assert_response().element_contains(
1347 '#changeset_compare_view_content .alert',
1368 '#changeset_compare_view_content .alert',
1348 'This pull request cannot be displayed, because one or more'
1369 'This pull request cannot be displayed, because one or more'
1349 ' commits no longer exist in the source repository.')
1370 ' commits no longer exist in the source repository.')
1350
1371
1351 def test_strip_commits_from_pull_request(
1372 def test_strip_commits_from_pull_request(
1352 self, backend, pr_util, csrf_token):
1373 self, backend, pr_util, csrf_token):
1353 commits = [
1374 commits = [
1354 {'message': 'initial-commit'},
1375 {'message': 'initial-commit'},
1355 {'message': 'old-feature'},
1376 {'message': 'old-feature'},
1356 {'message': 'new-feature', 'parents': ['initial-commit']},
1377 {'message': 'new-feature', 'parents': ['initial-commit']},
1357 ]
1378 ]
1358 pull_request = pr_util.create_pull_request(
1379 pull_request = pr_util.create_pull_request(
1359 commits, target_head='initial-commit', source_head='new-feature',
1380 commits, target_head='initial-commit', source_head='new-feature',
1360 revisions=['new-feature'])
1381 revisions=['new-feature'])
1361
1382
1362 vcs = pr_util.source_repository.scm_instance()
1383 vcs = pr_util.source_repository.scm_instance()
1363 if backend.alias == 'git':
1384 if backend.alias == 'git':
1364 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1385 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1365 else:
1386 else:
1366 vcs.strip(pr_util.commit_ids['new-feature'])
1387 vcs.strip(pr_util.commit_ids['new-feature'])
1367
1388
1368 response = self.app.get(route_path(
1389 response = self.app.get(route_path(
1369 'pullrequest_show',
1390 'pullrequest_show',
1370 repo_name=pr_util.target_repository.repo_name,
1391 repo_name=pr_util.target_repository.repo_name,
1371 pull_request_id=pull_request.pull_request_id))
1392 pull_request_id=pull_request.pull_request_id))
1372
1393
1373 assert response.status_int == 200
1394 assert response.status_int == 200
1374
1395
1375 response.assert_response().element_contains(
1396 response.assert_response().element_contains(
1376 '#changeset_compare_view_content .alert strong',
1397 '#changeset_compare_view_content .alert strong',
1377 'Missing commits')
1398 'Missing commits')
1378 response.assert_response().element_contains(
1399 response.assert_response().element_contains(
1379 '#changeset_compare_view_content .alert',
1400 '#changeset_compare_view_content .alert',
1380 'This pull request cannot be displayed, because one or more'
1401 'This pull request cannot be displayed, because one or more'
1381 ' commits no longer exist in the source repository.')
1402 ' commits no longer exist in the source repository.')
1382 response.assert_response().element_contains(
1403 response.assert_response().element_contains(
1383 '#update_commits',
1404 '#update_commits',
1384 'Update commits')
1405 'Update commits')
1385
1406
1386 def test_strip_commits_and_update(
1407 def test_strip_commits_and_update(
1387 self, backend, pr_util, csrf_token):
1408 self, backend, pr_util, csrf_token):
1388 commits = [
1409 commits = [
1389 {'message': 'initial-commit'},
1410 {'message': 'initial-commit'},
1390 {'message': 'old-feature'},
1411 {'message': 'old-feature'},
1391 {'message': 'new-feature', 'parents': ['old-feature']},
1412 {'message': 'new-feature', 'parents': ['old-feature']},
1392 ]
1413 ]
1393 pull_request = pr_util.create_pull_request(
1414 pull_request = pr_util.create_pull_request(
1394 commits, target_head='old-feature', source_head='new-feature',
1415 commits, target_head='old-feature', source_head='new-feature',
1395 revisions=['new-feature'], mergeable=True)
1416 revisions=['new-feature'], mergeable=True)
1396 pr_id = pull_request.pull_request_id
1417 pr_id = pull_request.pull_request_id
1397 target_repo_name = pull_request.target_repo.repo_name
1418 target_repo_name = pull_request.target_repo.repo_name
1398
1419
1399 vcs = pr_util.source_repository.scm_instance()
1420 vcs = pr_util.source_repository.scm_instance()
1400 if backend.alias == 'git':
1421 if backend.alias == 'git':
1401 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1422 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1402 else:
1423 else:
1403 vcs.strip(pr_util.commit_ids['new-feature'])
1424 vcs.strip(pr_util.commit_ids['new-feature'])
1404
1425
1405 url = route_path('pullrequest_update',
1426 url = route_path('pullrequest_update',
1406 repo_name=target_repo_name,
1427 repo_name=target_repo_name,
1407 pull_request_id=pr_id)
1428 pull_request_id=pr_id)
1408 response = self.app.post(url,
1429 response = self.app.post(url,
1409 params={'update_commits': 'true',
1430 params={'update_commits': 'true',
1410 'csrf_token': csrf_token})
1431 'csrf_token': csrf_token})
1411
1432
1412 assert response.status_int == 200
1433 assert response.status_int == 200
1413 assert response.body == '{"response": true, "redirect_url": null}'
1434 assert response.body == '{"response": true, "redirect_url": null}'
1414
1435
1415 # Make sure that after update, it won't raise 500 errors
1436 # Make sure that after update, it won't raise 500 errors
1416 response = self.app.get(route_path(
1437 response = self.app.get(route_path(
1417 'pullrequest_show',
1438 'pullrequest_show',
1418 repo_name=target_repo_name,
1439 repo_name=target_repo_name,
1419 pull_request_id=pr_id))
1440 pull_request_id=pr_id))
1420
1441
1421 assert response.status_int == 200
1442 assert response.status_int == 200
1422 response.assert_response().element_contains(
1443 response.assert_response().element_contains(
1423 '#changeset_compare_view_content .alert strong',
1444 '#changeset_compare_view_content .alert strong',
1424 'Missing commits')
1445 'Missing commits')
1425
1446
1426 def test_branch_is_a_link(self, pr_util):
1447 def test_branch_is_a_link(self, pr_util):
1427 pull_request = pr_util.create_pull_request()
1448 pull_request = pr_util.create_pull_request()
1428 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1449 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1429 pull_request.target_ref = 'branch:target:abcdef1234567890'
1450 pull_request.target_ref = 'branch:target:abcdef1234567890'
1430 Session().add(pull_request)
1451 Session().add(pull_request)
1431 Session().commit()
1452 Session().commit()
1432
1453
1433 response = self.app.get(route_path(
1454 response = self.app.get(route_path(
1434 'pullrequest_show',
1455 'pullrequest_show',
1435 repo_name=pull_request.target_repo.scm_instance().name,
1456 repo_name=pull_request.target_repo.scm_instance().name,
1436 pull_request_id=pull_request.pull_request_id))
1457 pull_request_id=pull_request.pull_request_id))
1437 assert response.status_int == 200
1458 assert response.status_int == 200
1438
1459
1439 source = response.assert_response().get_element('.pr-source-info')
1460 source = response.assert_response().get_element('.pr-source-info')
1440 source_parent = source.getparent()
1461 source_parent = source.getparent()
1441 assert len(source_parent) == 1
1462 assert len(source_parent) == 1
1442
1463
1443 target = response.assert_response().get_element('.pr-target-info')
1464 target = response.assert_response().get_element('.pr-target-info')
1444 target_parent = target.getparent()
1465 target_parent = target.getparent()
1445 assert len(target_parent) == 1
1466 assert len(target_parent) == 1
1446
1467
1447 expected_origin_link = route_path(
1468 expected_origin_link = route_path(
1448 'repo_commits',
1469 'repo_commits',
1449 repo_name=pull_request.source_repo.scm_instance().name,
1470 repo_name=pull_request.source_repo.scm_instance().name,
1450 params=dict(branch='origin'))
1471 params=dict(branch='origin'))
1451 expected_target_link = route_path(
1472 expected_target_link = route_path(
1452 'repo_commits',
1473 'repo_commits',
1453 repo_name=pull_request.target_repo.scm_instance().name,
1474 repo_name=pull_request.target_repo.scm_instance().name,
1454 params=dict(branch='target'))
1475 params=dict(branch='target'))
1455 assert source_parent.attrib['href'] == expected_origin_link
1476 assert source_parent.attrib['href'] == expected_origin_link
1456 assert target_parent.attrib['href'] == expected_target_link
1477 assert target_parent.attrib['href'] == expected_target_link
1457
1478
1458 def test_bookmark_is_not_a_link(self, pr_util):
1479 def test_bookmark_is_not_a_link(self, pr_util):
1459 pull_request = pr_util.create_pull_request()
1480 pull_request = pr_util.create_pull_request()
1460 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1481 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1461 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1482 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1462 Session().add(pull_request)
1483 Session().add(pull_request)
1463 Session().commit()
1484 Session().commit()
1464
1485
1465 response = self.app.get(route_path(
1486 response = self.app.get(route_path(
1466 'pullrequest_show',
1487 'pullrequest_show',
1467 repo_name=pull_request.target_repo.scm_instance().name,
1488 repo_name=pull_request.target_repo.scm_instance().name,
1468 pull_request_id=pull_request.pull_request_id))
1489 pull_request_id=pull_request.pull_request_id))
1469 assert response.status_int == 200
1490 assert response.status_int == 200
1470
1491
1471 source = response.assert_response().get_element('.pr-source-info')
1492 source = response.assert_response().get_element('.pr-source-info')
1472 assert source.text.strip() == 'bookmark:origin'
1493 assert source.text.strip() == 'bookmark:origin'
1473 assert source.getparent().attrib.get('href') is None
1494 assert source.getparent().attrib.get('href') is None
1474
1495
1475 target = response.assert_response().get_element('.pr-target-info')
1496 target = response.assert_response().get_element('.pr-target-info')
1476 assert target.text.strip() == 'bookmark:target'
1497 assert target.text.strip() == 'bookmark:target'
1477 assert target.getparent().attrib.get('href') is None
1498 assert target.getparent().attrib.get('href') is None
1478
1499
1479 def test_tag_is_not_a_link(self, pr_util):
1500 def test_tag_is_not_a_link(self, pr_util):
1480 pull_request = pr_util.create_pull_request()
1501 pull_request = pr_util.create_pull_request()
1481 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1502 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1482 pull_request.target_ref = 'tag:target:abcdef1234567890'
1503 pull_request.target_ref = 'tag:target:abcdef1234567890'
1483 Session().add(pull_request)
1504 Session().add(pull_request)
1484 Session().commit()
1505 Session().commit()
1485
1506
1486 response = self.app.get(route_path(
1507 response = self.app.get(route_path(
1487 'pullrequest_show',
1508 'pullrequest_show',
1488 repo_name=pull_request.target_repo.scm_instance().name,
1509 repo_name=pull_request.target_repo.scm_instance().name,
1489 pull_request_id=pull_request.pull_request_id))
1510 pull_request_id=pull_request.pull_request_id))
1490 assert response.status_int == 200
1511 assert response.status_int == 200
1491
1512
1492 source = response.assert_response().get_element('.pr-source-info')
1513 source = response.assert_response().get_element('.pr-source-info')
1493 assert source.text.strip() == 'tag:origin'
1514 assert source.text.strip() == 'tag:origin'
1494 assert source.getparent().attrib.get('href') is None
1515 assert source.getparent().attrib.get('href') is None
1495
1516
1496 target = response.assert_response().get_element('.pr-target-info')
1517 target = response.assert_response().get_element('.pr-target-info')
1497 assert target.text.strip() == 'tag:target'
1518 assert target.text.strip() == 'tag:target'
1498 assert target.getparent().attrib.get('href') is None
1519 assert target.getparent().attrib.get('href') is None
1499
1520
1500 @pytest.mark.parametrize('mergeable', [True, False])
1521 @pytest.mark.parametrize('mergeable', [True, False])
1501 def test_shadow_repository_link(
1522 def test_shadow_repository_link(
1502 self, mergeable, pr_util, http_host_only_stub):
1523 self, mergeable, pr_util, http_host_only_stub):
1503 """
1524 """
1504 Check that the pull request summary page displays a link to the shadow
1525 Check that the pull request summary page displays a link to the shadow
1505 repository if the pull request is mergeable. If it is not mergeable
1526 repository if the pull request is mergeable. If it is not mergeable
1506 the link should not be displayed.
1527 the link should not be displayed.
1507 """
1528 """
1508 pull_request = pr_util.create_pull_request(
1529 pull_request = pr_util.create_pull_request(
1509 mergeable=mergeable, enable_notifications=False)
1530 mergeable=mergeable, enable_notifications=False)
1510 target_repo = pull_request.target_repo.scm_instance()
1531 target_repo = pull_request.target_repo.scm_instance()
1511 pr_id = pull_request.pull_request_id
1532 pr_id = pull_request.pull_request_id
1512 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1533 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1513 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1534 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1514
1535
1515 response = self.app.get(route_path(
1536 response = self.app.get(route_path(
1516 'pullrequest_show',
1537 'pullrequest_show',
1517 repo_name=target_repo.name,
1538 repo_name=target_repo.name,
1518 pull_request_id=pr_id))
1539 pull_request_id=pr_id))
1519
1540
1520 if mergeable:
1541 if mergeable:
1521 response.assert_response().element_value_contains(
1542 response.assert_response().element_value_contains(
1522 'input.pr-mergeinfo', shadow_url)
1543 'input.pr-mergeinfo', shadow_url)
1523 response.assert_response().element_value_contains(
1544 response.assert_response().element_value_contains(
1524 'input.pr-mergeinfo ', 'pr-merge')
1545 'input.pr-mergeinfo ', 'pr-merge')
1525 else:
1546 else:
1526 response.assert_response().no_element_exists('.pr-mergeinfo')
1547 response.assert_response().no_element_exists('.pr-mergeinfo')
1527
1548
1528
1549
1529 @pytest.mark.usefixtures('app')
1550 @pytest.mark.usefixtures('app')
1530 @pytest.mark.backends("git", "hg")
1551 @pytest.mark.backends("git", "hg")
1531 class TestPullrequestsControllerDelete(object):
1552 class TestPullrequestsControllerDelete(object):
1532 def test_pull_request_delete_button_permissions_admin(
1553 def test_pull_request_delete_button_permissions_admin(
1533 self, autologin_user, user_admin, pr_util):
1554 self, autologin_user, user_admin, pr_util):
1534 pull_request = pr_util.create_pull_request(
1555 pull_request = pr_util.create_pull_request(
1535 author=user_admin.username, enable_notifications=False)
1556 author=user_admin.username, enable_notifications=False)
1536
1557
1537 response = self.app.get(route_path(
1558 response = self.app.get(route_path(
1538 'pullrequest_show',
1559 'pullrequest_show',
1539 repo_name=pull_request.target_repo.scm_instance().name,
1560 repo_name=pull_request.target_repo.scm_instance().name,
1540 pull_request_id=pull_request.pull_request_id))
1561 pull_request_id=pull_request.pull_request_id))
1541
1562
1542 response.mustcontain('id="delete_pullrequest"')
1563 response.mustcontain('id="delete_pullrequest"')
1543 response.mustcontain('Confirm to delete this pull request')
1564 response.mustcontain('Confirm to delete this pull request')
1544
1565
1545 def test_pull_request_delete_button_permissions_owner(
1566 def test_pull_request_delete_button_permissions_owner(
1546 self, autologin_regular_user, user_regular, pr_util):
1567 self, autologin_regular_user, user_regular, pr_util):
1547 pull_request = pr_util.create_pull_request(
1568 pull_request = pr_util.create_pull_request(
1548 author=user_regular.username, enable_notifications=False)
1569 author=user_regular.username, enable_notifications=False)
1549
1570
1550 response = self.app.get(route_path(
1571 response = self.app.get(route_path(
1551 'pullrequest_show',
1572 'pullrequest_show',
1552 repo_name=pull_request.target_repo.scm_instance().name,
1573 repo_name=pull_request.target_repo.scm_instance().name,
1553 pull_request_id=pull_request.pull_request_id))
1574 pull_request_id=pull_request.pull_request_id))
1554
1575
1555 response.mustcontain('id="delete_pullrequest"')
1576 response.mustcontain('id="delete_pullrequest"')
1556 response.mustcontain('Confirm to delete this pull request')
1577 response.mustcontain('Confirm to delete this pull request')
1557
1578
1558 def test_pull_request_delete_button_permissions_forbidden(
1579 def test_pull_request_delete_button_permissions_forbidden(
1559 self, autologin_regular_user, user_regular, user_admin, pr_util):
1580 self, autologin_regular_user, user_regular, user_admin, pr_util):
1560 pull_request = pr_util.create_pull_request(
1581 pull_request = pr_util.create_pull_request(
1561 author=user_admin.username, enable_notifications=False)
1582 author=user_admin.username, enable_notifications=False)
1562
1583
1563 response = self.app.get(route_path(
1584 response = self.app.get(route_path(
1564 'pullrequest_show',
1585 'pullrequest_show',
1565 repo_name=pull_request.target_repo.scm_instance().name,
1586 repo_name=pull_request.target_repo.scm_instance().name,
1566 pull_request_id=pull_request.pull_request_id))
1587 pull_request_id=pull_request.pull_request_id))
1567 response.mustcontain(no=['id="delete_pullrequest"'])
1588 response.mustcontain(no=['id="delete_pullrequest"'])
1568 response.mustcontain(no=['Confirm to delete this pull request'])
1589 response.mustcontain(no=['Confirm to delete this pull request'])
1569
1590
1570 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1591 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1571 self, autologin_regular_user, user_regular, user_admin, pr_util,
1592 self, autologin_regular_user, user_regular, user_admin, pr_util,
1572 user_util):
1593 user_util):
1573
1594
1574 pull_request = pr_util.create_pull_request(
1595 pull_request = pr_util.create_pull_request(
1575 author=user_admin.username, enable_notifications=False)
1596 author=user_admin.username, enable_notifications=False)
1576
1597
1577 user_util.grant_user_permission_to_repo(
1598 user_util.grant_user_permission_to_repo(
1578 pull_request.target_repo, user_regular,
1599 pull_request.target_repo, user_regular,
1579 'repository.write')
1600 'repository.write')
1580
1601
1581 response = self.app.get(route_path(
1602 response = self.app.get(route_path(
1582 'pullrequest_show',
1603 'pullrequest_show',
1583 repo_name=pull_request.target_repo.scm_instance().name,
1604 repo_name=pull_request.target_repo.scm_instance().name,
1584 pull_request_id=pull_request.pull_request_id))
1605 pull_request_id=pull_request.pull_request_id))
1585
1606
1586 response.mustcontain('id="open_edit_pullrequest"')
1607 response.mustcontain('id="open_edit_pullrequest"')
1587 response.mustcontain('id="delete_pullrequest"')
1608 response.mustcontain('id="delete_pullrequest"')
1588 response.mustcontain(no=['Confirm to delete this pull request'])
1609 response.mustcontain(no=['Confirm to delete this pull request'])
1589
1610
1590 def test_delete_comment_returns_404_if_comment_does_not_exist(
1611 def test_delete_comment_returns_404_if_comment_does_not_exist(
1591 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1612 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1592
1613
1593 pull_request = pr_util.create_pull_request(
1614 pull_request = pr_util.create_pull_request(
1594 author=user_admin.username, enable_notifications=False)
1615 author=user_admin.username, enable_notifications=False)
1595
1616
1596 self.app.post(
1617 self.app.post(
1597 route_path(
1618 route_path(
1598 'pullrequest_comment_delete',
1619 'pullrequest_comment_delete',
1599 repo_name=pull_request.target_repo.scm_instance().name,
1620 repo_name=pull_request.target_repo.scm_instance().name,
1600 pull_request_id=pull_request.pull_request_id,
1621 pull_request_id=pull_request.pull_request_id,
1601 comment_id=1024404),
1622 comment_id=1024404),
1602 extra_environ=xhr_header,
1623 extra_environ=xhr_header,
1603 params={'csrf_token': csrf_token},
1624 params={'csrf_token': csrf_token},
1604 status=404
1625 status=404
1605 )
1626 )
1606
1627
1607 def test_delete_comment(
1628 def test_delete_comment(
1608 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1629 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1609
1630
1610 pull_request = pr_util.create_pull_request(
1631 pull_request = pr_util.create_pull_request(
1611 author=user_admin.username, enable_notifications=False)
1632 author=user_admin.username, enable_notifications=False)
1612 comment = pr_util.create_comment()
1633 comment = pr_util.create_comment()
1613 comment_id = comment.comment_id
1634 comment_id = comment.comment_id
1614
1635
1615 response = self.app.post(
1636 response = self.app.post(
1616 route_path(
1637 route_path(
1617 'pullrequest_comment_delete',
1638 'pullrequest_comment_delete',
1618 repo_name=pull_request.target_repo.scm_instance().name,
1639 repo_name=pull_request.target_repo.scm_instance().name,
1619 pull_request_id=pull_request.pull_request_id,
1640 pull_request_id=pull_request.pull_request_id,
1620 comment_id=comment_id),
1641 comment_id=comment_id),
1621 extra_environ=xhr_header,
1642 extra_environ=xhr_header,
1622 params={'csrf_token': csrf_token},
1643 params={'csrf_token': csrf_token},
1623 status=200
1644 status=200
1624 )
1645 )
1625 assert response.body == 'true'
1646 assert response.body == 'true'
1626
1647
1627 @pytest.mark.parametrize('url_type', [
1648 @pytest.mark.parametrize('url_type', [
1628 'pullrequest_new',
1649 'pullrequest_new',
1629 'pullrequest_create',
1650 'pullrequest_create',
1630 'pullrequest_update',
1651 'pullrequest_update',
1631 'pullrequest_merge',
1652 'pullrequest_merge',
1632 ])
1653 ])
1633 def test_pull_request_is_forbidden_on_archived_repo(
1654 def test_pull_request_is_forbidden_on_archived_repo(
1634 self, autologin_user, backend, xhr_header, user_util, url_type):
1655 self, autologin_user, backend, xhr_header, user_util, url_type):
1635
1656
1636 # create a temporary repo
1657 # create a temporary repo
1637 source = user_util.create_repo(repo_type=backend.alias)
1658 source = user_util.create_repo(repo_type=backend.alias)
1638 repo_name = source.repo_name
1659 repo_name = source.repo_name
1639 repo = Repository.get_by_repo_name(repo_name)
1660 repo = Repository.get_by_repo_name(repo_name)
1640 repo.archived = True
1661 repo.archived = True
1641 Session().commit()
1662 Session().commit()
1642
1663
1643 response = self.app.get(
1664 response = self.app.get(
1644 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1665 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1645
1666
1646 msg = 'Action not supported for archived repository.'
1667 msg = 'Action not supported for archived repository.'
1647 assert_session_flash(response, msg)
1668 assert_session_flash(response, msg)
1648
1669
1649
1670
1650 def assert_pull_request_status(pull_request, expected_status):
1671 def assert_pull_request_status(pull_request, expected_status):
1651 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1672 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1652 assert status == expected_status
1673 assert status == expected_status
1653
1674
1654
1675
1655 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1676 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1656 @pytest.mark.usefixtures("autologin_user")
1677 @pytest.mark.usefixtures("autologin_user")
1657 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1678 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1658 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1679 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,1502 +1,1501 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45
45
46
46
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 failHandler = failHandler || function() {};
48 failHandler = failHandler || function() {};
49 postData = toQueryString(postData);
49 postData = toQueryString(postData);
50 var request = $.ajax({
50 var request = $.ajax({
51 url: url,
51 url: url,
52 type: 'POST',
52 type: 'POST',
53 data: postData,
53 data: postData,
54 headers: {'X-PARTIAL-XHR': true}
54 headers: {'X-PARTIAL-XHR': true}
55 })
55 })
56 .done(function (data) {
56 .done(function (data) {
57 successHandler(data);
57 successHandler(data);
58 })
58 })
59 .fail(function (data, textStatus, errorThrown) {
59 .fail(function (data, textStatus, errorThrown) {
60 failHandler(data, textStatus, errorThrown)
60 failHandler(data, textStatus, errorThrown)
61 });
61 });
62 return request;
62 return request;
63 };
63 };
64
64
65
65
66
66
67
67
68 /* Comment form for main and inline comments */
68 /* Comment form for main and inline comments */
69 (function(mod) {
69 (function(mod) {
70
70
71 if (typeof exports == "object" && typeof module == "object") {
71 if (typeof exports == "object" && typeof module == "object") {
72 // CommonJS
72 // CommonJS
73 module.exports = mod();
73 module.exports = mod();
74 }
74 }
75 else {
75 else {
76 // Plain browser env
76 // Plain browser env
77 (this || window).CommentForm = mod();
77 (this || window).CommentForm = mod();
78 }
78 }
79
79
80 })(function() {
80 })(function() {
81 "use strict";
81 "use strict";
82
82
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84
84
85 if (!(this instanceof CommentForm)) {
85 if (!(this instanceof CommentForm)) {
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 }
87 }
88
88
89 // bind the element instance to our Form
89 // bind the element instance to our Form
90 $(formElement).get(0).CommentForm = this;
90 $(formElement).get(0).CommentForm = this;
91
91
92 this.withLineNo = function(selector) {
92 this.withLineNo = function(selector) {
93 var lineNo = this.lineNo;
93 var lineNo = this.lineNo;
94 if (lineNo === undefined) {
94 if (lineNo === undefined) {
95 return selector
95 return selector
96 } else {
96 } else {
97 return selector + '_' + lineNo;
97 return selector + '_' + lineNo;
98 }
98 }
99 };
99 };
100
100
101 this.commitId = commitId;
101 this.commitId = commitId;
102 this.pullRequestId = pullRequestId;
102 this.pullRequestId = pullRequestId;
103 this.lineNo = lineNo;
103 this.lineNo = lineNo;
104 this.initAutocompleteActions = initAutocompleteActions;
104 this.initAutocompleteActions = initAutocompleteActions;
105
105
106 this.previewButton = this.withLineNo('#preview-btn');
106 this.previewButton = this.withLineNo('#preview-btn');
107 this.previewContainer = this.withLineNo('#preview-container');
107 this.previewContainer = this.withLineNo('#preview-container');
108
108
109 this.previewBoxSelector = this.withLineNo('#preview-box');
109 this.previewBoxSelector = this.withLineNo('#preview-box');
110
110
111 this.editButton = this.withLineNo('#edit-btn');
111 this.editButton = this.withLineNo('#edit-btn');
112 this.editContainer = this.withLineNo('#edit-container');
112 this.editContainer = this.withLineNo('#edit-container');
113 this.cancelButton = this.withLineNo('#cancel-btn');
113 this.cancelButton = this.withLineNo('#cancel-btn');
114 this.commentType = this.withLineNo('#comment_type');
114 this.commentType = this.withLineNo('#comment_type');
115
115
116 this.resolvesId = null;
116 this.resolvesId = null;
117 this.resolvesActionId = null;
117 this.resolvesActionId = null;
118
118
119 this.closesPr = '#close_pull_request';
119 this.closesPr = '#close_pull_request';
120
120
121 this.cmBox = this.withLineNo('#text');
121 this.cmBox = this.withLineNo('#text');
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123
123
124 this.statusChange = this.withLineNo('#change_status');
124 this.statusChange = this.withLineNo('#change_status');
125
125
126 this.submitForm = formElement;
126 this.submitForm = formElement;
127
127
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
129 this.submitButtonText = this.submitButton.val();
129 this.submitButtonText = this.submitButton.val();
130
130
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 this.submitDraftButtonText = this.submitDraftButton.val();
132 this.submitDraftButtonText = this.submitDraftButton.val();
133
133
134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
135 {'repo_name': templateContext.repo_name,
135 {'repo_name': templateContext.repo_name,
136 'commit_id': templateContext.commit_data.commit_id});
136 'commit_id': templateContext.commit_data.commit_id});
137
137
138 if (edit){
138 if (edit){
139 this.submitDraftButton.hide();
139 this.submitDraftButton.hide();
140 this.submitButtonText = _gettext('Update Comment');
140 this.submitButtonText = _gettext('Update Comment');
141 $(this.commentType).prop('disabled', true);
141 $(this.commentType).prop('disabled', true);
142 $(this.commentType).addClass('disabled');
142 $(this.commentType).addClass('disabled');
143 var editInfo =
143 var editInfo =
144 '';
144 '';
145 $(editInfo).insertBefore($(this.editButton).parent());
145 $(editInfo).insertBefore($(this.editButton).parent());
146 }
146 }
147
147
148 if (resolvesCommentId){
148 if (resolvesCommentId){
149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
151 $(this.commentType).prop('disabled', true);
151 $(this.commentType).prop('disabled', true);
152 $(this.commentType).addClass('disabled');
152 $(this.commentType).addClass('disabled');
153
153
154 // disable select
154 // disable select
155 setTimeout(function() {
155 setTimeout(function() {
156 $(self.statusChange).select2('readonly', true);
156 $(self.statusChange).select2('readonly', true);
157 }, 10);
157 }, 10);
158
158
159 var resolvedInfo = (
159 var resolvedInfo = (
160 '<li class="resolve-action">' +
160 '<li class="resolve-action">' +
161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
163 '</li>'
163 '</li>'
164 ).format(resolvesCommentId, _gettext('resolve comment'));
164 ).format(resolvesCommentId, _gettext('resolve comment'));
165 $(resolvedInfo).insertAfter($(this.commentType).parent());
165 $(resolvedInfo).insertAfter($(this.commentType).parent());
166 }
166 }
167
167
168 // based on commitId, or pullRequestId decide where do we submit
168 // based on commitId, or pullRequestId decide where do we submit
169 // out data
169 // out data
170 if (this.commitId){
170 if (this.commitId){
171 var pyurl = 'repo_commit_comment_create';
171 var pyurl = 'repo_commit_comment_create';
172 if(edit){
172 if(edit){
173 pyurl = 'repo_commit_comment_edit';
173 pyurl = 'repo_commit_comment_edit';
174 }
174 }
175 this.submitUrl = pyroutes.url(pyurl,
175 this.submitUrl = pyroutes.url(pyurl,
176 {'repo_name': templateContext.repo_name,
176 {'repo_name': templateContext.repo_name,
177 'commit_id': this.commitId,
177 'commit_id': this.commitId,
178 'comment_id': comment_id});
178 'comment_id': comment_id});
179 this.selfUrl = pyroutes.url('repo_commit',
179 this.selfUrl = pyroutes.url('repo_commit',
180 {'repo_name': templateContext.repo_name,
180 {'repo_name': templateContext.repo_name,
181 'commit_id': this.commitId});
181 'commit_id': this.commitId});
182
182
183 } else if (this.pullRequestId) {
183 } else if (this.pullRequestId) {
184 var pyurl = 'pullrequest_comment_create';
184 var pyurl = 'pullrequest_comment_create';
185 if(edit){
185 if(edit){
186 pyurl = 'pullrequest_comment_edit';
186 pyurl = 'pullrequest_comment_edit';
187 }
187 }
188 this.submitUrl = pyroutes.url(pyurl,
188 this.submitUrl = pyroutes.url(pyurl,
189 {'repo_name': templateContext.repo_name,
189 {'repo_name': templateContext.repo_name,
190 'pull_request_id': this.pullRequestId,
190 'pull_request_id': this.pullRequestId,
191 'comment_id': comment_id});
191 'comment_id': comment_id});
192 this.selfUrl = pyroutes.url('pullrequest_show',
192 this.selfUrl = pyroutes.url('pullrequest_show',
193 {'repo_name': templateContext.repo_name,
193 {'repo_name': templateContext.repo_name,
194 'pull_request_id': this.pullRequestId});
194 'pull_request_id': this.pullRequestId});
195
195
196 } else {
196 } else {
197 throw new Error(
197 throw new Error(
198 'CommentForm requires pullRequestId, or commitId to be specified.')
198 'CommentForm requires pullRequestId, or commitId to be specified.')
199 }
199 }
200
200
201 // FUNCTIONS and helpers
201 // FUNCTIONS and helpers
202 var self = this;
202 var self = this;
203
203
204 this.isInline = function(){
204 this.isInline = function(){
205 return this.lineNo && this.lineNo != 'general';
205 return this.lineNo && this.lineNo != 'general';
206 };
206 };
207
207
208 this.getCmInstance = function(){
208 this.getCmInstance = function(){
209 return this.cm
209 return this.cm
210 };
210 };
211
211
212 this.setPlaceholder = function(placeholder) {
212 this.setPlaceholder = function(placeholder) {
213 var cm = this.getCmInstance();
213 var cm = this.getCmInstance();
214 if (cm){
214 if (cm){
215 cm.setOption('placeholder', placeholder);
215 cm.setOption('placeholder', placeholder);
216 }
216 }
217 };
217 };
218
218
219 this.getCommentStatus = function() {
219 this.getCommentStatus = function() {
220 return $(this.submitForm).find(this.statusChange).val();
220 return $(this.submitForm).find(this.statusChange).val();
221 };
221 };
222
222
223 this.getCommentType = function() {
223 this.getCommentType = function() {
224 return $(this.submitForm).find(this.commentType).val();
224 return $(this.submitForm).find(this.commentType).val();
225 };
225 };
226
226
227 this.getDraftState = function () {
227 this.getDraftState = function () {
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 var data = $(submitterElem).data('isDraft');
229 var data = $(submitterElem).data('isDraft');
230 return data
230 return data
231 }
231 }
232
232
233 this.getResolvesId = function() {
233 this.getResolvesId = function() {
234 return $(this.submitForm).find(this.resolvesId).val() || null;
234 return $(this.submitForm).find(this.resolvesId).val() || null;
235 };
235 };
236
236
237 this.getClosePr = function() {
237 this.getClosePr = function() {
238 return $(this.submitForm).find(this.closesPr).val() || null;
238 return $(this.submitForm).find(this.closesPr).val() || null;
239 };
239 };
240
240
241 this.markCommentResolved = function(resolvedCommentId){
241 this.markCommentResolved = function(resolvedCommentId){
242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
244 };
244 };
245
245
246 this.isAllowedToSubmit = function() {
246 this.isAllowedToSubmit = function() {
247 var commentDisabled = $(this.submitButton).prop('disabled');
247 var commentDisabled = $(this.submitButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 return !commentDisabled && !draftDisabled;
249 return !commentDisabled && !draftDisabled;
250 };
250 };
251
251
252 this.initStatusChangeSelector = function(){
252 this.initStatusChangeSelector = function(){
253 var formatChangeStatus = function(state, escapeMarkup) {
253 var formatChangeStatus = function(state, escapeMarkup) {
254 var originalOption = state.element;
254 var originalOption = state.element;
255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
256 return tmpl
256 return tmpl
257 };
257 };
258 var formatResult = function(result, container, query, escapeMarkup) {
258 var formatResult = function(result, container, query, escapeMarkup) {
259 return formatChangeStatus(result, escapeMarkup);
259 return formatChangeStatus(result, escapeMarkup);
260 };
260 };
261
261
262 var formatSelection = function(data, container, escapeMarkup) {
262 var formatSelection = function(data, container, escapeMarkup) {
263 return formatChangeStatus(data, escapeMarkup);
263 return formatChangeStatus(data, escapeMarkup);
264 };
264 };
265
265
266 $(this.submitForm).find(this.statusChange).select2({
266 $(this.submitForm).find(this.statusChange).select2({
267 placeholder: _gettext('Status Review'),
267 placeholder: _gettext('Status Review'),
268 formatResult: formatResult,
268 formatResult: formatResult,
269 formatSelection: formatSelection,
269 formatSelection: formatSelection,
270 containerCssClass: "drop-menu status_box_menu",
270 containerCssClass: "drop-menu status_box_menu",
271 dropdownCssClass: "drop-menu-dropdown",
271 dropdownCssClass: "drop-menu-dropdown",
272 dropdownAutoWidth: true,
272 dropdownAutoWidth: true,
273 minimumResultsForSearch: -1
273 minimumResultsForSearch: -1
274 });
274 });
275
275
276 $(this.submitForm).find(this.statusChange).on('change', function() {
276 $(this.submitForm).find(this.statusChange).on('change', function() {
277 var status = self.getCommentStatus();
277 var status = self.getCommentStatus();
278
278
279 if (status && !self.isInline()) {
279 if (status && !self.isInline()) {
280 $(self.submitButton).prop('disabled', false);
280 $(self.submitButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
282 }
282 }
283
283
284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
285 self.setPlaceholder(placeholderText)
285 self.setPlaceholder(placeholderText)
286 })
286 })
287 };
287 };
288
288
289 // reset the comment form into it's original state
289 // reset the comment form into it's original state
290 this.resetCommentFormState = function(content) {
290 this.resetCommentFormState = function(content) {
291 content = content || '';
291 content = content || '';
292
292
293 $(this.editContainer).show();
293 $(this.editContainer).show();
294 $(this.editButton).parent().addClass('active');
294 $(this.editButton).parent().addClass('active');
295
295
296 $(this.previewContainer).hide();
296 $(this.previewContainer).hide();
297 $(this.previewButton).parent().removeClass('active');
297 $(this.previewButton).parent().removeClass('active');
298
298
299 this.setActionButtonsDisabled(true);
299 this.setActionButtonsDisabled(true);
300 self.cm.setValue(content);
300 self.cm.setValue(content);
301 self.cm.setOption("readOnly", false);
301 self.cm.setOption("readOnly", false);
302
302
303 if (this.resolvesId) {
303 if (this.resolvesId) {
304 // destroy the resolve action
304 // destroy the resolve action
305 $(this.resolvesId).parent().remove();
305 $(this.resolvesId).parent().remove();
306 }
306 }
307 // reset closingPR flag
307 // reset closingPR flag
308 $('.close-pr-input').remove();
308 $('.close-pr-input').remove();
309
309
310 $(this.statusChange).select2('readonly', false);
310 $(this.statusChange).select2('readonly', false);
311 };
311 };
312
312
313 this.globalSubmitSuccessCallback = function(comment){
313 this.globalSubmitSuccessCallback = function(comment){
314 // default behaviour is to call GLOBAL hook, if it's registered.
314 // default behaviour is to call GLOBAL hook, if it's registered.
315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
316 commentFormGlobalSubmitSuccessCallback(comment);
316 commentFormGlobalSubmitSuccessCallback(comment);
317 }
317 }
318 };
318 };
319
319
320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
322 };
322 };
323
323
324 // overwrite a submitHandler, we need to do it for inline comments
324 // overwrite a submitHandler, we need to do it for inline comments
325 this.setHandleFormSubmit = function(callback) {
325 this.setHandleFormSubmit = function(callback) {
326 this.handleFormSubmit = callback;
326 this.handleFormSubmit = callback;
327 };
327 };
328
328
329 // overwrite a submitSuccessHandler
329 // overwrite a submitSuccessHandler
330 this.setGlobalSubmitSuccessCallback = function(callback) {
330 this.setGlobalSubmitSuccessCallback = function(callback) {
331 this.globalSubmitSuccessCallback = callback;
331 this.globalSubmitSuccessCallback = callback;
332 };
332 };
333
333
334 // default handler for for submit for main comments
334 // default handler for for submit for main comments
335 this.handleFormSubmit = function() {
335 this.handleFormSubmit = function() {
336 var text = self.cm.getValue();
336 var text = self.cm.getValue();
337 var status = self.getCommentStatus();
337 var status = self.getCommentStatus();
338 var commentType = self.getCommentType();
338 var commentType = self.getCommentType();
339 var isDraft = self.getDraftState();
339 var isDraft = self.getDraftState();
340 var resolvesCommentId = self.getResolvesId();
340 var resolvesCommentId = self.getResolvesId();
341 var closePullRequest = self.getClosePr();
341 var closePullRequest = self.getClosePr();
342
342
343 if (text === "" && !status) {
343 if (text === "" && !status) {
344 return;
344 return;
345 }
345 }
346
346
347 var excludeCancelBtn = false;
347 var excludeCancelBtn = false;
348 var submitEvent = true;
348 var submitEvent = true;
349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
350 self.cm.setOption("readOnly", true);
350 self.cm.setOption("readOnly", true);
351
351
352 var postData = {
352 var postData = {
353 'text': text,
353 'text': text,
354 'changeset_status': status,
354 'changeset_status': status,
355 'comment_type': commentType,
355 'comment_type': commentType,
356 'csrf_token': CSRF_TOKEN
356 'csrf_token': CSRF_TOKEN
357 };
357 };
358
358
359 if (resolvesCommentId) {
359 if (resolvesCommentId) {
360 postData['resolves_comment_id'] = resolvesCommentId;
360 postData['resolves_comment_id'] = resolvesCommentId;
361 }
361 }
362
362
363 if (closePullRequest) {
363 if (closePullRequest) {
364 postData['close_pull_request'] = true;
364 postData['close_pull_request'] = true;
365 }
365 }
366
366
367 // submitSuccess for general comments
367 // submitSuccess for general comments
368 var submitSuccessCallback = function(json_data) {
368 var submitSuccessCallback = function(json_data) {
369 // reload page if we change status for single commit.
369 // reload page if we change status for single commit.
370 if (status && self.commitId) {
370 if (status && self.commitId) {
371 location.reload(true);
371 location.reload(true);
372 } else {
372 } else {
373 // inject newly created comments, json_data is {<comment_id>: {}}
373 // inject newly created comments, json_data is {<comment_id>: {}}
374 Rhodecode.comments.attachGeneralComment(json_data)
374 Rhodecode.comments.attachGeneralComment(json_data)
375
375
376 self.resetCommentFormState();
376 self.resetCommentFormState();
377 timeagoActivate();
377 timeagoActivate();
378 tooltipActivate();
378 tooltipActivate();
379
379
380 // mark visually which comment was resolved
380 // mark visually which comment was resolved
381 if (resolvesCommentId) {
381 if (resolvesCommentId) {
382 self.markCommentResolved(resolvesCommentId);
382 self.markCommentResolved(resolvesCommentId);
383 }
383 }
384 }
384 }
385
385
386 // run global callback on submit
386 // run global callback on submit
387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
388
388
389 };
389 };
390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
391 var prefix = "Error while submitting comment.\n"
391 var prefix = "Error while submitting comment.\n"
392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
393 ajaxErrorSwal(message);
393 ajaxErrorSwal(message);
394 self.resetCommentFormState(text);
394 self.resetCommentFormState(text);
395 };
395 };
396 self.submitAjaxPOST(
396 self.submitAjaxPOST(
397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
398 };
398 };
399
399
400 this.previewSuccessCallback = function(o) {
400 this.previewSuccessCallback = function(o) {
401 $(self.previewBoxSelector).html(o);
401 $(self.previewBoxSelector).html(o);
402 $(self.previewBoxSelector).removeClass('unloaded');
402 $(self.previewBoxSelector).removeClass('unloaded');
403
403
404 // swap buttons, making preview active
404 // swap buttons, making preview active
405 $(self.previewButton).parent().addClass('active');
405 $(self.previewButton).parent().addClass('active');
406 $(self.editButton).parent().removeClass('active');
406 $(self.editButton).parent().removeClass('active');
407
407
408 // unlock buttons
408 // unlock buttons
409 self.setActionButtonsDisabled(false);
409 self.setActionButtonsDisabled(false);
410 };
410 };
411
411
412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
413 excludeCancelBtn = excludeCancelBtn || false;
413 excludeCancelBtn = excludeCancelBtn || false;
414 submitEvent = submitEvent || false;
414 submitEvent = submitEvent || false;
415
415
416 $(this.editButton).prop('disabled', state);
416 $(this.editButton).prop('disabled', state);
417 $(this.previewButton).prop('disabled', state);
417 $(this.previewButton).prop('disabled', state);
418
418
419 if (!excludeCancelBtn) {
419 if (!excludeCancelBtn) {
420 $(this.cancelButton).prop('disabled', state);
420 $(this.cancelButton).prop('disabled', state);
421 }
421 }
422
422
423 var submitState = state;
423 var submitState = state;
424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
425 // if the value of commit review status is set, we allow
425 // if the value of commit review status is set, we allow
426 // submit button, but only on Main form, isInline means inline
426 // submit button, but only on Main form, isInline means inline
427 submitState = false
427 submitState = false
428 }
428 }
429
429
430 $(this.submitButton).prop('disabled', submitState);
430 $(this.submitButton).prop('disabled', submitState);
431 $(this.submitDraftButton).prop('disabled', submitState);
431 $(this.submitDraftButton).prop('disabled', submitState);
432
432
433 if (submitEvent) {
433 if (submitEvent) {
434 var isDraft = self.getDraftState();
434 var isDraft = self.getDraftState();
435
435
436 if (isDraft) {
436 if (isDraft) {
437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
438 } else {
438 } else {
439 $(this.submitButton).val(_gettext('Submitting...'));
439 $(this.submitButton).val(_gettext('Submitting...'));
440 }
440 }
441
441
442 } else {
442 } else {
443 $(this.submitButton).val(this.submitButtonText);
443 $(this.submitButton).val(this.submitButtonText);
444 $(this.submitDraftButton).val(this.submitDraftButtonText);
444 $(this.submitDraftButton).val(this.submitDraftButtonText);
445 }
445 }
446
446
447 };
447 };
448
448
449 // lock preview/edit/submit buttons on load, but exclude cancel button
449 // lock preview/edit/submit buttons on load, but exclude cancel button
450 var excludeCancelBtn = true;
450 var excludeCancelBtn = true;
451 this.setActionButtonsDisabled(true, excludeCancelBtn);
451 this.setActionButtonsDisabled(true, excludeCancelBtn);
452
452
453 // anonymous users don't have access to initialized CM instance
453 // anonymous users don't have access to initialized CM instance
454 if (this.cm !== undefined){
454 if (this.cm !== undefined){
455 this.cm.on('change', function(cMirror) {
455 this.cm.on('change', function(cMirror) {
456 if (cMirror.getValue() === "") {
456 if (cMirror.getValue() === "") {
457 self.setActionButtonsDisabled(true, excludeCancelBtn)
457 self.setActionButtonsDisabled(true, excludeCancelBtn)
458 } else {
458 } else {
459 self.setActionButtonsDisabled(false, excludeCancelBtn)
459 self.setActionButtonsDisabled(false, excludeCancelBtn)
460 }
460 }
461 });
461 });
462 }
462 }
463
463
464 $(this.editButton).on('click', function(e) {
464 $(this.editButton).on('click', function(e) {
465 e.preventDefault();
465 e.preventDefault();
466
466
467 $(self.previewButton).parent().removeClass('active');
467 $(self.previewButton).parent().removeClass('active');
468 $(self.previewContainer).hide();
468 $(self.previewContainer).hide();
469
469
470 $(self.editButton).parent().addClass('active');
470 $(self.editButton).parent().addClass('active');
471 $(self.editContainer).show();
471 $(self.editContainer).show();
472
472
473 });
473 });
474
474
475 $(this.previewButton).on('click', function(e) {
475 $(this.previewButton).on('click', function(e) {
476 e.preventDefault();
476 e.preventDefault();
477 var text = self.cm.getValue();
477 var text = self.cm.getValue();
478
478
479 if (text === "") {
479 if (text === "") {
480 return;
480 return;
481 }
481 }
482
482
483 var postData = {
483 var postData = {
484 'text': text,
484 'text': text,
485 'renderer': templateContext.visual.default_renderer,
485 'renderer': templateContext.visual.default_renderer,
486 'csrf_token': CSRF_TOKEN
486 'csrf_token': CSRF_TOKEN
487 };
487 };
488
488
489 // lock ALL buttons on preview
489 // lock ALL buttons on preview
490 self.setActionButtonsDisabled(true);
490 self.setActionButtonsDisabled(true);
491
491
492 $(self.previewBoxSelector).addClass('unloaded');
492 $(self.previewBoxSelector).addClass('unloaded');
493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
494
494
495 $(self.editContainer).hide();
495 $(self.editContainer).hide();
496 $(self.previewContainer).show();
496 $(self.previewContainer).show();
497
497
498 // by default we reset state of comment preserving the text
498 // by default we reset state of comment preserving the text
499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
500 var prefix = "Error while preview of comment.\n"
500 var prefix = "Error while preview of comment.\n"
501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
502 ajaxErrorSwal(message);
502 ajaxErrorSwal(message);
503
503
504 self.resetCommentFormState(text)
504 self.resetCommentFormState(text)
505 };
505 };
506 self.submitAjaxPOST(
506 self.submitAjaxPOST(
507 self.previewUrl, postData, self.previewSuccessCallback,
507 self.previewUrl, postData, self.previewSuccessCallback,
508 previewFailCallback);
508 previewFailCallback);
509
509
510 $(self.previewButton).parent().addClass('active');
510 $(self.previewButton).parent().addClass('active');
511 $(self.editButton).parent().removeClass('active');
511 $(self.editButton).parent().removeClass('active');
512 });
512 });
513
513
514 $(this.submitForm).submit(function(e) {
514 $(this.submitForm).submit(function(e) {
515 e.preventDefault();
515 e.preventDefault();
516 var allowedToSubmit = self.isAllowedToSubmit();
516 var allowedToSubmit = self.isAllowedToSubmit();
517 if (!allowedToSubmit){
517 if (!allowedToSubmit){
518 return false;
518 return false;
519 }
519 }
520
520
521 self.handleFormSubmit();
521 self.handleFormSubmit();
522 });
522 });
523
523
524 }
524 }
525
525
526 return CommentForm;
526 return CommentForm;
527 });
527 });
528
528
529 /* selector for comment versions */
529 /* selector for comment versions */
530 var initVersionSelector = function(selector, initialData) {
530 var initVersionSelector = function(selector, initialData) {
531
531
532 var formatResult = function(result, container, query, escapeMarkup) {
532 var formatResult = function(result, container, query, escapeMarkup) {
533
533
534 return renderTemplate('commentVersion', {
534 return renderTemplate('commentVersion', {
535 show_disabled: true,
535 show_disabled: true,
536 version: result.comment_version,
536 version: result.comment_version,
537 user_name: result.comment_author_username,
537 user_name: result.comment_author_username,
538 gravatar_url: result.comment_author_gravatar,
538 gravatar_url: result.comment_author_gravatar,
539 size: 16,
539 size: 16,
540 timeago_component: result.comment_created_on,
540 timeago_component: result.comment_created_on,
541 })
541 })
542 };
542 };
543
543
544 $(selector).select2({
544 $(selector).select2({
545 placeholder: "Edited",
545 placeholder: "Edited",
546 containerCssClass: "drop-menu-comment-history",
546 containerCssClass: "drop-menu-comment-history",
547 dropdownCssClass: "drop-menu-dropdown",
547 dropdownCssClass: "drop-menu-dropdown",
548 dropdownAutoWidth: true,
548 dropdownAutoWidth: true,
549 minimumResultsForSearch: -1,
549 minimumResultsForSearch: -1,
550 data: initialData,
550 data: initialData,
551 formatResult: formatResult,
551 formatResult: formatResult,
552 });
552 });
553
553
554 $(selector).on('select2-selecting', function (e) {
554 $(selector).on('select2-selecting', function (e) {
555 // hide the mast as we later do preventDefault()
555 // hide the mast as we later do preventDefault()
556 $("#select2-drop-mask").click();
556 $("#select2-drop-mask").click();
557 e.preventDefault();
557 e.preventDefault();
558 e.choice.action();
558 e.choice.action();
559 });
559 });
560
560
561 $(selector).on("select2-open", function() {
561 $(selector).on("select2-open", function() {
562 timeagoActivate();
562 timeagoActivate();
563 });
563 });
564 };
564 };
565
565
566 /* comments controller */
566 /* comments controller */
567 var CommentsController = function() {
567 var CommentsController = function() {
568 var mainComment = '#text';
568 var mainComment = '#text';
569 var self = this;
569 var self = this;
570
570
571 this.showVersion = function (comment_id, comment_history_id) {
571 this.showVersion = function (comment_id, comment_history_id) {
572
572
573 var historyViewUrl = pyroutes.url(
573 var historyViewUrl = pyroutes.url(
574 'repo_commit_comment_history_view',
574 'repo_commit_comment_history_view',
575 {
575 {
576 'repo_name': templateContext.repo_name,
576 'repo_name': templateContext.repo_name,
577 'commit_id': comment_id,
577 'commit_id': comment_id,
578 'comment_history_id': comment_history_id,
578 'comment_history_id': comment_history_id,
579 }
579 }
580 );
580 );
581 successRenderCommit = function (data) {
581 successRenderCommit = function (data) {
582 SwalNoAnimation.fire({
582 SwalNoAnimation.fire({
583 html: data,
583 html: data,
584 title: '',
584 title: '',
585 });
585 });
586 };
586 };
587 failRenderCommit = function () {
587 failRenderCommit = function () {
588 SwalNoAnimation.fire({
588 SwalNoAnimation.fire({
589 html: 'Error while loading comment history',
589 html: 'Error while loading comment history',
590 title: '',
590 title: '',
591 });
591 });
592 };
592 };
593 _submitAjaxPOST(
593 _submitAjaxPOST(
594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
595 successRenderCommit,
595 successRenderCommit,
596 failRenderCommit
596 failRenderCommit
597 );
597 );
598 };
598 };
599
599
600 this.getLineNumber = function(node) {
600 this.getLineNumber = function(node) {
601 var $node = $(node);
601 var $node = $(node);
602 var lineNo = $node.closest('td').attr('data-line-no');
602 var lineNo = $node.closest('td').attr('data-line-no');
603 if (lineNo === undefined && $node.data('commentInline')){
603 if (lineNo === undefined && $node.data('commentInline')){
604 lineNo = $node.data('commentLineNo')
604 lineNo = $node.data('commentLineNo')
605 }
605 }
606
606
607 return lineNo
607 return lineNo
608 };
608 };
609
609
610 this.scrollToComment = function(node, offset, outdated) {
610 this.scrollToComment = function(node, offset, outdated) {
611 if (offset === undefined) {
611 if (offset === undefined) {
612 offset = 0;
612 offset = 0;
613 }
613 }
614 var outdated = outdated || false;
614 var outdated = outdated || false;
615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
616
616
617 if (!node) {
617 if (!node) {
618 node = $('.comment-selected');
618 node = $('.comment-selected');
619 if (!node.length) {
619 if (!node.length) {
620 node = $('comment-current')
620 node = $('comment-current')
621 }
621 }
622 }
622 }
623
623
624 $wrapper = $(node).closest('div.comment');
624 $wrapper = $(node).closest('div.comment');
625
625
626 // show hidden comment when referenced.
626 // show hidden comment when referenced.
627 if (!$wrapper.is(':visible')){
627 if (!$wrapper.is(':visible')){
628 $wrapper.show();
628 $wrapper.show();
629 }
629 }
630
630
631 $comment = $(node).closest(klass);
631 $comment = $(node).closest(klass);
632 $comments = $(klass);
632 $comments = $(klass);
633
633
634 $('.comment-selected').removeClass('comment-selected');
634 $('.comment-selected').removeClass('comment-selected');
635
635
636 var nextIdx = $(klass).index($comment) + offset;
636 var nextIdx = $(klass).index($comment) + offset;
637 if (nextIdx >= $comments.length) {
637 if (nextIdx >= $comments.length) {
638 nextIdx = 0;
638 nextIdx = 0;
639 }
639 }
640 var $next = $(klass).eq(nextIdx);
640 var $next = $(klass).eq(nextIdx);
641
641
642 var $cb = $next.closest('.cb');
642 var $cb = $next.closest('.cb');
643 $cb.removeClass('cb-collapsed');
643 $cb.removeClass('cb-collapsed');
644
644
645 var $filediffCollapseState = $cb.closest('.filediff').prev();
645 var $filediffCollapseState = $cb.closest('.filediff').prev();
646 $filediffCollapseState.prop('checked', false);
646 $filediffCollapseState.prop('checked', false);
647 $next.addClass('comment-selected');
647 $next.addClass('comment-selected');
648 scrollToElement($next);
648 scrollToElement($next);
649 return false;
649 return false;
650 };
650 };
651
651
652 this.nextComment = function(node) {
652 this.nextComment = function(node) {
653 return self.scrollToComment(node, 1);
653 return self.scrollToComment(node, 1);
654 };
654 };
655
655
656 this.prevComment = function(node) {
656 this.prevComment = function(node) {
657 return self.scrollToComment(node, -1);
657 return self.scrollToComment(node, -1);
658 };
658 };
659
659
660 this.nextOutdatedComment = function(node) {
660 this.nextOutdatedComment = function(node) {
661 return self.scrollToComment(node, 1, true);
661 return self.scrollToComment(node, 1, true);
662 };
662 };
663
663
664 this.prevOutdatedComment = function(node) {
664 this.prevOutdatedComment = function(node) {
665 return self.scrollToComment(node, -1, true);
665 return self.scrollToComment(node, -1, true);
666 };
666 };
667
667
668 this.cancelComment = function (node) {
668 this.cancelComment = function (node) {
669 var $node = $(node);
669 var $node = $(node);
670 var edit = $(this).attr('edit');
670 var edit = $(this).attr('edit');
671 var $inlineComments = $node.closest('div.inline-comments');
671 var $inlineComments = $node.closest('div.inline-comments');
672
672
673 if (edit) {
673 if (edit) {
674 var $general_comments = null;
674 var $general_comments = null;
675 if (!$inlineComments.length) {
675 if (!$inlineComments.length) {
676 $general_comments = $('#comments');
676 $general_comments = $('#comments');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 // show hidden general comment form
678 // show hidden general comment form
679 $('#cb-comment-general-form-placeholder').show();
679 $('#cb-comment-general-form-placeholder').show();
680 } else {
680 } else {
681 var $comment = $inlineComments.find('div.comment:hidden');
681 var $comment = $inlineComments.find('div.comment:hidden');
682 }
682 }
683 $comment.show();
683 $comment.show();
684 }
684 }
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 $replyWrapper.removeClass('comment-form-active');
686 $replyWrapper.removeClass('comment-form-active');
687
687
688 var lastComment = $inlineComments.find('.comment-inline').last();
688 var lastComment = $inlineComments.find('.comment-inline').last();
689 if ($(lastComment).hasClass('comment-outdated')) {
689 if ($(lastComment).hasClass('comment-outdated')) {
690 $replyWrapper.hide();
690 $replyWrapper.hide();
691 }
691 }
692
692
693 $node.closest('.comment-inline-form').remove();
693 $node.closest('.comment-inline-form').remove();
694 return false;
694 return false;
695 };
695 };
696
696
697 this._deleteComment = function(node) {
697 this._deleteComment = function(node) {
698 var $node = $(node);
698 var $node = $(node);
699 var $td = $node.closest('td');
699 var $td = $node.closest('td');
700 var $comment = $node.closest('.comment');
700 var $comment = $node.closest('.comment');
701 var comment_id = $($comment).data('commentId');
701 var comment_id = $($comment).data('commentId');
702 var isDraft = $($comment).data('commentDraft');
702 var isDraft = $($comment).data('commentDraft');
703
703
704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
705 var commitId = templateContext.commit_data.commit_id;
705 var commitId = templateContext.commit_data.commit_id;
706
706
707 if (pullRequestId) {
707 if (pullRequestId) {
708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
709 } else if (commitId) {
709 } else if (commitId) {
710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
711 }
711 }
712
712
713 var postData = {
713 var postData = {
714 'csrf_token': CSRF_TOKEN
714 'csrf_token': CSRF_TOKEN
715 };
715 };
716
716
717 $comment.addClass('comment-deleting');
717 $comment.addClass('comment-deleting');
718 $comment.hide('fast');
718 $comment.hide('fast');
719
719
720 var success = function(response) {
720 var success = function(response) {
721 $comment.remove();
721 $comment.remove();
722
722
723 if (window.updateSticky !== undefined) {
723 if (window.updateSticky !== undefined) {
724 // potentially our comments change the active window size, so we
724 // potentially our comments change the active window size, so we
725 // notify sticky elements
725 // notify sticky elements
726 updateSticky()
726 updateSticky()
727 }
727 }
728
728
729 if (window.refreshAllComments !== undefined && !isDraft) {
729 if (window.refreshAllComments !== undefined && !isDraft) {
730 // if we have this handler, run it, and refresh all comments boxes
730 // if we have this handler, run it, and refresh all comments boxes
731 refreshAllComments()
731 refreshAllComments()
732 }
732 }
733 else if (window.refreshDraftComments !== undefined && isDraft) {
733 else if (window.refreshDraftComments !== undefined && isDraft) {
734 // if we have this handler, run it, and refresh all comments boxes
734 // if we have this handler, run it, and refresh all comments boxes
735 refreshDraftComments();
735 refreshDraftComments();
736 }
736 }
737 return false;
737 return false;
738 };
738 };
739
739
740 var failure = function(jqXHR, textStatus, errorThrown) {
740 var failure = function(jqXHR, textStatus, errorThrown) {
741 var prefix = "Error while deleting this comment.\n"
741 var prefix = "Error while deleting this comment.\n"
742 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
742 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
743 ajaxErrorSwal(message);
743 ajaxErrorSwal(message);
744
744
745 $comment.show('fast');
745 $comment.show('fast');
746 $comment.removeClass('comment-deleting');
746 $comment.removeClass('comment-deleting');
747 return false;
747 return false;
748 };
748 };
749 ajaxPOST(url, postData, success, failure);
749 ajaxPOST(url, postData, success, failure);
750
750
751 }
751 }
752
752
753 this.deleteComment = function(node) {
753 this.deleteComment = function(node) {
754 var $comment = $(node).closest('.comment');
754 var $comment = $(node).closest('.comment');
755 var comment_id = $comment.attr('data-comment-id');
755 var comment_id = $comment.attr('data-comment-id');
756
756
757 SwalNoAnimation.fire({
757 SwalNoAnimation.fire({
758 title: 'Delete this comment?',
758 title: 'Delete this comment?',
759 icon: 'warning',
759 icon: 'warning',
760 showCancelButton: true,
760 showCancelButton: true,
761 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
761 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
762
762
763 }).then(function(result) {
763 }).then(function(result) {
764 if (result.value) {
764 if (result.value) {
765 self._deleteComment(node);
765 self._deleteComment(node);
766 }
766 }
767 })
767 })
768 };
768 };
769
769
770 this._finalizeDrafts = function(commentIds) {
770 this._finalizeDrafts = function(commentIds) {
771
771
772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
773 var commitId = templateContext.commit_data.commit_id;
773 var commitId = templateContext.commit_data.commit_id;
774
774
775 if (pullRequestId) {
775 if (pullRequestId) {
776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
777 } else if (commitId) {
777 } else if (commitId) {
778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
779 }
779 }
780
780
781 // remove the drafts so we can lock them before submit.
781 // remove the drafts so we can lock them before submit.
782 $.each(commentIds, function(idx, val){
782 $.each(commentIds, function(idx, val){
783 $('#comment-{0}'.format(val)).remove();
783 $('#comment-{0}'.format(val)).remove();
784 })
784 })
785
785
786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
787
787
788 var submitSuccessCallback = function(json_data) {
788 var submitSuccessCallback = function(json_data) {
789 self.attachInlineComment(json_data);
789 self.attachInlineComment(json_data);
790
790
791 if (window.refreshDraftComments !== undefined) {
791 if (window.refreshDraftComments !== undefined) {
792 // if we have this handler, run it, and refresh all comments boxes
792 // if we have this handler, run it, and refresh all comments boxes
793 refreshDraftComments()
793 refreshDraftComments()
794 }
794 }
795
795
796 return false;
796 return false;
797 };
797 };
798
798
799 ajaxPOST(url, postData, submitSuccessCallback)
799 ajaxPOST(url, postData, submitSuccessCallback)
800
800
801 }
801 }
802
802
803 this.finalizeDrafts = function(commentIds, callback) {
803 this.finalizeDrafts = function(commentIds, callback) {
804
804
805 SwalNoAnimation.fire({
805 SwalNoAnimation.fire({
806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
807 icon: 'warning',
807 icon: 'warning',
808 showCancelButton: true,
808 showCancelButton: true,
809 confirmButtonText: _gettext('Yes'),
809 confirmButtonText: _gettext('Yes'),
810
810
811 }).then(function(result) {
811 }).then(function(result) {
812 if (result.value) {
812 if (result.value) {
813 if (callback !== undefined) {
813 if (callback !== undefined) {
814 callback(result)
814 callback(result)
815 }
815 }
816 self._finalizeDrafts(commentIds);
816 self._finalizeDrafts(commentIds);
817 }
817 }
818 })
818 })
819 };
819 };
820
820
821 this.toggleWideMode = function (node) {
821 this.toggleWideMode = function (node) {
822
822
823 if ($('#content').hasClass('wrapper')) {
823 if ($('#content').hasClass('wrapper')) {
824 $('#content').removeClass("wrapper");
824 $('#content').removeClass("wrapper");
825 $('#content').addClass("wide-mode-wrapper");
825 $('#content').addClass("wide-mode-wrapper");
826 $(node).addClass('btn-success');
826 $(node).addClass('btn-success');
827 return true
827 return true
828 } else {
828 } else {
829 $('#content').removeClass("wide-mode-wrapper");
829 $('#content').removeClass("wide-mode-wrapper");
830 $('#content').addClass("wrapper");
830 $('#content').addClass("wrapper");
831 $(node).removeClass('btn-success');
831 $(node).removeClass('btn-success');
832 return false
832 return false
833 }
833 }
834
834
835 };
835 };
836
836
837 /**
837 /**
838 * Turn off/on all comments in file diff
838 * Turn off/on all comments in file diff
839 */
839 */
840 this.toggleDiffComments = function(node) {
840 this.toggleDiffComments = function(node) {
841 // Find closes filediff container
841 // Find closes filediff container
842 var $filediff = $(node).closest('.filediff');
842 var $filediff = $(node).closest('.filediff');
843 if ($(node).hasClass('toggle-on')) {
843 if ($(node).hasClass('toggle-on')) {
844 var show = false;
844 var show = false;
845 } else if ($(node).hasClass('toggle-off')) {
845 } else if ($(node).hasClass('toggle-off')) {
846 var show = true;
846 var show = true;
847 }
847 }
848
848
849 // Toggle each individual comment block, so we can un-toggle single ones
849 // Toggle each individual comment block, so we can un-toggle single ones
850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
851 self.toggleLineComments($(val), show)
851 self.toggleLineComments($(val), show)
852 })
852 })
853
853
854 // since we change the height of the diff container that has anchor points for upper
854 // since we change the height of the diff container that has anchor points for upper
855 // sticky header, we need to tell it to re-calculate those
855 // sticky header, we need to tell it to re-calculate those
856 if (window.updateSticky !== undefined) {
856 if (window.updateSticky !== undefined) {
857 // potentially our comments change the active window size, so we
857 // potentially our comments change the active window size, so we
858 // notify sticky elements
858 // notify sticky elements
859 updateSticky()
859 updateSticky()
860 }
860 }
861
861
862 return false;
862 return false;
863 }
863 }
864
864
865 this.toggleLineComments = function(node, show) {
865 this.toggleLineComments = function(node, show) {
866
866
867 var trElem = $(node).closest('tr')
867 var trElem = $(node).closest('tr')
868
868
869 if (show === true) {
869 if (show === true) {
870 // mark outdated comments as visible before the toggle;
870 // mark outdated comments as visible before the toggle;
871 $(trElem).find('.comment-outdated').show();
871 $(trElem).find('.comment-outdated').show();
872 $(trElem).removeClass('hide-line-comments');
872 $(trElem).removeClass('hide-line-comments');
873 } else if (show === false) {
873 } else if (show === false) {
874 $(trElem).find('.comment-outdated').hide();
874 $(trElem).find('.comment-outdated').hide();
875 $(trElem).addClass('hide-line-comments');
875 $(trElem).addClass('hide-line-comments');
876 } else {
876 } else {
877 // mark outdated comments as visible before the toggle;
877 // mark outdated comments as visible before the toggle;
878 $(trElem).find('.comment-outdated').show();
878 $(trElem).find('.comment-outdated').show();
879 $(trElem).toggleClass('hide-line-comments');
879 $(trElem).toggleClass('hide-line-comments');
880 }
880 }
881
881
882 // since we change the height of the diff container that has anchor points for upper
882 // since we change the height of the diff container that has anchor points for upper
883 // sticky header, we need to tell it to re-calculate those
883 // sticky header, we need to tell it to re-calculate those
884 if (window.updateSticky !== undefined) {
884 if (window.updateSticky !== undefined) {
885 // potentially our comments change the active window size, so we
885 // potentially our comments change the active window size, so we
886 // notify sticky elements
886 // notify sticky elements
887 updateSticky()
887 updateSticky()
888 }
888 }
889
889
890 };
890 };
891
891
892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
893 var pullRequestId = templateContext.pull_request_data.pull_request_id;
893 var pullRequestId = templateContext.pull_request_data.pull_request_id;
894 var commitId = templateContext.commit_data.commit_id;
894 var commitId = templateContext.commit_data.commit_id;
895
895
896 var commentForm = new CommentForm(
896 var commentForm = new CommentForm(
897 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
897 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
898 var cm = commentForm.getCmInstance();
898 var cm = commentForm.getCmInstance();
899
899
900 if (resolvesCommentId){
900 if (resolvesCommentId){
901 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
901 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
902 }
902 }
903
903
904 setTimeout(function() {
904 setTimeout(function() {
905 // callbacks
905 // callbacks
906 if (cm !== undefined) {
906 if (cm !== undefined) {
907 commentForm.setPlaceholder(placeholderText);
907 commentForm.setPlaceholder(placeholderText);
908 if (commentForm.isInline()) {
908 if (commentForm.isInline()) {
909 cm.focus();
909 cm.focus();
910 cm.refresh();
910 cm.refresh();
911 }
911 }
912 }
912 }
913 }, 10);
913 }, 10);
914
914
915 // trigger scrolldown to the resolve comment, since it might be away
915 // trigger scrolldown to the resolve comment, since it might be away
916 // from the clicked
916 // from the clicked
917 if (resolvesCommentId){
917 if (resolvesCommentId){
918 var actionNode = $(commentForm.resolvesActionId).offset();
918 var actionNode = $(commentForm.resolvesActionId).offset();
919
919
920 setTimeout(function() {
920 setTimeout(function() {
921 if (actionNode) {
921 if (actionNode) {
922 $('body, html').animate({scrollTop: actionNode.top}, 10);
922 $('body, html').animate({scrollTop: actionNode.top}, 10);
923 }
923 }
924 }, 100);
924 }, 100);
925 }
925 }
926
926
927 // add dropzone support
927 // add dropzone support
928 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
928 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
929 var renderer = templateContext.visual.default_renderer;
929 var renderer = templateContext.visual.default_renderer;
930 if (renderer == 'rst') {
930 if (renderer == 'rst') {
931 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
931 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
932 if (isRendered){
932 if (isRendered){
933 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
933 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
934 }
934 }
935 } else if (renderer == 'markdown') {
935 } else if (renderer == 'markdown') {
936 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
936 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
937 if (isRendered){
937 if (isRendered){
938 attachmentUrl = '!' + attachmentUrl;
938 attachmentUrl = '!' + attachmentUrl;
939 }
939 }
940 } else {
940 } else {
941 var attachmentUrl = '{}'.format(attachmentStoreUrl);
941 var attachmentUrl = '{}'.format(attachmentStoreUrl);
942 }
942 }
943 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
943 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
944
944
945 return false;
945 return false;
946 };
946 };
947
947
948 //see: https://www.dropzonejs.com/#configuration
948 //see: https://www.dropzonejs.com/#configuration
949 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
949 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
950 {'repo_name': templateContext.repo_name,
950 {'repo_name': templateContext.repo_name,
951 'commit_id': templateContext.commit_data.commit_id})
951 'commit_id': templateContext.commit_data.commit_id})
952
952
953 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
953 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
954 if (previewTmpl !== undefined){
954 if (previewTmpl !== undefined){
955 var selectLink = $(formElement).find('.pick-attachment').get(0);
955 var selectLink = $(formElement).find('.pick-attachment').get(0);
956 $(formElement).find('.comment-attachment-uploader').dropzone({
956 $(formElement).find('.comment-attachment-uploader').dropzone({
957 url: storeUrl,
957 url: storeUrl,
958 headers: {"X-CSRF-Token": CSRF_TOKEN},
958 headers: {"X-CSRF-Token": CSRF_TOKEN},
959 paramName: function () {
959 paramName: function () {
960 return "attachment"
960 return "attachment"
961 }, // The name that will be used to transfer the file
961 }, // The name that will be used to transfer the file
962 clickable: selectLink,
962 clickable: selectLink,
963 parallelUploads: 1,
963 parallelUploads: 1,
964 maxFiles: 10,
964 maxFiles: 10,
965 maxFilesize: templateContext.attachment_store.max_file_size_mb,
965 maxFilesize: templateContext.attachment_store.max_file_size_mb,
966 uploadMultiple: false,
966 uploadMultiple: false,
967 autoProcessQueue: true, // if false queue will not be processed automatically.
967 autoProcessQueue: true, // if false queue will not be processed automatically.
968 createImageThumbnails: false,
968 createImageThumbnails: false,
969 previewTemplate: previewTmpl.innerHTML,
969 previewTemplate: previewTmpl.innerHTML,
970
970
971 accept: function (file, done) {
971 accept: function (file, done) {
972 done();
972 done();
973 },
973 },
974 init: function () {
974 init: function () {
975
975
976 this.on("sending", function (file, xhr, formData) {
976 this.on("sending", function (file, xhr, formData) {
977 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
977 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
978 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
978 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
979 });
979 });
980
980
981 this.on("success", function (file, response) {
981 this.on("success", function (file, response) {
982 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
982 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
983 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
983 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
984
984
985 var isRendered = false;
985 var isRendered = false;
986 var ext = file.name.split('.').pop();
986 var ext = file.name.split('.').pop();
987 var imageExts = templateContext.attachment_store.image_ext;
987 var imageExts = templateContext.attachment_store.image_ext;
988 if (imageExts.indexOf(ext) !== -1){
988 if (imageExts.indexOf(ext) !== -1){
989 isRendered = true;
989 isRendered = true;
990 }
990 }
991
991
992 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
992 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
993 });
993 });
994
994
995 this.on("error", function (file, errorMessage, xhr) {
995 this.on("error", function (file, errorMessage, xhr) {
996 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
996 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
997
997
998 var error = null;
998 var error = null;
999
999
1000 if (xhr !== undefined){
1000 if (xhr !== undefined){
1001 var httpStatus = xhr.status + " " + xhr.statusText;
1001 var httpStatus = xhr.status + " " + xhr.statusText;
1002 if (xhr !== undefined && xhr.status >= 500) {
1002 if (xhr !== undefined && xhr.status >= 500) {
1003 error = httpStatus;
1003 error = httpStatus;
1004 }
1004 }
1005 }
1005 }
1006
1006
1007 if (error === null) {
1007 if (error === null) {
1008 error = errorMessage.error || errorMessage || httpStatus;
1008 error = errorMessage.error || errorMessage || httpStatus;
1009 }
1009 }
1010 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1010 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1011
1011
1012 });
1012 });
1013 }
1013 }
1014 });
1014 });
1015 }
1015 }
1016 return commentForm;
1016 return commentForm;
1017 };
1017 };
1018
1018
1019 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1019 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1020
1020
1021 var tmpl = $('#cb-comment-general-form-template').html();
1021 var tmpl = $('#cb-comment-general-form-template').html();
1022 tmpl = tmpl.format(null, 'general');
1022 tmpl = tmpl.format(null, 'general');
1023 var $form = $(tmpl);
1023 var $form = $(tmpl);
1024
1024
1025 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1025 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1026 var curForm = $formPlaceholder.find('form');
1026 var curForm = $formPlaceholder.find('form');
1027 if (curForm){
1027 if (curForm){
1028 curForm.remove();
1028 curForm.remove();
1029 }
1029 }
1030 $formPlaceholder.append($form);
1030 $formPlaceholder.append($form);
1031
1031
1032 var _form = $($form[0]);
1032 var _form = $($form[0]);
1033 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1033 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1034 var edit = false;
1034 var edit = false;
1035 var comment_id = null;
1035 var comment_id = null;
1036 var commentForm = this.createCommentForm(
1036 var commentForm = this.createCommentForm(
1037 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1037 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1038 commentForm.initStatusChangeSelector();
1038 commentForm.initStatusChangeSelector();
1039
1039
1040 return commentForm;
1040 return commentForm;
1041 };
1041 };
1042
1042
1043 this.editComment = function(node, line_no, f_path) {
1043 this.editComment = function(node, line_no, f_path) {
1044 self.edit = true;
1044 self.edit = true;
1045 var $node = $(node);
1045 var $node = $(node);
1046 var $td = $node.closest('td');
1046 var $td = $node.closest('td');
1047
1047
1048 var $comment = $(node).closest('.comment');
1048 var $comment = $(node).closest('.comment');
1049 var comment_id = $($comment).data('commentId');
1049 var comment_id = $($comment).data('commentId');
1050 var isDraft = $($comment).data('commentDraft');
1050 var isDraft = $($comment).data('commentDraft');
1051 var $editForm = null
1051 var $editForm = null
1052
1052
1053 var $comments = $node.closest('div.inline-comments');
1053 var $comments = $node.closest('div.inline-comments');
1054 var $general_comments = null;
1054 var $general_comments = null;
1055
1055
1056 if($comments.length){
1056 if($comments.length){
1057 // inline comments setup
1057 // inline comments setup
1058 $editForm = $comments.find('.comment-inline-form');
1058 $editForm = $comments.find('.comment-inline-form');
1059 line_no = self.getLineNumber(node)
1059 line_no = self.getLineNumber(node)
1060 }
1060 }
1061 else{
1061 else{
1062 // general comments setup
1062 // general comments setup
1063 $comments = $('#comments');
1063 $comments = $('#comments');
1064 $editForm = $comments.find('.comment-inline-form');
1064 $editForm = $comments.find('.comment-inline-form');
1065 line_no = $comment[0].id
1065 line_no = $comment[0].id
1066 $('#cb-comment-general-form-placeholder').hide();
1066 $('#cb-comment-general-form-placeholder').hide();
1067 }
1067 }
1068
1068
1069 if ($editForm.length === 0) {
1069 if ($editForm.length === 0) {
1070
1070
1071 // unhide all comments if they are hidden for a proper REPLY mode
1071 // unhide all comments if they are hidden for a proper REPLY mode
1072 var $filediff = $node.closest('.filediff');
1072 var $filediff = $node.closest('.filediff');
1073 $filediff.removeClass('hide-comments');
1073 $filediff.removeClass('hide-comments');
1074
1074
1075 $editForm = self.createNewFormWrapper(f_path, line_no);
1075 $editForm = self.createNewFormWrapper(f_path, line_no);
1076 if(f_path && line_no) {
1076 if(f_path && line_no) {
1077 $editForm.addClass('comment-inline-form-edit')
1077 $editForm.addClass('comment-inline-form-edit')
1078 }
1078 }
1079
1079
1080 $comment.after($editForm)
1080 $comment.after($editForm)
1081
1081
1082 var _form = $($editForm[0]).find('form');
1082 var _form = $($editForm[0]).find('form');
1083 var autocompleteActions = ['as_note',];
1083 var autocompleteActions = ['as_note',];
1084 var commentForm = this.createCommentForm(
1084 var commentForm = this.createCommentForm(
1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
1086 this.edit, comment_id);
1086 this.edit, comment_id);
1087 var old_comment_text_binary = $comment.attr('data-comment-text');
1087 var old_comment_text_binary = $comment.attr('data-comment-text');
1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1089 commentForm.cm.setValue(old_comment_text);
1089 commentForm.cm.setValue(old_comment_text);
1090 $comment.hide();
1090 $comment.hide();
1091 tooltipActivate();
1091 tooltipActivate();
1092
1092
1093 // set a CUSTOM submit handler for inline comment edit action.
1093 // set a CUSTOM submit handler for inline comment edit action.
1094 commentForm.setHandleFormSubmit(function(o) {
1094 commentForm.setHandleFormSubmit(function(o) {
1095 var text = commentForm.cm.getValue();
1095 var text = commentForm.cm.getValue();
1096 var commentType = commentForm.getCommentType();
1096 var commentType = commentForm.getCommentType();
1097
1097
1098 if (text === "") {
1098 if (text === "") {
1099 return;
1099 return;
1100 }
1100 }
1101
1101
1102 if (old_comment_text == text) {
1102 if (old_comment_text == text) {
1103 SwalNoAnimation.fire({
1103 SwalNoAnimation.fire({
1104 title: 'Unable to edit comment',
1104 title: 'Unable to edit comment',
1105 html: _gettext('Comment body was not changed.'),
1105 html: _gettext('Comment body was not changed.'),
1106 });
1106 });
1107 return;
1107 return;
1108 }
1108 }
1109 var excludeCancelBtn = false;
1109 var excludeCancelBtn = false;
1110 var submitEvent = true;
1110 var submitEvent = true;
1111 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1111 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1112 commentForm.cm.setOption("readOnly", true);
1112 commentForm.cm.setOption("readOnly", true);
1113
1113
1114 // Read last version known
1114 // Read last version known
1115 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1115 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1116 var version = versionSelector.data('lastVersion');
1116 var version = versionSelector.data('lastVersion');
1117
1117
1118 if (!version) {
1118 if (!version) {
1119 version = 0;
1119 version = 0;
1120 }
1120 }
1121
1121
1122 var postData = {
1122 var postData = {
1123 'text': text,
1123 'text': text,
1124 'f_path': f_path,
1124 'f_path': f_path,
1125 'line': line_no,
1125 'line': line_no,
1126 'comment_type': commentType,
1126 'comment_type': commentType,
1127 'draft': isDraft,
1127 'draft': isDraft,
1128 'version': version,
1128 'version': version,
1129 'csrf_token': CSRF_TOKEN
1129 'csrf_token': CSRF_TOKEN
1130 };
1130 };
1131
1131
1132 var submitSuccessCallback = function(json_data) {
1132 var submitSuccessCallback = function(json_data) {
1133 $editForm.remove();
1133 $editForm.remove();
1134 $comment.show();
1134 $comment.show();
1135 var postData = {
1135 var postData = {
1136 'text': text,
1136 'text': text,
1137 'renderer': $comment.attr('data-comment-renderer'),
1137 'renderer': $comment.attr('data-comment-renderer'),
1138 'csrf_token': CSRF_TOKEN
1138 'csrf_token': CSRF_TOKEN
1139 };
1139 };
1140
1140
1141 /* Inject new edited version selector */
1141 /* Inject new edited version selector */
1142 var updateCommentVersionDropDown = function () {
1142 var updateCommentVersionDropDown = function () {
1143 var versionSelectId = '#comment_versions_'+comment_id;
1143 var versionSelectId = '#comment_versions_'+comment_id;
1144 var preLoadVersionData = [
1144 var preLoadVersionData = [
1145 {
1145 {
1146 id: json_data['comment_version'],
1146 id: json_data['comment_version'],
1147 text: "v{0}".format(json_data['comment_version']),
1147 text: "v{0}".format(json_data['comment_version']),
1148 action: function () {
1148 action: function () {
1149 Rhodecode.comments.showVersion(
1149 Rhodecode.comments.showVersion(
1150 json_data['comment_id'],
1150 json_data['comment_id'],
1151 json_data['comment_history_id']
1151 json_data['comment_history_id']
1152 )
1152 )
1153 },
1153 },
1154 comment_version: json_data['comment_version'],
1154 comment_version: json_data['comment_version'],
1155 comment_author_username: json_data['comment_author_username'],
1155 comment_author_username: json_data['comment_author_username'],
1156 comment_author_gravatar: json_data['comment_author_gravatar'],
1156 comment_author_gravatar: json_data['comment_author_gravatar'],
1157 comment_created_on: json_data['comment_created_on'],
1157 comment_created_on: json_data['comment_created_on'],
1158 },
1158 },
1159 ]
1159 ]
1160
1160
1161
1161
1162 if ($(versionSelectId).data('select2')) {
1162 if ($(versionSelectId).data('select2')) {
1163 var oldData = $(versionSelectId).data('select2').opts.data.results;
1163 var oldData = $(versionSelectId).data('select2').opts.data.results;
1164 $(versionSelectId).select2("destroy");
1164 $(versionSelectId).select2("destroy");
1165 preLoadVersionData = oldData.concat(preLoadVersionData)
1165 preLoadVersionData = oldData.concat(preLoadVersionData)
1166 }
1166 }
1167
1167
1168 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1168 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1169
1169
1170 $comment.attr('data-comment-text', utf8ToB64(text));
1170 $comment.attr('data-comment-text', utf8ToB64(text));
1171
1171
1172 var versionSelector = $('#comment_versions_'+comment_id);
1172 var versionSelector = $('#comment_versions_'+comment_id);
1173
1173
1174 // set lastVersion so we know our last edit version
1174 // set lastVersion so we know our last edit version
1175 versionSelector.data('lastVersion', json_data['comment_version'])
1175 versionSelector.data('lastVersion', json_data['comment_version'])
1176 versionSelector.parent().show();
1176 versionSelector.parent().show();
1177 }
1177 }
1178 updateCommentVersionDropDown();
1178 updateCommentVersionDropDown();
1179
1179
1180 // by default we reset state of comment preserving the text
1180 // by default we reset state of comment preserving the text
1181 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1181 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1182 var prefix = "Error while editing this comment.\n"
1182 var prefix = "Error while editing this comment.\n"
1183 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1183 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1184 ajaxErrorSwal(message);
1184 ajaxErrorSwal(message);
1185 };
1185 };
1186
1186
1187 var successRenderCommit = function(o){
1187 var successRenderCommit = function(o){
1188 $comment.show();
1188 $comment.show();
1189 $comment[0].lastElementChild.innerHTML = o;
1189 $comment[0].lastElementChild.innerHTML = o;
1190 };
1190 };
1191
1191
1192 var previewUrl = pyroutes.url(
1192 var previewUrl = pyroutes.url(
1193 'repo_commit_comment_preview',
1193 'repo_commit_comment_preview',
1194 {'repo_name': templateContext.repo_name,
1194 {'repo_name': templateContext.repo_name,
1195 'commit_id': templateContext.commit_data.commit_id});
1195 'commit_id': templateContext.commit_data.commit_id});
1196
1196
1197 _submitAjaxPOST(
1197 _submitAjaxPOST(
1198 previewUrl, postData, successRenderCommit, failRenderCommit
1198 previewUrl, postData, successRenderCommit, failRenderCommit
1199 );
1199 );
1200
1200
1201 try {
1201 try {
1202 var html = json_data.rendered_text;
1202 var html = json_data.rendered_text;
1203 var lineno = json_data.line_no;
1203 var lineno = json_data.line_no;
1204 var target_id = json_data.target_id;
1204 var target_id = json_data.target_id;
1205
1205
1206 $comments.find('.cb-comment-add-button').before(html);
1206 $comments.find('.cb-comment-add-button').before(html);
1207
1207
1208 // run global callback on submit
1208 // run global callback on submit
1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1210
1210
1211 } catch (e) {
1211 } catch (e) {
1212 console.error(e);
1212 console.error(e);
1213 }
1213 }
1214
1214
1215 // re trigger the linkification of next/prev navigation
1215 // re trigger the linkification of next/prev navigation
1216 linkifyComments($('.inline-comment-injected'));
1216 linkifyComments($('.inline-comment-injected'));
1217 timeagoActivate();
1217 timeagoActivate();
1218 tooltipActivate();
1218 tooltipActivate();
1219
1219
1220 if (window.updateSticky !== undefined) {
1220 if (window.updateSticky !== undefined) {
1221 // potentially our comments change the active window size, so we
1221 // potentially our comments change the active window size, so we
1222 // notify sticky elements
1222 // notify sticky elements
1223 updateSticky()
1223 updateSticky()
1224 }
1224 }
1225
1225
1226 if (window.refreshAllComments !== undefined && !isDraft) {
1226 if (window.refreshAllComments !== undefined && !isDraft) {
1227 // if we have this handler, run it, and refresh all comments boxes
1227 // if we have this handler, run it, and refresh all comments boxes
1228 refreshAllComments()
1228 refreshAllComments()
1229 }
1229 }
1230 else if (window.refreshDraftComments !== undefined && isDraft) {
1230 else if (window.refreshDraftComments !== undefined && isDraft) {
1231 // if we have this handler, run it, and refresh all comments boxes
1231 // if we have this handler, run it, and refresh all comments boxes
1232 refreshDraftComments();
1232 refreshDraftComments();
1233 }
1233 }
1234
1234
1235 commentForm.setActionButtonsDisabled(false);
1235 commentForm.setActionButtonsDisabled(false);
1236
1236
1237 };
1237 };
1238
1238
1239 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1239 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1240 var prefix = "Error while editing comment.\n"
1240 var prefix = "Error while editing comment.\n"
1241 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1241 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1242 if (jqXHR.status == 409){
1242 if (jqXHR.status == 409){
1243 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1243 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1244 ajaxErrorSwal(message, 'Comment version mismatch.');
1244 ajaxErrorSwal(message, 'Comment version mismatch.');
1245 } else {
1245 } else {
1246 ajaxErrorSwal(message);
1246 ajaxErrorSwal(message);
1247 }
1247 }
1248
1248
1249 commentForm.resetCommentFormState(text)
1249 commentForm.resetCommentFormState(text)
1250 };
1250 };
1251 commentForm.submitAjaxPOST(
1251 commentForm.submitAjaxPOST(
1252 commentForm.submitUrl, postData,
1252 commentForm.submitUrl, postData,
1253 submitSuccessCallback,
1253 submitSuccessCallback,
1254 submitFailCallback);
1254 submitFailCallback);
1255 });
1255 });
1256 }
1256 }
1257
1257
1258 $editForm.addClass('comment-inline-form-open');
1258 $editForm.addClass('comment-inline-form-open');
1259 };
1259 };
1260
1260
1261 this.attachComment = function(json_data) {
1261 this.attachComment = function(json_data) {
1262 var self = this;
1262 var self = this;
1263 $.each(json_data, function(idx, val) {
1263 $.each(json_data, function(idx, val) {
1264 var json_data_elem = [val]
1264 var json_data_elem = [val]
1265 var isInline = val.comment_f_path && val.comment_lineno
1265 var isInline = val.comment_f_path && val.comment_lineno
1266
1266
1267 if (isInline) {
1267 if (isInline) {
1268 self.attachInlineComment(json_data_elem)
1268 self.attachInlineComment(json_data_elem)
1269 } else {
1269 } else {
1270 self.attachGeneralComment(json_data_elem)
1270 self.attachGeneralComment(json_data_elem)
1271 }
1271 }
1272 })
1272 })
1273
1273
1274 }
1274 }
1275
1275
1276 this.attachGeneralComment = function(json_data) {
1276 this.attachGeneralComment = function(json_data) {
1277 $.each(json_data, function(idx, val) {
1277 $.each(json_data, function(idx, val) {
1278 $('#injected_page_comments').append(val.rendered_text);
1278 $('#injected_page_comments').append(val.rendered_text);
1279 })
1279 })
1280 }
1280 }
1281
1281
1282 this.attachInlineComment = function(json_data) {
1282 this.attachInlineComment = function(json_data) {
1283
1283
1284 $.each(json_data, function (idx, val) {
1284 $.each(json_data, function (idx, val) {
1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1286 var html = val.rendered_text;
1286 var html = val.rendered_text;
1287 var $inlineComments = $('#' + val.target_id)
1287 var $inlineComments = $('#' + val.target_id)
1288 .find(line_qry)
1288 .find(line_qry)
1289 .find('.inline-comments');
1289 .find('.inline-comments');
1290
1290
1291 var lastComment = $inlineComments.find('.comment-inline').last();
1291 var lastComment = $inlineComments.find('.comment-inline').last();
1292
1292
1293 if (lastComment.length === 0) {
1293 if (lastComment.length === 0) {
1294 // first comment, we append simply
1294 // first comment, we append simply
1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1296 } else {
1296 } else {
1297 $(lastComment).after(html)
1297 $(lastComment).after(html)
1298 }
1298 }
1299
1299
1300 })
1300 })
1301
1301
1302 };
1302 };
1303
1303
1304 this.createNewFormWrapper = function(f_path, line_no) {
1304 this.createNewFormWrapper = function(f_path, line_no) {
1305 // create a new reply HTML form from template
1305 // create a new reply HTML form from template
1306 var tmpl = $('#cb-comment-inline-form-template').html();
1306 var tmpl = $('#cb-comment-inline-form-template').html();
1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1308 return $(tmpl);
1308 return $(tmpl);
1309 }
1309 }
1310
1310
1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1312 self.edit = false;
1312 self.edit = false;
1313 var $node = $(node);
1313 var $node = $(node);
1314 var $td = $node.closest('td');
1314 var $td = $node.closest('td');
1315 var resolvesCommentId = resolutionComment || null;
1315 var resolvesCommentId = resolutionComment || null;
1316
1316
1317 var $replyForm = $td.find('.comment-inline-form');
1317 var $replyForm = $td.find('.comment-inline-form');
1318
1318
1319 // if form isn't existing, we're generating a new one and injecting it.
1319 // if form isn't existing, we're generating a new one and injecting it.
1320 if ($replyForm.length === 0) {
1320 if ($replyForm.length === 0) {
1321
1321
1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1323 self.toggleLineComments($node, true);
1323 self.toggleLineComments($node, true);
1324
1324
1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1326
1326
1327 var $comments = $td.find('.inline-comments');
1327 var $comments = $td.find('.inline-comments');
1328
1328
1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1330 if ($comments.length===0) {
1330 if ($comments.length===0) {
1331 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1331 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1332 var $reply_container = $('#cb-comments-inline-container-template')
1332 var $reply_container = $('#cb-comments-inline-container-template')
1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1334 $td.append($($reply_container).html());
1334 $td.append($($reply_container).html());
1335 }
1335 }
1336
1336
1337 // default comment button exists, so we prepend the form for leaving initial comment
1337 // default comment button exists, so we prepend the form for leaving initial comment
1338 $td.find('.cb-comment-add-button').before($replyForm);
1338 $td.find('.cb-comment-add-button').before($replyForm);
1339 // set marker, that we have a open form
1339 // set marker, that we have a open form
1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1341 $replyWrapper.addClass('comment-form-active');
1341 $replyWrapper.addClass('comment-form-active');
1342
1342
1343 var lastComment = $comments.find('.comment-inline').last();
1343 var lastComment = $comments.find('.comment-inline').last();
1344 if ($(lastComment).hasClass('comment-outdated')) {
1344 if ($(lastComment).hasClass('comment-outdated')) {
1345 $replyWrapper.show();
1345 $replyWrapper.show();
1346 }
1346 }
1347
1347
1348 var _form = $($replyForm[0]).find('form');
1348 var _form = $($replyForm[0]).find('form');
1349 var autocompleteActions = ['as_note', 'as_todo'];
1349 var autocompleteActions = ['as_note', 'as_todo'];
1350 var comment_id=null;
1350 var comment_id=null;
1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1352 var commentForm = self.createCommentForm(
1352 var commentForm = self.createCommentForm(
1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1354 self.edit, comment_id);
1354 self.edit, comment_id);
1355
1355
1356 // set a CUSTOM submit handler for inline comments.
1356 // set a CUSTOM submit handler for inline comments.
1357 commentForm.setHandleFormSubmit(function(o) {
1357 commentForm.setHandleFormSubmit(function(o) {
1358 var text = commentForm.cm.getValue();
1358 var text = commentForm.cm.getValue();
1359 var commentType = commentForm.getCommentType();
1359 var commentType = commentForm.getCommentType();
1360 var resolvesCommentId = commentForm.getResolvesId();
1360 var resolvesCommentId = commentForm.getResolvesId();
1361 var isDraft = commentForm.getDraftState();
1361 var isDraft = commentForm.getDraftState();
1362
1362
1363 if (text === "") {
1363 if (text === "") {
1364 return;
1364 return;
1365 }
1365 }
1366
1366
1367 if (line_no === undefined) {
1367 if (line_no === undefined) {
1368 alert('Error: unable to fetch line number for this inline comment !');
1368 alert('Error: unable to fetch line number for this inline comment !');
1369 return;
1369 return;
1370 }
1370 }
1371
1371
1372 if (f_path === undefined) {
1372 if (f_path === undefined) {
1373 alert('Error: unable to fetch file path for this inline comment !');
1373 alert('Error: unable to fetch file path for this inline comment !');
1374 return;
1374 return;
1375 }
1375 }
1376
1376
1377 var excludeCancelBtn = false;
1377 var excludeCancelBtn = false;
1378 var submitEvent = true;
1378 var submitEvent = true;
1379 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1379 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1380 commentForm.cm.setOption("readOnly", true);
1380 commentForm.cm.setOption("readOnly", true);
1381 var postData = {
1381 var postData = {
1382 'text': text,
1382 'text': text,
1383 'f_path': f_path,
1383 'f_path': f_path,
1384 'line': line_no,
1384 'line': line_no,
1385 'comment_type': commentType,
1385 'comment_type': commentType,
1386 'draft': isDraft,
1386 'draft': isDraft,
1387 'csrf_token': CSRF_TOKEN
1387 'csrf_token': CSRF_TOKEN
1388 };
1388 };
1389 if (resolvesCommentId){
1389 if (resolvesCommentId){
1390 postData['resolves_comment_id'] = resolvesCommentId;
1390 postData['resolves_comment_id'] = resolvesCommentId;
1391 }
1391 }
1392
1392
1393 // submitSuccess for inline commits
1393 // submitSuccess for inline commits
1394 var submitSuccessCallback = function(json_data) {
1394 var submitSuccessCallback = function(json_data) {
1395
1395
1396 $replyForm.remove();
1396 $replyForm.remove();
1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1398
1398
1399 try {
1399 try {
1400
1400
1401 // inject newly created comments, json_data is {<comment_id>: {}}
1401 // inject newly created comments, json_data is {<comment_id>: {}}
1402 self.attachInlineComment(json_data)
1402 self.attachInlineComment(json_data)
1403
1403
1404 //mark visually which comment was resolved
1404 //mark visually which comment was resolved
1405 if (resolvesCommentId) {
1405 if (resolvesCommentId) {
1406 commentForm.markCommentResolved(resolvesCommentId);
1406 commentForm.markCommentResolved(resolvesCommentId);
1407 }
1407 }
1408
1408
1409 // run global callback on submit
1409 // run global callback on submit
1410 commentForm.globalSubmitSuccessCallback({
1410 commentForm.globalSubmitSuccessCallback({
1411 draft: isDraft,
1411 draft: isDraft,
1412 comment_id: comment_id
1412 comment_id: comment_id
1413 });
1413 });
1414
1414
1415 } catch (e) {
1415 } catch (e) {
1416 console.error(e);
1416 console.error(e);
1417 }
1417 }
1418
1418
1419 if (window.updateSticky !== undefined) {
1419 if (window.updateSticky !== undefined) {
1420 // potentially our comments change the active window size, so we
1420 // potentially our comments change the active window size, so we
1421 // notify sticky elements
1421 // notify sticky elements
1422 updateSticky()
1422 updateSticky()
1423 }
1423 }
1424
1424
1425 if (window.refreshAllComments !== undefined && !isDraft) {
1425 if (window.refreshAllComments !== undefined && !isDraft) {
1426 // if we have this handler, run it, and refresh all comments boxes
1426 // if we have this handler, run it, and refresh all comments boxes
1427 refreshAllComments()
1427 refreshAllComments()
1428 }
1428 }
1429 else if (window.refreshDraftComments !== undefined && isDraft) {
1429 else if (window.refreshDraftComments !== undefined && isDraft) {
1430 // if we have this handler, run it, and refresh all comments boxes
1430 // if we have this handler, run it, and refresh all comments boxes
1431 refreshDraftComments();
1431 refreshDraftComments();
1432 }
1432 }
1433
1433
1434 commentForm.setActionButtonsDisabled(false);
1434 commentForm.setActionButtonsDisabled(false);
1435
1435
1436 // re trigger the linkification of next/prev navigation
1436 // re trigger the linkification of next/prev navigation
1437 linkifyComments($('.inline-comment-injected'));
1437 linkifyComments($('.inline-comment-injected'));
1438 timeagoActivate();
1438 timeagoActivate();
1439 tooltipActivate();
1439 tooltipActivate();
1440 };
1440 };
1441
1441
1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1443 var prefix = "Error while submitting comment.\n"
1443 var prefix = "Error while submitting comment.\n"
1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1445 ajaxErrorSwal(message);
1445 ajaxErrorSwal(message);
1446 commentForm.resetCommentFormState(text)
1446 commentForm.resetCommentFormState(text)
1447 };
1447 };
1448
1448
1449 commentForm.submitAjaxPOST(
1449 commentForm.submitAjaxPOST(
1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1451 });
1451 });
1452 }
1452 }
1453
1453
1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1455 $replyForm.addClass('comment-inline-form-open');
1455 $replyForm.addClass('comment-inline-form-open');
1456 tooltipActivate();
1456 tooltipActivate();
1457 };
1457 };
1458
1458
1459 this.createResolutionComment = function(commentId){
1459 this.createResolutionComment = function(commentId){
1460 // hide the trigger text
1460 // hide the trigger text
1461 $('#resolve-comment-{0}'.format(commentId)).hide();
1461 $('#resolve-comment-{0}'.format(commentId)).hide();
1462
1462
1463 var comment = $('#comment-'+commentId);
1463 var comment = $('#comment-'+commentId);
1464 var commentData = comment.data();
1464 var commentData = comment.data();
1465 console.log(commentData);
1466
1465
1467 if (commentData.commentInline) {
1466 if (commentData.commentInline) {
1468 var f_path = commentData.commentFPath;
1467 var f_path = commentData.commentFPath;
1469 var line_no = commentData.commentLineNo;
1468 var line_no = commentData.commentLineNo;
1470 this.createComment(comment, f_path, line_no, commentId)
1469 this.createComment(comment, f_path, line_no, commentId)
1471 } else {
1470 } else {
1472 this.createGeneralComment('general', "$placeholder", commentId)
1471 this.createGeneralComment('general', "$placeholder", commentId)
1473 }
1472 }
1474
1473
1475 return false;
1474 return false;
1476 };
1475 };
1477
1476
1478 this.submitResolution = function(commentId){
1477 this.submitResolution = function(commentId){
1479 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1478 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1480 var commentForm = form.get(0).CommentForm;
1479 var commentForm = form.get(0).CommentForm;
1481
1480
1482 var cm = commentForm.getCmInstance();
1481 var cm = commentForm.getCmInstance();
1483 var renderer = templateContext.visual.default_renderer;
1482 var renderer = templateContext.visual.default_renderer;
1484 if (renderer == 'rst'){
1483 if (renderer == 'rst'){
1485 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1484 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1486 } else if (renderer == 'markdown') {
1485 } else if (renderer == 'markdown') {
1487 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1486 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1488 } else {
1487 } else {
1489 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1488 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1490 }
1489 }
1491
1490
1492 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1491 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1493 form.submit();
1492 form.submit();
1494 return false;
1493 return false;
1495 };
1494 };
1496
1495
1497 };
1496 };
1498
1497
1499 window.commentHelp = function(renderer) {
1498 window.commentHelp = function(renderer) {
1500 var funcData = {'renderer': renderer}
1499 var funcData = {'renderer': renderer}
1501 return renderTemplate('commentHelpHovercard', funcData)
1500 return renderTemplate('commentHelpHovercard', funcData)
1502 } No newline at end of file
1501 }
@@ -1,206 +1,205 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'repo_name': repo_name,
10 'repo_name': repo_name,
11 'status': status_change,
11 'status': status_change,
12 'comment_file': comment_file,
12 'comment_file': comment_file,
13 'comment_line': comment_line,
13 'comment_line': comment_line,
14 'comment_type': comment_type,
14 'comment_type': comment_type,
15 'comment_id': comment_id,
15 'comment_id': comment_id,
16
16
17 'pr_title': pull_request.title,
17 'pr_title': pull_request.title_safe,
18 'pr_id': pull_request.pull_request_id,
18 'pr_id': pull_request.pull_request_id,
19 'mention_prefix': '[mention] ' if mention else '',
19 'mention_prefix': '[mention] ' if mention else '',
20 }
20 }
21
21
22 if comment_file:
22 if comment_file:
23 subject_template = email_pr_comment_file_subject_template or \
23 subject_template = email_pr_comment_file_subject_template or \
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
25 else:
25 else:
26 if status_change:
26 if status_change:
27 subject_template = email_pr_comment_status_change_subject_template or \
27 subject_template = email_pr_comment_status_change_subject_template or \
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
29 else:
29 else:
30 subject_template = email_pr_comment_subject_template or \
30 subject_template = email_pr_comment_subject_template or \
31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
32 %>
32 %>
33
33
34
35 ${subject_template.format(**data) |n}
34 ${subject_template.format(**data) |n}
36 </%def>
35 </%def>
37
36
38 ## PLAINTEXT VERSION OF BODY
37 ## PLAINTEXT VERSION OF BODY
39 <%def name="body_plaintext()" filter="n,trim">
38 <%def name="body_plaintext()" filter="n,trim">
40 <%
39 <%
41 data = {
40 data = {
42 'user': h.person(user),
41 'user': h.person(user),
43 'repo_name': repo_name,
42 'repo_name': repo_name,
44 'status': status_change,
43 'status': status_change,
45 'comment_file': comment_file,
44 'comment_file': comment_file,
46 'comment_line': comment_line,
45 'comment_line': comment_line,
47 'comment_type': comment_type,
46 'comment_type': comment_type,
48 'comment_id': comment_id,
47 'comment_id': comment_id,
49
48
50 'pr_title': pull_request.title,
49 'pr_title': pull_request.title_safe,
51 'pr_id': pull_request.pull_request_id,
50 'pr_id': pull_request.pull_request_id,
52 'source_ref_type': pull_request.source_ref_parts.type,
51 'source_ref_type': pull_request.source_ref_parts.type,
53 'source_ref_name': pull_request.source_ref_parts.name,
52 'source_ref_name': pull_request.source_ref_parts.name,
54 'target_ref_type': pull_request.target_ref_parts.type,
53 'target_ref_type': pull_request.target_ref_parts.type,
55 'target_ref_name': pull_request.target_ref_parts.name,
54 'target_ref_name': pull_request.target_ref_parts.name,
56 'source_repo': pull_request_source_repo.repo_name,
55 'source_repo': pull_request_source_repo.repo_name,
57 'target_repo': pull_request_target_repo.repo_name,
56 'target_repo': pull_request_target_repo.repo_name,
58 'source_repo_url': pull_request_source_repo_url,
57 'source_repo_url': pull_request_source_repo_url,
59 'target_repo_url': pull_request_target_repo_url,
58 'target_repo_url': pull_request_target_repo_url,
60 }
59 }
61 %>
60 %>
62
61
63 * ${_('Comment link')}: ${pr_comment_url}
62 * ${_('Comment link')}: ${pr_comment_url}
64
63
65 * ${_('Pull Request')}: !${pull_request.pull_request_id}
64 * ${_('Pull Request')}: !${pull_request.pull_request_id}
66
65
67 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
66 * ${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))}
68
67
69 %if status_change and not closing_pr:
68 %if status_change and not closing_pr:
70 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
69 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
71
70
72 %elif status_change and closing_pr:
71 %elif status_change and closing_pr:
73 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
72 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
74
73
75 %endif
74 %endif
76 %if comment_file:
75 %if comment_file:
77 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
76 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
78
77
79 %endif
78 %endif
80 % if comment_type == 'todo':
79 % if comment_type == 'todo':
81 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
80 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
82 % else:
81 % else:
83 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
82 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
84 % endif
83 % endif
85
84
86 ${comment_body |n, trim}
85 ${comment_body |n, trim}
87
86
88 ---
87 ---
89 ${self.plaintext_footer()}
88 ${self.plaintext_footer()}
90 </%def>
89 </%def>
91
90
92
91
93 <%
92 <%
94 data = {
93 data = {
95 'user': h.person(user),
94 'user': h.person(user),
96 'comment_file': comment_file,
95 'comment_file': comment_file,
97 'comment_line': comment_line,
96 'comment_line': comment_line,
98 'comment_type': comment_type,
97 'comment_type': comment_type,
99 'comment_id': comment_id,
98 'comment_id': comment_id,
100 'renderer_type': renderer_type or 'plain',
99 'renderer_type': renderer_type or 'plain',
101
100
102 'pr_title': pull_request.title,
101 'pr_title': pull_request.title_safe,
103 'pr_id': pull_request.pull_request_id,
102 'pr_id': pull_request.pull_request_id,
104 'status': status_change,
103 'status': status_change,
105 'source_ref_type': pull_request.source_ref_parts.type,
104 'source_ref_type': pull_request.source_ref_parts.type,
106 'source_ref_name': pull_request.source_ref_parts.name,
105 'source_ref_name': pull_request.source_ref_parts.name,
107 'target_ref_type': pull_request.target_ref_parts.type,
106 'target_ref_type': pull_request.target_ref_parts.type,
108 'target_ref_name': pull_request.target_ref_parts.name,
107 'target_ref_name': pull_request.target_ref_parts.name,
109 'source_repo': pull_request_source_repo.repo_name,
108 'source_repo': pull_request_source_repo.repo_name,
110 'target_repo': pull_request_target_repo.repo_name,
109 'target_repo': pull_request_target_repo.repo_name,
111 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
110 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
112 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
111 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
113 }
112 }
114 %>
113 %>
115
114
116 ## header
115 ## header
117 <table style="text-align:left;vertical-align:middle;width: 100%">
116 <table style="text-align:left;vertical-align:middle;width: 100%">
118 <tr>
117 <tr>
119 <td style="width:100%;border-bottom:1px solid #dbd9da;">
118 <td style="width:100%;border-bottom:1px solid #dbd9da;">
120
119
121 <div style="margin: 0; font-weight: bold">
120 <div style="margin: 0; font-weight: bold">
122 <div class="clear-both" style="margin-bottom: 4px">
121 <div class="clear-both" style="margin-bottom: 4px">
123 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
122 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
124 ${_('left a')}
123 ${_('left a')}
125 <a href="${pr_comment_url}" style="${base.link_css()}">
124 <a href="${pr_comment_url}" style="${base.link_css()}">
126 % if comment_file:
125 % if comment_file:
127 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
126 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
128 % else:
127 % else:
129 ${_('{comment_type} on pull request.').format(**data) |n}
128 ${_('{comment_type} on pull request.').format(**data) |n}
130 % endif
129 % endif
131 </a>
130 </a>
132 </div>
131 </div>
133 <div style="margin-top: 10px"></div>
132 <div style="margin-top: 10px"></div>
134 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
133 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
135 </div>
134 </div>
136
135
137 </td>
136 </td>
138 </tr>
137 </tr>
139
138
140 </table>
139 </table>
141 <div class="clear-both"></div>
140 <div class="clear-both"></div>
142 ## main body
141 ## main body
143 <table style="text-align:left;vertical-align:middle;width: 100%">
142 <table style="text-align:left;vertical-align:middle;width: 100%">
144
143
145 ## spacing def
144 ## spacing def
146 <tr>
145 <tr>
147 <td style="width: 130px"></td>
146 <td style="width: 130px"></td>
148 <td></td>
147 <td></td>
149 </tr>
148 </tr>
150
149
151 % if status_change:
150 % if status_change:
152 <tr>
151 <tr>
153 <td style="padding-right:20px;">${_('Review Status')}:</td>
152 <td style="padding-right:20px;">${_('Review Status')}:</td>
154 <td>
153 <td>
155 % if closing_pr:
154 % if closing_pr:
156 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
155 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
157 % else:
156 % else:
158 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
157 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
159 % endif
158 % endif
160 </td>
159 </td>
161 </tr>
160 </tr>
162 % endif
161 % endif
163 <tr>
162 <tr>
164 <td style="padding-right:20px;">${_('Pull request')}:</td>
163 <td style="padding-right:20px;">${_('Pull request')}:</td>
165 <td>
164 <td>
166 <a href="${pull_request_url}" style="${base.link_css()}">
165 <a href="${pull_request_url}" style="${base.link_css()}">
167 !${pull_request.pull_request_id}
166 !${pull_request.pull_request_id}
168 </a>
167 </a>
169 </td>
168 </td>
170 </tr>
169 </tr>
171
170
172 <tr>
171 <tr>
173 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
172 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
174 <td style="line-height:20px;">
173 <td style="line-height:20px;">
175 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
174 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
176 &rarr;
175 &rarr;
177 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
176 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
178 </td>
177 </td>
179 </tr>
178 </tr>
180
179
181 % if comment_file:
180 % if comment_file:
182 <tr>
181 <tr>
183 <td style="padding-right:20px;">${_('File')}:</td>
182 <td style="padding-right:20px;">${_('File')}:</td>
184 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
183 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
185 </tr>
184 </tr>
186 % endif
185 % endif
187
186
188 <tr style="border-bottom:1px solid #dbd9da;">
187 <tr style="border-bottom:1px solid #dbd9da;">
189 <td colspan="2" style="padding-right:20px;">
188 <td colspan="2" style="padding-right:20px;">
190 % if comment_type == 'todo':
189 % if comment_type == 'todo':
191 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
190 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
192 % else:
191 % else:
193 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
192 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
194 % endif
193 % endif
195 </td>
194 </td>
196 </tr>
195 </tr>
197
196
198 <tr>
197 <tr>
199 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
198 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
200 </tr>
199 </tr>
201
200
202 <tr>
201 <tr>
203 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
202 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
204 <td></td>
203 <td></td>
205 </tr>
204 </tr>
206 </table>
205 </table>
@@ -1,154 +1,154 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'pr_id': pull_request.pull_request_id,
10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title,
11 'pr_title': pull_request.title_safe,
12 }
12 }
13
13
14 if user_role == 'observer':
14 if user_role == 'observer':
15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
16 else:
16 else:
17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
18 %>
18 %>
19
19
20 ${subject_template.format(**data) |n}
20 ${subject_template.format(**data) |n}
21 </%def>
21 </%def>
22
22
23 ## PLAINTEXT VERSION OF BODY
23 ## PLAINTEXT VERSION OF BODY
24 <%def name="body_plaintext()" filter="n,trim">
24 <%def name="body_plaintext()" filter="n,trim">
25 <%
25 <%
26 data = {
26 data = {
27 'user': h.person(user),
27 'user': h.person(user),
28 'pr_id': pull_request.pull_request_id,
28 'pr_id': pull_request.pull_request_id,
29 'pr_title': pull_request.title,
29 'pr_title': pull_request.title_safe,
30 'source_ref_type': pull_request.source_ref_parts.type,
30 'source_ref_type': pull_request.source_ref_parts.type,
31 'source_ref_name': pull_request.source_ref_parts.name,
31 'source_ref_name': pull_request.source_ref_parts.name,
32 'target_ref_type': pull_request.target_ref_parts.type,
32 'target_ref_type': pull_request.target_ref_parts.type,
33 'target_ref_name': pull_request.target_ref_parts.name,
33 'target_ref_name': pull_request.target_ref_parts.name,
34 'repo_url': pull_request_source_repo_url,
34 'repo_url': pull_request_source_repo_url,
35 'source_repo': pull_request_source_repo.repo_name,
35 'source_repo': pull_request_source_repo.repo_name,
36 'target_repo': pull_request_target_repo.repo_name,
36 'target_repo': pull_request_target_repo.repo_name,
37 'source_repo_url': pull_request_source_repo_url,
37 'source_repo_url': pull_request_source_repo_url,
38 'target_repo_url': pull_request_target_repo_url,
38 'target_repo_url': pull_request_target_repo_url,
39 }
39 }
40
40
41 %>
41 %>
42
42
43 * ${_('Pull Request link')}: ${pull_request_url}
43 * ${_('Pull Request link')}: ${pull_request_url}
44
44
45 * ${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))}
45 * ${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
46
47 * ${_('Title')}: ${pull_request.title}
47 * ${_('Title')}: ${pull_request.title}
48
48
49 * ${_('Description')}:
49 * ${_('Description')}:
50
50
51 ${pull_request.description | trim}
51 ${pull_request.description | trim}
52
52
53
53
54 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
54 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
55
55
56 % for commit_id, message in pull_request_commits:
56 % for commit_id, message in pull_request_commits:
57 - ${h.short_id(commit_id)}
57 - ${h.short_id(commit_id)}
58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
59
59
60 % endfor
60 % endfor
61
61
62 ---
62 ---
63 ${self.plaintext_footer()}
63 ${self.plaintext_footer()}
64 </%def>
64 </%def>
65 <%
65 <%
66 data = {
66 data = {
67 'user': h.person(user),
67 'user': h.person(user),
68 'pr_id': pull_request.pull_request_id,
68 'pr_id': pull_request.pull_request_id,
69 'pr_title': pull_request.title,
69 'pr_title': pull_request.title_safe,
70 'source_ref_type': pull_request.source_ref_parts.type,
70 'source_ref_type': pull_request.source_ref_parts.type,
71 'source_ref_name': pull_request.source_ref_parts.name,
71 'source_ref_name': pull_request.source_ref_parts.name,
72 'target_ref_type': pull_request.target_ref_parts.type,
72 'target_ref_type': pull_request.target_ref_parts.type,
73 'target_ref_name': pull_request.target_ref_parts.name,
73 'target_ref_name': pull_request.target_ref_parts.name,
74 'repo_url': pull_request_source_repo_url,
74 'repo_url': pull_request_source_repo_url,
75 'source_repo': pull_request_source_repo.repo_name,
75 'source_repo': pull_request_source_repo.repo_name,
76 'target_repo': pull_request_target_repo.repo_name,
76 'target_repo': pull_request_target_repo.repo_name,
77 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
77 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
78 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
78 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
79 }
79 }
80 %>
80 %>
81 ## header
81 ## header
82 <table style="text-align:left;vertical-align:middle;width: 100%">
82 <table style="text-align:left;vertical-align:middle;width: 100%">
83 <tr>
83 <tr>
84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
85 <div style="margin: 0; font-weight: bold">
85 <div style="margin: 0; font-weight: bold">
86 % if user_role == 'observer':
86 % if user_role == 'observer':
87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
89 ${_('added you as observer to')}
89 ${_('added you as observer to')}
90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
91 </div>
91 </div>
92 % else:
92 % else:
93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
95 ${_('requested a')}
95 ${_('requested a')}
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
97 </div>
97 </div>
98 % endif
98 % endif
99 <div style="margin-top: 10px"></div>
99 <div style="margin-top: 10px"></div>
100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
101 </div>
101 </div>
102 </td>
102 </td>
103 </tr>
103 </tr>
104
104
105 </table>
105 </table>
106 <div class="clear-both"></div>
106 <div class="clear-both"></div>
107 ## main body
107 ## main body
108 <table style="text-align:left;vertical-align:middle;width: 100%">
108 <table style="text-align:left;vertical-align:middle;width: 100%">
109 ## spacing def
109 ## spacing def
110 <tr>
110 <tr>
111 <td style="width: 130px"></td>
111 <td style="width: 130px"></td>
112 <td></td>
112 <td></td>
113 </tr>
113 </tr>
114
114
115 <tr>
115 <tr>
116 <td style="padding-right:20px;">${_('Pull request')}:</td>
116 <td style="padding-right:20px;">${_('Pull request')}:</td>
117 <td>
117 <td>
118 <a href="${pull_request_url}" style="${base.link_css()}">
118 <a href="${pull_request_url}" style="${base.link_css()}">
119 !${pull_request.pull_request_id}
119 !${pull_request.pull_request_id}
120 </a>
120 </a>
121 </td>
121 </td>
122 </tr>
122 </tr>
123
123
124 <tr>
124 <tr>
125 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
125 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
126 <td style="line-height:20px;">
126 <td style="line-height:20px;">
127 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
127 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
128 &rarr;
128 &rarr;
129 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
129 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
130 </td>
130 </td>
131 </tr>
131 </tr>
132
132
133 <tr>
133 <tr>
134 <td style="padding-right:20px;">${_('Description')}:</td>
134 <td style="padding-right:20px;">${_('Description')}:</td>
135 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
135 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
136 </tr>
136 </tr>
137 <tr>
137 <tr>
138 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
138 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
139 <td></td>
139 <td></td>
140 </tr>
140 </tr>
141
141
142 <tr>
142 <tr>
143 <td colspan="2">
143 <td colspan="2">
144 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
144 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
145 % for commit_id, message in pull_request_commits:
145 % for commit_id, message in pull_request_commits:
146 <li style="margin:0 0 1em;">
146 <li style="margin:0 0 1em;">
147 <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>
147 <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>
148 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
148 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
149 </li>
149 </li>
150 % endfor
150 % endfor
151 </ol>
151 </ol>
152 </td>
152 </td>
153 </tr>
153 </tr>
154 </table>
154 </table>
@@ -1,172 +1,172 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'updating_user': '@'+h.person(updating_user),
9 'updating_user': '@'+h.person(updating_user),
10 'pr_id': pull_request.pull_request_id,
10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title,
11 'pr_title': pull_request.title_safe,
12 }
12 }
13
13
14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
15 %>
15 %>
16
16
17 ${subject_template.format(**data) |n}
17 ${subject_template.format(**data) |n}
18 </%def>
18 </%def>
19
19
20 ## PLAINTEXT VERSION OF BODY
20 ## PLAINTEXT VERSION OF BODY
21 <%def name="body_plaintext()" filter="n,trim">
21 <%def name="body_plaintext()" filter="n,trim">
22 <%
22 <%
23 data = {
23 data = {
24 'updating_user': h.person(updating_user),
24 'updating_user': h.person(updating_user),
25 'pr_id': pull_request.pull_request_id,
25 'pr_id': pull_request.pull_request_id,
26 'pr_title': pull_request.title,
26 'pr_title': pull_request.title_safe,
27 'source_ref_type': pull_request.source_ref_parts.type,
27 'source_ref_type': pull_request.source_ref_parts.type,
28 'source_ref_name': pull_request.source_ref_parts.name,
28 'source_ref_name': pull_request.source_ref_parts.name,
29 'target_ref_type': pull_request.target_ref_parts.type,
29 'target_ref_type': pull_request.target_ref_parts.type,
30 'target_ref_name': pull_request.target_ref_parts.name,
30 'target_ref_name': pull_request.target_ref_parts.name,
31 'repo_url': pull_request_source_repo_url,
31 'repo_url': pull_request_source_repo_url,
32 'source_repo': pull_request_source_repo.repo_name,
32 'source_repo': pull_request_source_repo.repo_name,
33 'target_repo': pull_request_target_repo.repo_name,
33 'target_repo': pull_request_target_repo.repo_name,
34 'source_repo_url': pull_request_source_repo_url,
34 'source_repo_url': pull_request_source_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
36 }
36 }
37 %>
37 %>
38
38
39 * ${_('Pull Request link')}: ${pull_request_url}
39 * ${_('Pull Request link')}: ${pull_request_url}
40
40
41 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
41 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
42
42
43 * ${_('Title')}: ${pull_request.title}
43 * ${_('Title')}: ${pull_request.title}
44
44
45 * ${_('Description')}:
45 * ${_('Description')}:
46
46
47 ${pull_request.description | trim}
47 ${pull_request.description | trim}
48
48
49 * Changed commits:
49 * Changed commits:
50
50
51 - Added: ${len(added_commits)}
51 - Added: ${len(added_commits)}
52 - Removed: ${len(removed_commits)}
52 - Removed: ${len(removed_commits)}
53
53
54 * Changed files:
54 * Changed files:
55
55
56 %if not changed_files:
56 %if not changed_files:
57 No file changes found
57 No file changes found
58 %else:
58 %else:
59 %for file_name in added_files:
59 %for file_name in added_files:
60 - A `${file_name}`
60 - A `${file_name}`
61 %endfor
61 %endfor
62 %for file_name in modified_files:
62 %for file_name in modified_files:
63 - M `${file_name}`
63 - M `${file_name}`
64 %endfor
64 %endfor
65 %for file_name in removed_files:
65 %for file_name in removed_files:
66 - R `${file_name}`
66 - R `${file_name}`
67 %endfor
67 %endfor
68 %endif
68 %endif
69
69
70 ---
70 ---
71 ${self.plaintext_footer()}
71 ${self.plaintext_footer()}
72 </%def>
72 </%def>
73 <%
73 <%
74 data = {
74 data = {
75 'updating_user': h.person(updating_user),
75 'updating_user': h.person(updating_user),
76 'pr_id': pull_request.pull_request_id,
76 'pr_id': pull_request.pull_request_id,
77 'pr_title': pull_request.title,
77 'pr_title': pull_request.title_safe,
78 'source_ref_type': pull_request.source_ref_parts.type,
78 'source_ref_type': pull_request.source_ref_parts.type,
79 'source_ref_name': pull_request.source_ref_parts.name,
79 'source_ref_name': pull_request.source_ref_parts.name,
80 'target_ref_type': pull_request.target_ref_parts.type,
80 'target_ref_type': pull_request.target_ref_parts.type,
81 'target_ref_name': pull_request.target_ref_parts.name,
81 'target_ref_name': pull_request.target_ref_parts.name,
82 'repo_url': pull_request_source_repo_url,
82 'repo_url': pull_request_source_repo_url,
83 'source_repo': pull_request_source_repo.repo_name,
83 'source_repo': pull_request_source_repo.repo_name,
84 'target_repo': pull_request_target_repo.repo_name,
84 'target_repo': pull_request_target_repo.repo_name,
85 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
85 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
86 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
86 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
87 }
87 }
88 %>
88 %>
89
89
90 ## header
90 ## header
91 <table style="text-align:left;vertical-align:middle;width: 100%">
91 <table style="text-align:left;vertical-align:middle;width: 100%">
92 <tr>
92 <tr>
93 <td style="width:100%;border-bottom:1px solid #dbd9da;">
93 <td style="width:100%;border-bottom:1px solid #dbd9da;">
94
94
95 <div style="margin: 0; font-weight: bold">
95 <div style="margin: 0; font-weight: bold">
96 <div class="clear-both" style="margin-bottom: 4px">
96 <div class="clear-both" style="margin-bottom: 4px">
97 <span style="color:#7E7F7F">@${h.person(updating_user.username)}</span>
97 <span style="color:#7E7F7F">@${h.person(updating_user.username)}</span>
98 ${_('updated')}
98 ${_('updated')}
99 <a href="${pull_request_url}" style="${base.link_css()}">
99 <a href="${pull_request_url}" style="${base.link_css()}">
100 ${_('pull request.').format(**data) }
100 ${_('pull request.').format(**data) }
101 </a>
101 </a>
102 </div>
102 </div>
103 <div style="margin-top: 10px"></div>
103 <div style="margin-top: 10px"></div>
104 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
104 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
105 </div>
105 </div>
106
106
107 </td>
107 </td>
108 </tr>
108 </tr>
109
109
110 </table>
110 </table>
111 <div class="clear-both"></div>
111 <div class="clear-both"></div>
112 ## main body
112 ## main body
113 <table style="text-align:left;vertical-align:middle;width: 100%">
113 <table style="text-align:left;vertical-align:middle;width: 100%">
114 ## spacing def
114 ## spacing def
115 <tr>
115 <tr>
116 <td style="width: 130px"></td>
116 <td style="width: 130px"></td>
117 <td></td>
117 <td></td>
118 </tr>
118 </tr>
119
119
120 <tr>
120 <tr>
121 <td style="padding-right:20px;">${_('Pull request')}:</td>
121 <td style="padding-right:20px;">${_('Pull request')}:</td>
122 <td>
122 <td>
123 <a href="${pull_request_url}" style="${base.link_css()}">
123 <a href="${pull_request_url}" style="${base.link_css()}">
124 !${pull_request.pull_request_id}
124 !${pull_request.pull_request_id}
125 </a>
125 </a>
126 </td>
126 </td>
127 </tr>
127 </tr>
128
128
129 <tr>
129 <tr>
130 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
130 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
131 <td style="line-height:20px;">
131 <td style="line-height:20px;">
132 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
132 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
133 &rarr;
133 &rarr;
134 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
134 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
135 </td>
135 </td>
136 </tr>
136 </tr>
137
137
138 <tr>
138 <tr>
139 <td style="padding-right:20px;">${_('Description')}:</td>
139 <td style="padding-right:20px;">${_('Description')}:</td>
140 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
140 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
141 </tr>
141 </tr>
142 <tr>
142 <tr>
143 <td style="padding-right:20px;">${_('Changes')}:</td>
143 <td style="padding-right:20px;">${_('Changes')}:</td>
144 <td>
144 <td>
145 <strong>Changed commits:</strong>
145 <strong>Changed commits:</strong>
146 <ul class="changes-ul">
146 <ul class="changes-ul">
147 <li>- Added: ${len(added_commits)}</li>
147 <li>- Added: ${len(added_commits)}</li>
148 <li>- Removed: ${len(removed_commits)}</li>
148 <li>- Removed: ${len(removed_commits)}</li>
149 </ul>
149 </ul>
150
150
151 <strong>Changed files:</strong>
151 <strong>Changed files:</strong>
152 <ul class="changes-ul">
152 <ul class="changes-ul">
153
153
154 %if not changed_files:
154 %if not changed_files:
155 <li>No file changes found</li>
155 <li>No file changes found</li>
156 %else:
156 %else:
157 %for file_name in added_files:
157 %for file_name in added_files:
158 <li>- A <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
158 <li>- A <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
159 %endfor
159 %endfor
160 %for file_name in modified_files:
160 %for file_name in modified_files:
161 <li>- M <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
161 <li>- M <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
162 %endfor
162 %endfor
163 %for file_name in removed_files:
163 %for file_name in removed_files:
164 <li>- R <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
164 <li>- R <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
165 %endfor
165 %endfor
166 %endif
166 %endif
167
167
168 </ul>
168 </ul>
169 </td>
169 </td>
170 </tr>
170 </tr>
171
171
172 </table>
172 </table>
@@ -1,195 +1,197 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22 import collections
22 import collections
23
23
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 from rhodecode.lib.utils2 import AttributeDict
25 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.model.db import User, PullRequestReviewers
26 from rhodecode.model.db import User, PullRequestReviewers
27 from rhodecode.model.notification import EmailNotificationModel
27 from rhodecode.model.notification import EmailNotificationModel
28
28
29
29
30 @pytest.fixture()
31 def pr():
32 def factory(ref):
33 return collections.namedtuple(
34 'PullRequest',
35 'pull_request_id, title, title_safe, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')\
36 (200, 'Example Pull Request', 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
37 return factory
38
39
30 def test_get_template_obj(app, request_stub):
40 def test_get_template_obj(app, request_stub):
31 template = EmailNotificationModel().get_renderer(
41 template = EmailNotificationModel().get_renderer(
32 EmailNotificationModel.TYPE_TEST, request_stub)
42 EmailNotificationModel.TYPE_TEST, request_stub)
33 assert isinstance(template, PyramidPartialRenderer)
43 assert isinstance(template, PyramidPartialRenderer)
34
44
35
45
36 def test_render_email(app, http_host_only_stub):
46 def test_render_email(app, http_host_only_stub):
37 kwargs = {}
47 kwargs = {}
38 subject, body, body_plaintext = EmailNotificationModel().render_email(
48 subject, body, body_plaintext = EmailNotificationModel().render_email(
39 EmailNotificationModel.TYPE_TEST, **kwargs)
49 EmailNotificationModel.TYPE_TEST, **kwargs)
40
50
41 # subject
51 # subject
42 assert subject == 'Test "Subject" hello "world"'
52 assert subject == 'Test "Subject" hello "world"'
43
53
44 # body plaintext
54 # body plaintext
45 assert body_plaintext == 'Email Plaintext Body'
55 assert body_plaintext == 'Email Plaintext Body'
46
56
47 # body
57 # body
48 notification_footer1 = 'This is a notification from RhodeCode.'
58 notification_footer1 = 'This is a notification from RhodeCode.'
49 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
59 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
50 assert notification_footer1 in body
60 assert notification_footer1 in body
51 assert notification_footer2 in body
61 assert notification_footer2 in body
52 assert 'Email Body' in body
62 assert 'Email Body' in body
53
63
54
64
55 @pytest.mark.parametrize('role', PullRequestReviewers.ROLES)
65 @pytest.mark.parametrize('role', PullRequestReviewers.ROLES)
56 def test_render_pr_email(app, user_admin, role):
66 def test_render_pr_email(app, user_admin, role, pr):
57 ref = collections.namedtuple(
67 ref = collections.namedtuple(
58 'Ref', 'name, type')('fxies123', 'book')
68 'Ref', 'name, type')('fxies123', 'book')
59
69 pr = pr(ref)
60 pr = collections.namedtuple('PullRequest',
61 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
62 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
63
64 source_repo = target_repo = collections.namedtuple(
70 source_repo = target_repo = collections.namedtuple(
65 'Repo', 'type, repo_name')('hg', 'pull_request_1')
71 'Repo', 'type, repo_name')('hg', 'pull_request_1')
66
72
67 kwargs = {
73 kwargs = {
68 'user': User.get_first_super_admin(),
74 'user': User.get_first_super_admin(),
69 'pull_request': pr,
75 'pull_request': pr,
70 'pull_request_commits': [],
76 'pull_request_commits': [],
71
77
72 'pull_request_target_repo': target_repo,
78 'pull_request_target_repo': target_repo,
73 'pull_request_target_repo_url': 'x',
79 'pull_request_target_repo_url': 'x',
74
80
75 'pull_request_source_repo': source_repo,
81 'pull_request_source_repo': source_repo,
76 'pull_request_source_repo_url': 'x',
82 'pull_request_source_repo_url': 'x',
77
83
78 'pull_request_url': 'http://localhost/pr1',
84 'pull_request_url': 'http://localhost/pr1',
79 'user_role': role,
85 'user_role': role,
80 }
86 }
81
87
82 subject, body, body_plaintext = EmailNotificationModel().render_email(
88 subject, body, body_plaintext = EmailNotificationModel().render_email(
83 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
89 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
84
90
85 # subject
91 # subject
86 if role == PullRequestReviewers.ROLE_REVIEWER:
92 if role == PullRequestReviewers.ROLE_REVIEWER:
87 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
93 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
88 elif role == PullRequestReviewers.ROLE_OBSERVER:
94 elif role == PullRequestReviewers.ROLE_OBSERVER:
89 assert subject == '@test_admin (RhodeCode Admin) added you as observer to pull request. !200: "Example Pull Request"'
95 assert subject == '@test_admin (RhodeCode Admin) added you as observer to pull request. !200: "Example Pull Request"'
90
96
91
97
92 def test_render_pr_update_email(app, user_admin):
98 def test_render_pr_update_email(app, user_admin, pr):
93 ref = collections.namedtuple(
99 ref = collections.namedtuple(
94 'Ref', 'name, type')('fxies123', 'book')
100 'Ref', 'name, type')('fxies123', 'book')
95
101
96 pr = collections.namedtuple('PullRequest',
102 pr = pr(ref)
97 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
98 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
99
103
100 source_repo = target_repo = collections.namedtuple(
104 source_repo = target_repo = collections.namedtuple(
101 'Repo', 'type, repo_name')('hg', 'pull_request_1')
105 'Repo', 'type, repo_name')('hg', 'pull_request_1')
102
106
103 commit_changes = AttributeDict({
107 commit_changes = AttributeDict({
104 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
108 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
105 'removed': ['eeeeeeeeeee'],
109 'removed': ['eeeeeeeeeee'],
106 })
110 })
107 file_changes = AttributeDict({
111 file_changes = AttributeDict({
108 'added': ['a/file1.md', 'file2.py'],
112 'added': ['a/file1.md', 'file2.py'],
109 'modified': ['b/modified_file.rst'],
113 'modified': ['b/modified_file.rst'],
110 'removed': ['.idea'],
114 'removed': ['.idea'],
111 })
115 })
112
116
113 kwargs = {
117 kwargs = {
114 'updating_user': User.get_first_super_admin(),
118 'updating_user': User.get_first_super_admin(),
115
119
116 'pull_request': pr,
120 'pull_request': pr,
117 'pull_request_commits': [],
121 'pull_request_commits': [],
118
122
119 'pull_request_target_repo': target_repo,
123 'pull_request_target_repo': target_repo,
120 'pull_request_target_repo_url': 'x',
124 'pull_request_target_repo_url': 'x',
121
125
122 'pull_request_source_repo': source_repo,
126 'pull_request_source_repo': source_repo,
123 'pull_request_source_repo_url': 'x',
127 'pull_request_source_repo_url': 'x',
124
128
125 'pull_request_url': 'http://localhost/pr1',
129 'pull_request_url': 'http://localhost/pr1',
126
130
127 'pr_comment_url': 'http://comment-url',
131 'pr_comment_url': 'http://comment-url',
128 'pr_comment_reply_url': 'http://comment-url#reply',
132 'pr_comment_reply_url': 'http://comment-url#reply',
129 'ancestor_commit_id': 'f39bd443',
133 'ancestor_commit_id': 'f39bd443',
130 'added_commits': commit_changes.added,
134 'added_commits': commit_changes.added,
131 'removed_commits': commit_changes.removed,
135 'removed_commits': commit_changes.removed,
132 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
136 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
133 'added_files': file_changes.added,
137 'added_files': file_changes.added,
134 'modified_files': file_changes.modified,
138 'modified_files': file_changes.modified,
135 'removed_files': file_changes.removed,
139 'removed_files': file_changes.removed,
136 }
140 }
137
141
138 subject, body, body_plaintext = EmailNotificationModel().render_email(
142 subject, body, body_plaintext = EmailNotificationModel().render_email(
139 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
143 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
140
144
141 # subject
145 # subject
142 assert subject == '@test_admin (RhodeCode Admin) updated pull request. !200: "Example Pull Request"'
146 assert subject == '@test_admin (RhodeCode Admin) updated pull request. !200: "Example Pull Request"'
143
147
144
148
145 @pytest.mark.parametrize('mention', [
149 @pytest.mark.parametrize('mention', [
146 True,
150 True,
147 False
151 False
148 ])
152 ])
149 @pytest.mark.parametrize('email_type', [
153 @pytest.mark.parametrize('email_type', [
150 EmailNotificationModel.TYPE_COMMIT_COMMENT,
154 EmailNotificationModel.TYPE_COMMIT_COMMENT,
151 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
155 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
152 ])
156 ])
153 def test_render_comment_subject_no_newlines(app, mention, email_type):
157 def test_render_comment_subject_no_newlines(app, mention, email_type, pr):
154 ref = collections.namedtuple(
158 ref = collections.namedtuple(
155 'Ref', 'name, type')('fxies123', 'book')
159 'Ref', 'name, type')('fxies123', 'book')
156
160
157 pr = collections.namedtuple('PullRequest',
161 pr = pr(ref)
158 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
159 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
160
162
161 source_repo = target_repo = collections.namedtuple(
163 source_repo = target_repo = collections.namedtuple(
162 'Repo', 'type, repo_name')('hg', 'pull_request_1')
164 'Repo', 'type, repo_name')('hg', 'pull_request_1')
163
165
164 kwargs = {
166 kwargs = {
165 'user': User.get_first_super_admin(),
167 'user': User.get_first_super_admin(),
166 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
168 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
167 'status_change': 'approved',
169 'status_change': 'approved',
168 'commit_target_repo_url': 'http://foo.example.com/#comment1',
170 'commit_target_repo_url': 'http://foo.example.com/#comment1',
169 'repo_name': 'test-repo',
171 'repo_name': 'test-repo',
170 'comment_file': 'test-file.py',
172 'comment_file': 'test-file.py',
171 'comment_line': 'n100',
173 'comment_line': 'n100',
172 'comment_type': 'note',
174 'comment_type': 'note',
173 'comment_id': 2048,
175 'comment_id': 2048,
174 'commit_comment_url': 'http://comment-url',
176 'commit_comment_url': 'http://comment-url',
175 'commit_comment_reply_url': 'http://comment-url/#Reply',
177 'commit_comment_reply_url': 'http://comment-url/#Reply',
176 'instance_url': 'http://rc-instance',
178 'instance_url': 'http://rc-instance',
177 'comment_body': 'hello world',
179 'comment_body': 'hello world',
178 'mention': mention,
180 'mention': mention,
179
181
180 'pr_comment_url': 'http://comment-url',
182 'pr_comment_url': 'http://comment-url',
181 'pr_comment_reply_url': 'http://comment-url/#Reply',
183 'pr_comment_reply_url': 'http://comment-url/#Reply',
182 'pull_request': pr,
184 'pull_request': pr,
183 'pull_request_commits': [],
185 'pull_request_commits': [],
184
186
185 'pull_request_target_repo': target_repo,
187 'pull_request_target_repo': target_repo,
186 'pull_request_target_repo_url': 'x',
188 'pull_request_target_repo_url': 'x',
187
189
188 'pull_request_source_repo': source_repo,
190 'pull_request_source_repo': source_repo,
189 'pull_request_source_repo_url': 'x',
191 'pull_request_source_repo_url': 'x',
190
192
191 'pull_request_url': 'http://code.rc.com/_pr/123'
193 'pull_request_url': 'http://code.rc.com/_pr/123'
192 }
194 }
193 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
195 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
194
196
195 assert '\n' not in subject
197 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now