##// END OF EJS Templates
pull-requests: added update pull-requests email+notifications...
marcink -
r4120:7cd93c2b default
parent child Browse files
Show More

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

1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,1009 +1,1011 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
47 merge_state=Optional(False)):
47 merge_state=Optional(False)):
48 """
48 """
49 Get a pull request based on the given ID.
49 Get a pull request based on the given ID.
50
50
51 :param apiuser: This is filled automatically from the |authtoken|.
51 :param apiuser: This is filled automatically from the |authtoken|.
52 :type apiuser: AuthUser
52 :type apiuser: AuthUser
53 :param repoid: Optional, repository name or repository ID from where
53 :param repoid: Optional, repository name or repository ID from where
54 the pull request was opened.
54 the pull request was opened.
55 :type repoid: str or int
55 :type repoid: str or int
56 :param pullrequestid: ID of the requested pull request.
56 :param pullrequestid: ID of the requested pull request.
57 :type pullrequestid: int
57 :type pullrequestid: int
58 :param merge_state: Optional calculate merge state for each repository.
58 :param merge_state: Optional calculate merge state for each repository.
59 This could result in longer time to fetch the data
59 This could result in longer time to fetch the data
60 :type merge_state: bool
60 :type merge_state: bool
61
61
62 Example output:
62 Example output:
63
63
64 .. code-block:: bash
64 .. code-block:: bash
65
65
66 "id": <id_given_in_input>,
66 "id": <id_given_in_input>,
67 "result":
67 "result":
68 {
68 {
69 "pull_request_id": "<pull_request_id>",
69 "pull_request_id": "<pull_request_id>",
70 "url": "<url>",
70 "url": "<url>",
71 "title": "<title>",
71 "title": "<title>",
72 "description": "<description>",
72 "description": "<description>",
73 "status" : "<status>",
73 "status" : "<status>",
74 "created_on": "<date_time_created>",
74 "created_on": "<date_time_created>",
75 "updated_on": "<date_time_updated>",
75 "updated_on": "<date_time_updated>",
76 "commit_ids": [
76 "commit_ids": [
77 ...
77 ...
78 "<commit_id>",
78 "<commit_id>",
79 "<commit_id>",
79 "<commit_id>",
80 ...
80 ...
81 ],
81 ],
82 "review_status": "<review_status>",
82 "review_status": "<review_status>",
83 "mergeable": {
83 "mergeable": {
84 "status": "<bool>",
84 "status": "<bool>",
85 "message": "<message>",
85 "message": "<message>",
86 },
86 },
87 "source": {
87 "source": {
88 "clone_url": "<clone_url>",
88 "clone_url": "<clone_url>",
89 "repository": "<repository_name>",
89 "repository": "<repository_name>",
90 "reference":
90 "reference":
91 {
91 {
92 "name": "<name>",
92 "name": "<name>",
93 "type": "<type>",
93 "type": "<type>",
94 "commit_id": "<commit_id>",
94 "commit_id": "<commit_id>",
95 }
95 }
96 },
96 },
97 "target": {
97 "target": {
98 "clone_url": "<clone_url>",
98 "clone_url": "<clone_url>",
99 "repository": "<repository_name>",
99 "repository": "<repository_name>",
100 "reference":
100 "reference":
101 {
101 {
102 "name": "<name>",
102 "name": "<name>",
103 "type": "<type>",
103 "type": "<type>",
104 "commit_id": "<commit_id>",
104 "commit_id": "<commit_id>",
105 }
105 }
106 },
106 },
107 "merge": {
107 "merge": {
108 "clone_url": "<clone_url>",
108 "clone_url": "<clone_url>",
109 "reference":
109 "reference":
110 {
110 {
111 "name": "<name>",
111 "name": "<name>",
112 "type": "<type>",
112 "type": "<type>",
113 "commit_id": "<commit_id>",
113 "commit_id": "<commit_id>",
114 }
114 }
115 },
115 },
116 "author": <user_obj>,
116 "author": <user_obj>,
117 "reviewers": [
117 "reviewers": [
118 ...
118 ...
119 {
119 {
120 "user": "<user_obj>",
120 "user": "<user_obj>",
121 "review_status": "<review_status>",
121 "review_status": "<review_status>",
122 }
122 }
123 ...
123 ...
124 ]
124 ]
125 },
125 },
126 "error": null
126 "error": null
127 """
127 """
128
128
129 pull_request = get_pull_request_or_error(pullrequestid)
129 pull_request = get_pull_request_or_error(pullrequestid)
130 if Optional.extract(repoid):
130 if Optional.extract(repoid):
131 repo = get_repo_or_error(repoid)
131 repo = get_repo_or_error(repoid)
132 else:
132 else:
133 repo = pull_request.target_repo
133 repo = pull_request.target_repo
134
134
135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
136 raise JSONRPCError('repository `%s` or pull request `%s` '
136 raise JSONRPCError('repository `%s` or pull request `%s` '
137 'does not exist' % (repoid, pullrequestid))
137 'does not exist' % (repoid, pullrequestid))
138
138
139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
140 # otherwise we can lock the repo on calculation of merge state while update/merge
140 # otherwise we can lock the repo on calculation of merge state while update/merge
141 # is happening.
141 # is happening.
142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
144 data = pull_request.get_api_data(with_merge_state=merge_state)
144 data = pull_request.get_api_data(with_merge_state=merge_state)
145 return data
145 return data
146
146
147
147
148 @jsonrpc_method()
148 @jsonrpc_method()
149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
150 merge_state=Optional(False)):
150 merge_state=Optional(False)):
151 """
151 """
152 Get all pull requests from the repository specified in `repoid`.
152 Get all pull requests from the repository specified in `repoid`.
153
153
154 :param apiuser: This is filled automatically from the |authtoken|.
154 :param apiuser: This is filled automatically from the |authtoken|.
155 :type apiuser: AuthUser
155 :type apiuser: AuthUser
156 :param repoid: Optional repository name or repository ID.
156 :param repoid: Optional repository name or repository ID.
157 :type repoid: str or int
157 :type repoid: str or int
158 :param status: Only return pull requests with the specified status.
158 :param status: Only return pull requests with the specified status.
159 Valid options are.
159 Valid options are.
160 * ``new`` (default)
160 * ``new`` (default)
161 * ``open``
161 * ``open``
162 * ``closed``
162 * ``closed``
163 :type status: str
163 :type status: str
164 :param merge_state: Optional calculate merge state for each repository.
164 :param merge_state: Optional calculate merge state for each repository.
165 This could result in longer time to fetch the data
165 This could result in longer time to fetch the data
166 :type merge_state: bool
166 :type merge_state: bool
167
167
168 Example output:
168 Example output:
169
169
170 .. code-block:: bash
170 .. code-block:: bash
171
171
172 "id": <id_given_in_input>,
172 "id": <id_given_in_input>,
173 "result":
173 "result":
174 [
174 [
175 ...
175 ...
176 {
176 {
177 "pull_request_id": "<pull_request_id>",
177 "pull_request_id": "<pull_request_id>",
178 "url": "<url>",
178 "url": "<url>",
179 "title" : "<title>",
179 "title" : "<title>",
180 "description": "<description>",
180 "description": "<description>",
181 "status": "<status>",
181 "status": "<status>",
182 "created_on": "<date_time_created>",
182 "created_on": "<date_time_created>",
183 "updated_on": "<date_time_updated>",
183 "updated_on": "<date_time_updated>",
184 "commit_ids": [
184 "commit_ids": [
185 ...
185 ...
186 "<commit_id>",
186 "<commit_id>",
187 "<commit_id>",
187 "<commit_id>",
188 ...
188 ...
189 ],
189 ],
190 "review_status": "<review_status>",
190 "review_status": "<review_status>",
191 "mergeable": {
191 "mergeable": {
192 "status": "<bool>",
192 "status": "<bool>",
193 "message: "<message>",
193 "message: "<message>",
194 },
194 },
195 "source": {
195 "source": {
196 "clone_url": "<clone_url>",
196 "clone_url": "<clone_url>",
197 "reference":
197 "reference":
198 {
198 {
199 "name": "<name>",
199 "name": "<name>",
200 "type": "<type>",
200 "type": "<type>",
201 "commit_id": "<commit_id>",
201 "commit_id": "<commit_id>",
202 }
202 }
203 },
203 },
204 "target": {
204 "target": {
205 "clone_url": "<clone_url>",
205 "clone_url": "<clone_url>",
206 "reference":
206 "reference":
207 {
207 {
208 "name": "<name>",
208 "name": "<name>",
209 "type": "<type>",
209 "type": "<type>",
210 "commit_id": "<commit_id>",
210 "commit_id": "<commit_id>",
211 }
211 }
212 },
212 },
213 "merge": {
213 "merge": {
214 "clone_url": "<clone_url>",
214 "clone_url": "<clone_url>",
215 "reference":
215 "reference":
216 {
216 {
217 "name": "<name>",
217 "name": "<name>",
218 "type": "<type>",
218 "type": "<type>",
219 "commit_id": "<commit_id>",
219 "commit_id": "<commit_id>",
220 }
220 }
221 },
221 },
222 "author": <user_obj>,
222 "author": <user_obj>,
223 "reviewers": [
223 "reviewers": [
224 ...
224 ...
225 {
225 {
226 "user": "<user_obj>",
226 "user": "<user_obj>",
227 "review_status": "<review_status>",
227 "review_status": "<review_status>",
228 }
228 }
229 ...
229 ...
230 ]
230 ]
231 }
231 }
232 ...
232 ...
233 ],
233 ],
234 "error": null
234 "error": null
235
235
236 """
236 """
237 repo = get_repo_or_error(repoid)
237 repo = get_repo_or_error(repoid)
238 if not has_superadmin_permission(apiuser):
238 if not has_superadmin_permission(apiuser):
239 _perms = (
239 _perms = (
240 'repository.admin', 'repository.write', 'repository.read',)
240 'repository.admin', 'repository.write', 'repository.read',)
241 validate_repo_permissions(apiuser, repoid, repo, _perms)
241 validate_repo_permissions(apiuser, repoid, repo, _perms)
242
242
243 status = Optional.extract(status)
243 status = Optional.extract(status)
244 merge_state = Optional.extract(merge_state, binary=True)
244 merge_state = Optional.extract(merge_state, binary=True)
245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
246 order_by='id', order_dir='desc')
246 order_by='id', order_dir='desc')
247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
248 return data
248 return data
249
249
250
250
251 @jsonrpc_method()
251 @jsonrpc_method()
252 def merge_pull_request(
252 def merge_pull_request(
253 request, apiuser, pullrequestid, repoid=Optional(None),
253 request, apiuser, pullrequestid, repoid=Optional(None),
254 userid=Optional(OAttr('apiuser'))):
254 userid=Optional(OAttr('apiuser'))):
255 """
255 """
256 Merge the pull request specified by `pullrequestid` into its target
256 Merge the pull request specified by `pullrequestid` into its target
257 repository.
257 repository.
258
258
259 :param apiuser: This is filled automatically from the |authtoken|.
259 :param apiuser: This is filled automatically from the |authtoken|.
260 :type apiuser: AuthUser
260 :type apiuser: AuthUser
261 :param repoid: Optional, repository name or repository ID of the
261 :param repoid: Optional, repository name or repository ID of the
262 target repository to which the |pr| is to be merged.
262 target repository to which the |pr| is to be merged.
263 :type repoid: str or int
263 :type repoid: str or int
264 :param pullrequestid: ID of the pull request which shall be merged.
264 :param pullrequestid: ID of the pull request which shall be merged.
265 :type pullrequestid: int
265 :type pullrequestid: int
266 :param userid: Merge the pull request as this user.
266 :param userid: Merge the pull request as this user.
267 :type userid: Optional(str or int)
267 :type userid: Optional(str or int)
268
268
269 Example output:
269 Example output:
270
270
271 .. code-block:: bash
271 .. code-block:: bash
272
272
273 "id": <id_given_in_input>,
273 "id": <id_given_in_input>,
274 "result": {
274 "result": {
275 "executed": "<bool>",
275 "executed": "<bool>",
276 "failure_reason": "<int>",
276 "failure_reason": "<int>",
277 "merge_status_message": "<str>",
277 "merge_status_message": "<str>",
278 "merge_commit_id": "<merge_commit_id>",
278 "merge_commit_id": "<merge_commit_id>",
279 "possible": "<bool>",
279 "possible": "<bool>",
280 "merge_ref": {
280 "merge_ref": {
281 "commit_id": "<commit_id>",
281 "commit_id": "<commit_id>",
282 "type": "<type>",
282 "type": "<type>",
283 "name": "<name>"
283 "name": "<name>"
284 }
284 }
285 },
285 },
286 "error": null
286 "error": null
287 """
287 """
288 pull_request = get_pull_request_or_error(pullrequestid)
288 pull_request = get_pull_request_or_error(pullrequestid)
289 if Optional.extract(repoid):
289 if Optional.extract(repoid):
290 repo = get_repo_or_error(repoid)
290 repo = get_repo_or_error(repoid)
291 else:
291 else:
292 repo = pull_request.target_repo
292 repo = pull_request.target_repo
293 auth_user = apiuser
293 auth_user = apiuser
294 if not isinstance(userid, Optional):
294 if not isinstance(userid, Optional):
295 if (has_superadmin_permission(apiuser) or
295 if (has_superadmin_permission(apiuser) or
296 HasRepoPermissionAnyApi('repository.admin')(
296 HasRepoPermissionAnyApi('repository.admin')(
297 user=apiuser, repo_name=repo.repo_name)):
297 user=apiuser, repo_name=repo.repo_name)):
298 apiuser = get_user_or_error(userid)
298 apiuser = get_user_or_error(userid)
299 auth_user = apiuser.AuthUser()
299 auth_user = apiuser.AuthUser()
300 else:
300 else:
301 raise JSONRPCError('userid is not the same as your user')
301 raise JSONRPCError('userid is not the same as your user')
302
302
303 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
303 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
304 raise JSONRPCError(
304 raise JSONRPCError(
305 'Operation forbidden because pull request is in state {}, '
305 'Operation forbidden because pull request is in state {}, '
306 'only state {} is allowed.'.format(
306 'only state {} is allowed.'.format(
307 pull_request.pull_request_state, PullRequest.STATE_CREATED))
307 pull_request.pull_request_state, PullRequest.STATE_CREATED))
308
308
309 with pull_request.set_state(PullRequest.STATE_UPDATING):
309 with pull_request.set_state(PullRequest.STATE_UPDATING):
310 check = MergeCheck.validate(pull_request, auth_user=auth_user,
310 check = MergeCheck.validate(pull_request, auth_user=auth_user,
311 translator=request.translate)
311 translator=request.translate)
312 merge_possible = not check.failed
312 merge_possible = not check.failed
313
313
314 if not merge_possible:
314 if not merge_possible:
315 error_messages = []
315 error_messages = []
316 for err_type, error_msg in check.errors:
316 for err_type, error_msg in check.errors:
317 error_msg = request.translate(error_msg)
317 error_msg = request.translate(error_msg)
318 error_messages.append(error_msg)
318 error_messages.append(error_msg)
319
319
320 reasons = ','.join(error_messages)
320 reasons = ','.join(error_messages)
321 raise JSONRPCError(
321 raise JSONRPCError(
322 'merge not possible for following reasons: {}'.format(reasons))
322 'merge not possible for following reasons: {}'.format(reasons))
323
323
324 target_repo = pull_request.target_repo
324 target_repo = pull_request.target_repo
325 extras = vcs_operation_context(
325 extras = vcs_operation_context(
326 request.environ, repo_name=target_repo.repo_name,
326 request.environ, repo_name=target_repo.repo_name,
327 username=auth_user.username, action='push',
327 username=auth_user.username, action='push',
328 scm=target_repo.repo_type)
328 scm=target_repo.repo_type)
329 with pull_request.set_state(PullRequest.STATE_UPDATING):
329 with pull_request.set_state(PullRequest.STATE_UPDATING):
330 merge_response = PullRequestModel().merge_repo(
330 merge_response = PullRequestModel().merge_repo(
331 pull_request, apiuser, extras=extras)
331 pull_request, apiuser, extras=extras)
332 if merge_response.executed:
332 if merge_response.executed:
333 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
333 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
334
334
335 Session().commit()
335 Session().commit()
336
336
337 # In previous versions the merge response directly contained the merge
337 # In previous versions the merge response directly contained the merge
338 # commit id. It is now contained in the merge reference object. To be
338 # commit id. It is now contained in the merge reference object. To be
339 # backwards compatible we have to extract it again.
339 # backwards compatible we have to extract it again.
340 merge_response = merge_response.asdict()
340 merge_response = merge_response.asdict()
341 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
341 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
342
342
343 return merge_response
343 return merge_response
344
344
345
345
346 @jsonrpc_method()
346 @jsonrpc_method()
347 def get_pull_request_comments(
347 def get_pull_request_comments(
348 request, apiuser, pullrequestid, repoid=Optional(None)):
348 request, apiuser, pullrequestid, repoid=Optional(None)):
349 """
349 """
350 Get all comments of pull request specified with the `pullrequestid`
350 Get all comments of pull request specified with the `pullrequestid`
351
351
352 :param apiuser: This is filled automatically from the |authtoken|.
352 :param apiuser: This is filled automatically from the |authtoken|.
353 :type apiuser: AuthUser
353 :type apiuser: AuthUser
354 :param repoid: Optional repository name or repository ID.
354 :param repoid: Optional repository name or repository ID.
355 :type repoid: str or int
355 :type repoid: str or int
356 :param pullrequestid: The pull request ID.
356 :param pullrequestid: The pull request ID.
357 :type pullrequestid: int
357 :type pullrequestid: int
358
358
359 Example output:
359 Example output:
360
360
361 .. code-block:: bash
361 .. code-block:: bash
362
362
363 id : <id_given_in_input>
363 id : <id_given_in_input>
364 result : [
364 result : [
365 {
365 {
366 "comment_author": {
366 "comment_author": {
367 "active": true,
367 "active": true,
368 "full_name_or_username": "Tom Gore",
368 "full_name_or_username": "Tom Gore",
369 "username": "admin"
369 "username": "admin"
370 },
370 },
371 "comment_created_on": "2017-01-02T18:43:45.533",
371 "comment_created_on": "2017-01-02T18:43:45.533",
372 "comment_f_path": null,
372 "comment_f_path": null,
373 "comment_id": 25,
373 "comment_id": 25,
374 "comment_lineno": null,
374 "comment_lineno": null,
375 "comment_status": {
375 "comment_status": {
376 "status": "under_review",
376 "status": "under_review",
377 "status_lbl": "Under Review"
377 "status_lbl": "Under Review"
378 },
378 },
379 "comment_text": "Example text",
379 "comment_text": "Example text",
380 "comment_type": null,
380 "comment_type": null,
381 "pull_request_version": null
381 "pull_request_version": null
382 }
382 }
383 ],
383 ],
384 error : null
384 error : null
385 """
385 """
386
386
387 pull_request = get_pull_request_or_error(pullrequestid)
387 pull_request = get_pull_request_or_error(pullrequestid)
388 if Optional.extract(repoid):
388 if Optional.extract(repoid):
389 repo = get_repo_or_error(repoid)
389 repo = get_repo_or_error(repoid)
390 else:
390 else:
391 repo = pull_request.target_repo
391 repo = pull_request.target_repo
392
392
393 if not PullRequestModel().check_user_read(
393 if not PullRequestModel().check_user_read(
394 pull_request, apiuser, api=True):
394 pull_request, apiuser, api=True):
395 raise JSONRPCError('repository `%s` or pull request `%s` '
395 raise JSONRPCError('repository `%s` or pull request `%s` '
396 'does not exist' % (repoid, pullrequestid))
396 'does not exist' % (repoid, pullrequestid))
397
397
398 (pull_request_latest,
398 (pull_request_latest,
399 pull_request_at_ver,
399 pull_request_at_ver,
400 pull_request_display_obj,
400 pull_request_display_obj,
401 at_version) = PullRequestModel().get_pr_version(
401 at_version) = PullRequestModel().get_pr_version(
402 pull_request.pull_request_id, version=None)
402 pull_request.pull_request_id, version=None)
403
403
404 versions = pull_request_display_obj.versions()
404 versions = pull_request_display_obj.versions()
405 ver_map = {
405 ver_map = {
406 ver.pull_request_version_id: cnt
406 ver.pull_request_version_id: cnt
407 for cnt, ver in enumerate(versions, 1)
407 for cnt, ver in enumerate(versions, 1)
408 }
408 }
409
409
410 # GENERAL COMMENTS with versions #
410 # GENERAL COMMENTS with versions #
411 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
411 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
412 q = q.order_by(ChangesetComment.comment_id.asc())
412 q = q.order_by(ChangesetComment.comment_id.asc())
413 general_comments = q.all()
413 general_comments = q.all()
414
414
415 # INLINE COMMENTS with versions #
415 # INLINE COMMENTS with versions #
416 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
416 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
417 q = q.order_by(ChangesetComment.comment_id.asc())
417 q = q.order_by(ChangesetComment.comment_id.asc())
418 inline_comments = q.all()
418 inline_comments = q.all()
419
419
420 data = []
420 data = []
421 for comment in inline_comments + general_comments:
421 for comment in inline_comments + general_comments:
422 full_data = comment.get_api_data()
422 full_data = comment.get_api_data()
423 pr_version_id = None
423 pr_version_id = None
424 if comment.pull_request_version_id:
424 if comment.pull_request_version_id:
425 pr_version_id = 'v{}'.format(
425 pr_version_id = 'v{}'.format(
426 ver_map[comment.pull_request_version_id])
426 ver_map[comment.pull_request_version_id])
427
427
428 # sanitize some entries
428 # sanitize some entries
429
429
430 full_data['pull_request_version'] = pr_version_id
430 full_data['pull_request_version'] = pr_version_id
431 full_data['comment_author'] = {
431 full_data['comment_author'] = {
432 'username': full_data['comment_author'].username,
432 'username': full_data['comment_author'].username,
433 'full_name_or_username': full_data['comment_author'].full_name_or_username,
433 'full_name_or_username': full_data['comment_author'].full_name_or_username,
434 'active': full_data['comment_author'].active,
434 'active': full_data['comment_author'].active,
435 }
435 }
436
436
437 if full_data['comment_status']:
437 if full_data['comment_status']:
438 full_data['comment_status'] = {
438 full_data['comment_status'] = {
439 'status': full_data['comment_status'][0].status,
439 'status': full_data['comment_status'][0].status,
440 'status_lbl': full_data['comment_status'][0].status_lbl,
440 'status_lbl': full_data['comment_status'][0].status_lbl,
441 }
441 }
442 else:
442 else:
443 full_data['comment_status'] = {}
443 full_data['comment_status'] = {}
444
444
445 data.append(full_data)
445 data.append(full_data)
446 return data
446 return data
447
447
448
448
449 @jsonrpc_method()
449 @jsonrpc_method()
450 def comment_pull_request(
450 def comment_pull_request(
451 request, apiuser, pullrequestid, repoid=Optional(None),
451 request, apiuser, pullrequestid, repoid=Optional(None),
452 message=Optional(None), commit_id=Optional(None), status=Optional(None),
452 message=Optional(None), commit_id=Optional(None), status=Optional(None),
453 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
453 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
454 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
454 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
455 userid=Optional(OAttr('apiuser'))):
455 userid=Optional(OAttr('apiuser'))):
456 """
456 """
457 Comment on the pull request specified with the `pullrequestid`,
457 Comment on the pull request specified with the `pullrequestid`,
458 in the |repo| specified by the `repoid`, and optionally change the
458 in the |repo| specified by the `repoid`, and optionally change the
459 review status.
459 review status.
460
460
461 :param apiuser: This is filled automatically from the |authtoken|.
461 :param apiuser: This is filled automatically from the |authtoken|.
462 :type apiuser: AuthUser
462 :type apiuser: AuthUser
463 :param repoid: Optional repository name or repository ID.
463 :param repoid: Optional repository name or repository ID.
464 :type repoid: str or int
464 :type repoid: str or int
465 :param pullrequestid: The pull request ID.
465 :param pullrequestid: The pull request ID.
466 :type pullrequestid: int
466 :type pullrequestid: int
467 :param commit_id: Specify the commit_id for which to set a comment. If
467 :param commit_id: Specify the commit_id for which to set a comment. If
468 given commit_id is different than latest in the PR status
468 given commit_id is different than latest in the PR status
469 change won't be performed.
469 change won't be performed.
470 :type commit_id: str
470 :type commit_id: str
471 :param message: The text content of the comment.
471 :param message: The text content of the comment.
472 :type message: str
472 :type message: str
473 :param status: (**Optional**) Set the approval status of the pull
473 :param status: (**Optional**) Set the approval status of the pull
474 request. One of: 'not_reviewed', 'approved', 'rejected',
474 request. One of: 'not_reviewed', 'approved', 'rejected',
475 'under_review'
475 'under_review'
476 :type status: str
476 :type status: str
477 :param comment_type: Comment type, one of: 'note', 'todo'
477 :param comment_type: Comment type, one of: 'note', 'todo'
478 :type comment_type: Optional(str), default: 'note'
478 :type comment_type: Optional(str), default: 'note'
479 :param resolves_comment_id: id of comment which this one will resolve
479 :param resolves_comment_id: id of comment which this one will resolve
480 :type resolves_comment_id: Optional(int)
480 :type resolves_comment_id: Optional(int)
481 :param extra_recipients: list of user ids or usernames to add
481 :param extra_recipients: list of user ids or usernames to add
482 notifications for this comment. Acts like a CC for notification
482 notifications for this comment. Acts like a CC for notification
483 :type extra_recipients: Optional(list)
483 :type extra_recipients: Optional(list)
484 :param userid: Comment on the pull request as this user
484 :param userid: Comment on the pull request as this user
485 :type userid: Optional(str or int)
485 :type userid: Optional(str or int)
486
486
487 Example output:
487 Example output:
488
488
489 .. code-block:: bash
489 .. code-block:: bash
490
490
491 id : <id_given_in_input>
491 id : <id_given_in_input>
492 result : {
492 result : {
493 "pull_request_id": "<Integer>",
493 "pull_request_id": "<Integer>",
494 "comment_id": "<Integer>",
494 "comment_id": "<Integer>",
495 "status": {"given": <given_status>,
495 "status": {"given": <given_status>,
496 "was_changed": <bool status_was_actually_changed> },
496 "was_changed": <bool status_was_actually_changed> },
497 },
497 },
498 error : null
498 error : null
499 """
499 """
500 pull_request = get_pull_request_or_error(pullrequestid)
500 pull_request = get_pull_request_or_error(pullrequestid)
501 if Optional.extract(repoid):
501 if Optional.extract(repoid):
502 repo = get_repo_or_error(repoid)
502 repo = get_repo_or_error(repoid)
503 else:
503 else:
504 repo = pull_request.target_repo
504 repo = pull_request.target_repo
505
505
506 auth_user = apiuser
506 auth_user = apiuser
507 if not isinstance(userid, Optional):
507 if not isinstance(userid, Optional):
508 if (has_superadmin_permission(apiuser) or
508 if (has_superadmin_permission(apiuser) or
509 HasRepoPermissionAnyApi('repository.admin')(
509 HasRepoPermissionAnyApi('repository.admin')(
510 user=apiuser, repo_name=repo.repo_name)):
510 user=apiuser, repo_name=repo.repo_name)):
511 apiuser = get_user_or_error(userid)
511 apiuser = get_user_or_error(userid)
512 auth_user = apiuser.AuthUser()
512 auth_user = apiuser.AuthUser()
513 else:
513 else:
514 raise JSONRPCError('userid is not the same as your user')
514 raise JSONRPCError('userid is not the same as your user')
515
515
516 if pull_request.is_closed():
516 if pull_request.is_closed():
517 raise JSONRPCError(
517 raise JSONRPCError(
518 'pull request `%s` comment failed, pull request is closed' % (
518 'pull request `%s` comment failed, pull request is closed' % (
519 pullrequestid,))
519 pullrequestid,))
520
520
521 if not PullRequestModel().check_user_read(
521 if not PullRequestModel().check_user_read(
522 pull_request, apiuser, api=True):
522 pull_request, apiuser, api=True):
523 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
523 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
524 message = Optional.extract(message)
524 message = Optional.extract(message)
525 status = Optional.extract(status)
525 status = Optional.extract(status)
526 commit_id = Optional.extract(commit_id)
526 commit_id = Optional.extract(commit_id)
527 comment_type = Optional.extract(comment_type)
527 comment_type = Optional.extract(comment_type)
528 resolves_comment_id = Optional.extract(resolves_comment_id)
528 resolves_comment_id = Optional.extract(resolves_comment_id)
529 extra_recipients = Optional.extract(extra_recipients)
529 extra_recipients = Optional.extract(extra_recipients)
530
530
531 if not message and not status:
531 if not message and not status:
532 raise JSONRPCError(
532 raise JSONRPCError(
533 'Both message and status parameters are missing. '
533 'Both message and status parameters are missing. '
534 'At least one is required.')
534 'At least one is required.')
535
535
536 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
536 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
537 status is not None):
537 status is not None):
538 raise JSONRPCError('Unknown comment status: `%s`' % status)
538 raise JSONRPCError('Unknown comment status: `%s`' % status)
539
539
540 if commit_id and commit_id not in pull_request.revisions:
540 if commit_id and commit_id not in pull_request.revisions:
541 raise JSONRPCError(
541 raise JSONRPCError(
542 'Invalid commit_id `%s` for this pull request.' % commit_id)
542 'Invalid commit_id `%s` for this pull request.' % commit_id)
543
543
544 allowed_to_change_status = PullRequestModel().check_user_change_status(
544 allowed_to_change_status = PullRequestModel().check_user_change_status(
545 pull_request, apiuser)
545 pull_request, apiuser)
546
546
547 # if commit_id is passed re-validated if user is allowed to change status
547 # if commit_id is passed re-validated if user is allowed to change status
548 # based on latest commit_id from the PR
548 # based on latest commit_id from the PR
549 if commit_id:
549 if commit_id:
550 commit_idx = pull_request.revisions.index(commit_id)
550 commit_idx = pull_request.revisions.index(commit_id)
551 if commit_idx != 0:
551 if commit_idx != 0:
552 allowed_to_change_status = False
552 allowed_to_change_status = False
553
553
554 if resolves_comment_id:
554 if resolves_comment_id:
555 comment = ChangesetComment.get(resolves_comment_id)
555 comment = ChangesetComment.get(resolves_comment_id)
556 if not comment:
556 if not comment:
557 raise JSONRPCError(
557 raise JSONRPCError(
558 'Invalid resolves_comment_id `%s` for this pull request.'
558 'Invalid resolves_comment_id `%s` for this pull request.'
559 % resolves_comment_id)
559 % resolves_comment_id)
560 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
560 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
561 raise JSONRPCError(
561 raise JSONRPCError(
562 'Comment `%s` is wrong type for setting status to resolved.'
562 'Comment `%s` is wrong type for setting status to resolved.'
563 % resolves_comment_id)
563 % resolves_comment_id)
564
564
565 text = message
565 text = message
566 status_label = ChangesetStatus.get_status_lbl(status)
566 status_label = ChangesetStatus.get_status_lbl(status)
567 if status and allowed_to_change_status:
567 if status and allowed_to_change_status:
568 st_message = ('Status change %(transition_icon)s %(status)s'
568 st_message = ('Status change %(transition_icon)s %(status)s'
569 % {'transition_icon': '>', 'status': status_label})
569 % {'transition_icon': '>', 'status': status_label})
570 text = message or st_message
570 text = message or st_message
571
571
572 rc_config = SettingsModel().get_all_settings()
572 rc_config = SettingsModel().get_all_settings()
573 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
573 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
574
574
575 status_change = status and allowed_to_change_status
575 status_change = status and allowed_to_change_status
576 comment = CommentsModel().create(
576 comment = CommentsModel().create(
577 text=text,
577 text=text,
578 repo=pull_request.target_repo.repo_id,
578 repo=pull_request.target_repo.repo_id,
579 user=apiuser.user_id,
579 user=apiuser.user_id,
580 pull_request=pull_request.pull_request_id,
580 pull_request=pull_request.pull_request_id,
581 f_path=None,
581 f_path=None,
582 line_no=None,
582 line_no=None,
583 status_change=(status_label if status_change else None),
583 status_change=(status_label if status_change else None),
584 status_change_type=(status if status_change else None),
584 status_change_type=(status if status_change else None),
585 closing_pr=False,
585 closing_pr=False,
586 renderer=renderer,
586 renderer=renderer,
587 comment_type=comment_type,
587 comment_type=comment_type,
588 resolves_comment_id=resolves_comment_id,
588 resolves_comment_id=resolves_comment_id,
589 auth_user=auth_user,
589 auth_user=auth_user,
590 extra_recipients=extra_recipients
590 extra_recipients=extra_recipients
591 )
591 )
592
592
593 if allowed_to_change_status and status:
593 if allowed_to_change_status and status:
594 old_calculated_status = pull_request.calculated_review_status()
594 old_calculated_status = pull_request.calculated_review_status()
595 ChangesetStatusModel().set_status(
595 ChangesetStatusModel().set_status(
596 pull_request.target_repo.repo_id,
596 pull_request.target_repo.repo_id,
597 status,
597 status,
598 apiuser.user_id,
598 apiuser.user_id,
599 comment,
599 comment,
600 pull_request=pull_request.pull_request_id
600 pull_request=pull_request.pull_request_id
601 )
601 )
602 Session().flush()
602 Session().flush()
603
603
604 Session().commit()
604 Session().commit()
605
605
606 PullRequestModel().trigger_pull_request_hook(
606 PullRequestModel().trigger_pull_request_hook(
607 pull_request, apiuser, 'comment',
607 pull_request, apiuser, 'comment',
608 data={'comment': comment})
608 data={'comment': comment})
609
609
610 if allowed_to_change_status and status:
610 if allowed_to_change_status and status:
611 # we now calculate the status of pull request, and based on that
611 # we now calculate the status of pull request, and based on that
612 # calculation we set the commits status
612 # calculation we set the commits status
613 calculated_status = pull_request.calculated_review_status()
613 calculated_status = pull_request.calculated_review_status()
614 if old_calculated_status != calculated_status:
614 if old_calculated_status != calculated_status:
615 PullRequestModel().trigger_pull_request_hook(
615 PullRequestModel().trigger_pull_request_hook(
616 pull_request, apiuser, 'review_status_change',
616 pull_request, apiuser, 'review_status_change',
617 data={'status': calculated_status})
617 data={'status': calculated_status})
618
618
619 data = {
619 data = {
620 'pull_request_id': pull_request.pull_request_id,
620 'pull_request_id': pull_request.pull_request_id,
621 'comment_id': comment.comment_id if comment else None,
621 'comment_id': comment.comment_id if comment else None,
622 'status': {'given': status, 'was_changed': status_change},
622 'status': {'given': status, 'was_changed': status_change},
623 }
623 }
624 return data
624 return data
625
625
626
626
627 @jsonrpc_method()
627 @jsonrpc_method()
628 def create_pull_request(
628 def create_pull_request(
629 request, apiuser, source_repo, target_repo, source_ref, target_ref,
629 request, apiuser, source_repo, target_repo, source_ref, target_ref,
630 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
630 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
631 description_renderer=Optional(''), reviewers=Optional(None)):
631 description_renderer=Optional(''), reviewers=Optional(None)):
632 """
632 """
633 Creates a new pull request.
633 Creates a new pull request.
634
634
635 Accepts refs in the following formats:
635 Accepts refs in the following formats:
636
636
637 * branch:<branch_name>:<sha>
637 * branch:<branch_name>:<sha>
638 * branch:<branch_name>
638 * branch:<branch_name>
639 * bookmark:<bookmark_name>:<sha> (Mercurial only)
639 * bookmark:<bookmark_name>:<sha> (Mercurial only)
640 * bookmark:<bookmark_name> (Mercurial only)
640 * bookmark:<bookmark_name> (Mercurial only)
641
641
642 :param apiuser: This is filled automatically from the |authtoken|.
642 :param apiuser: This is filled automatically from the |authtoken|.
643 :type apiuser: AuthUser
643 :type apiuser: AuthUser
644 :param source_repo: Set the source repository name.
644 :param source_repo: Set the source repository name.
645 :type source_repo: str
645 :type source_repo: str
646 :param target_repo: Set the target repository name.
646 :param target_repo: Set the target repository name.
647 :type target_repo: str
647 :type target_repo: str
648 :param source_ref: Set the source ref name.
648 :param source_ref: Set the source ref name.
649 :type source_ref: str
649 :type source_ref: str
650 :param target_ref: Set the target ref name.
650 :param target_ref: Set the target ref name.
651 :type target_ref: str
651 :type target_ref: str
652 :param owner: user_id or username
652 :param owner: user_id or username
653 :type owner: Optional(str)
653 :type owner: Optional(str)
654 :param title: Optionally Set the pull request title, it's generated otherwise
654 :param title: Optionally Set the pull request title, it's generated otherwise
655 :type title: str
655 :type title: str
656 :param description: Set the pull request description.
656 :param description: Set the pull request description.
657 :type description: Optional(str)
657 :type description: Optional(str)
658 :type description_renderer: Optional(str)
658 :type description_renderer: Optional(str)
659 :param description_renderer: Set pull request renderer for the description.
659 :param description_renderer: Set pull request renderer for the description.
660 It should be 'rst', 'markdown' or 'plain'. If not give default
660 It should be 'rst', 'markdown' or 'plain'. If not give default
661 system renderer will be used
661 system renderer will be used
662 :param reviewers: Set the new pull request reviewers list.
662 :param reviewers: Set the new pull request reviewers list.
663 Reviewer defined by review rules will be added automatically to the
663 Reviewer defined by review rules will be added automatically to the
664 defined list.
664 defined list.
665 :type reviewers: Optional(list)
665 :type reviewers: Optional(list)
666 Accepts username strings or objects of the format:
666 Accepts username strings or objects of the format:
667
667
668 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
668 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
669 """
669 """
670
670
671 source_db_repo = get_repo_or_error(source_repo)
671 source_db_repo = get_repo_or_error(source_repo)
672 target_db_repo = get_repo_or_error(target_repo)
672 target_db_repo = get_repo_or_error(target_repo)
673 if not has_superadmin_permission(apiuser):
673 if not has_superadmin_permission(apiuser):
674 _perms = ('repository.admin', 'repository.write', 'repository.read',)
674 _perms = ('repository.admin', 'repository.write', 'repository.read',)
675 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
675 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
676
676
677 owner = validate_set_owner_permissions(apiuser, owner)
677 owner = validate_set_owner_permissions(apiuser, owner)
678
678
679 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
679 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
680 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
680 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
681
681
682 source_scm = source_db_repo.scm_instance()
682 source_scm = source_db_repo.scm_instance()
683 target_scm = target_db_repo.scm_instance()
683 target_scm = target_db_repo.scm_instance()
684
684
685 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
685 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
686 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
686 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
687
687
688 ancestor = source_scm.get_common_ancestor(
688 ancestor = source_scm.get_common_ancestor(
689 source_commit.raw_id, target_commit.raw_id, target_scm)
689 source_commit.raw_id, target_commit.raw_id, target_scm)
690 if not ancestor:
690 if not ancestor:
691 raise JSONRPCError('no common ancestor found')
691 raise JSONRPCError('no common ancestor found')
692
692
693 # recalculate target ref based on ancestor
693 # recalculate target ref based on ancestor
694 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
694 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
695 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
695 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
696
696
697 commit_ranges = target_scm.compare(
697 commit_ranges = target_scm.compare(
698 target_commit.raw_id, source_commit.raw_id, source_scm,
698 target_commit.raw_id, source_commit.raw_id, source_scm,
699 merge=True, pre_load=[])
699 merge=True, pre_load=[])
700
700
701 if not commit_ranges:
701 if not commit_ranges:
702 raise JSONRPCError('no commits found')
702 raise JSONRPCError('no commits found')
703
703
704 reviewer_objects = Optional.extract(reviewers) or []
704 reviewer_objects = Optional.extract(reviewers) or []
705
705
706 # serialize and validate passed in given reviewers
706 # serialize and validate passed in given reviewers
707 if reviewer_objects:
707 if reviewer_objects:
708 schema = ReviewerListSchema()
708 schema = ReviewerListSchema()
709 try:
709 try:
710 reviewer_objects = schema.deserialize(reviewer_objects)
710 reviewer_objects = schema.deserialize(reviewer_objects)
711 except Invalid as err:
711 except Invalid as err:
712 raise JSONRPCValidationError(colander_exc=err)
712 raise JSONRPCValidationError(colander_exc=err)
713
713
714 # validate users
714 # validate users
715 for reviewer_object in reviewer_objects:
715 for reviewer_object in reviewer_objects:
716 user = get_user_or_error(reviewer_object['username'])
716 user = get_user_or_error(reviewer_object['username'])
717 reviewer_object['user_id'] = user.user_id
717 reviewer_object['user_id'] = user.user_id
718
718
719 get_default_reviewers_data, validate_default_reviewers = \
719 get_default_reviewers_data, validate_default_reviewers = \
720 PullRequestModel().get_reviewer_functions()
720 PullRequestModel().get_reviewer_functions()
721
721
722 # recalculate reviewers logic, to make sure we can validate this
722 # recalculate reviewers logic, to make sure we can validate this
723 reviewer_rules = get_default_reviewers_data(
723 reviewer_rules = get_default_reviewers_data(
724 owner, source_db_repo,
724 owner, source_db_repo,
725 source_commit, target_db_repo, target_commit)
725 source_commit, target_db_repo, target_commit)
726
726
727 # now MERGE our given with the calculated
727 # now MERGE our given with the calculated
728 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
728 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
729
729
730 try:
730 try:
731 reviewers = validate_default_reviewers(
731 reviewers = validate_default_reviewers(
732 reviewer_objects, reviewer_rules)
732 reviewer_objects, reviewer_rules)
733 except ValueError as e:
733 except ValueError as e:
734 raise JSONRPCError('Reviewers Validation: {}'.format(e))
734 raise JSONRPCError('Reviewers Validation: {}'.format(e))
735
735
736 title = Optional.extract(title)
736 title = Optional.extract(title)
737 if not title:
737 if not title:
738 title_source_ref = source_ref.split(':', 2)[1]
738 title_source_ref = source_ref.split(':', 2)[1]
739 title = PullRequestModel().generate_pullrequest_title(
739 title = PullRequestModel().generate_pullrequest_title(
740 source=source_repo,
740 source=source_repo,
741 source_ref=title_source_ref,
741 source_ref=title_source_ref,
742 target=target_repo
742 target=target_repo
743 )
743 )
744 # fetch renderer, if set fallback to plain in case of PR
744 # fetch renderer, if set fallback to plain in case of PR
745 rc_config = SettingsModel().get_all_settings()
745 rc_config = SettingsModel().get_all_settings()
746 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
746 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
747 description = Optional.extract(description)
747 description = Optional.extract(description)
748 description_renderer = Optional.extract(description_renderer) or default_system_renderer
748 description_renderer = Optional.extract(description_renderer) or default_system_renderer
749
749
750 pull_request = PullRequestModel().create(
750 pull_request = PullRequestModel().create(
751 created_by=owner.user_id,
751 created_by=owner.user_id,
752 source_repo=source_repo,
752 source_repo=source_repo,
753 source_ref=full_source_ref,
753 source_ref=full_source_ref,
754 target_repo=target_repo,
754 target_repo=target_repo,
755 target_ref=full_target_ref,
755 target_ref=full_target_ref,
756 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
756 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
757 reviewers=reviewers,
757 reviewers=reviewers,
758 title=title,
758 title=title,
759 description=description,
759 description=description,
760 description_renderer=description_renderer,
760 description_renderer=description_renderer,
761 reviewer_data=reviewer_rules,
761 reviewer_data=reviewer_rules,
762 auth_user=apiuser
762 auth_user=apiuser
763 )
763 )
764
764
765 Session().commit()
765 Session().commit()
766 data = {
766 data = {
767 'msg': 'Created new pull request `{}`'.format(title),
767 'msg': 'Created new pull request `{}`'.format(title),
768 'pull_request_id': pull_request.pull_request_id,
768 'pull_request_id': pull_request.pull_request_id,
769 }
769 }
770 return data
770 return data
771
771
772
772
773 @jsonrpc_method()
773 @jsonrpc_method()
774 def update_pull_request(
774 def update_pull_request(
775 request, apiuser, pullrequestid, repoid=Optional(None),
775 request, apiuser, pullrequestid, repoid=Optional(None),
776 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
776 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
777 reviewers=Optional(None), update_commits=Optional(None)):
777 reviewers=Optional(None), update_commits=Optional(None)):
778 """
778 """
779 Updates a pull request.
779 Updates a pull request.
780
780
781 :param apiuser: This is filled automatically from the |authtoken|.
781 :param apiuser: This is filled automatically from the |authtoken|.
782 :type apiuser: AuthUser
782 :type apiuser: AuthUser
783 :param repoid: Optional repository name or repository ID.
783 :param repoid: Optional repository name or repository ID.
784 :type repoid: str or int
784 :type repoid: str or int
785 :param pullrequestid: The pull request ID.
785 :param pullrequestid: The pull request ID.
786 :type pullrequestid: int
786 :type pullrequestid: int
787 :param title: Set the pull request title.
787 :param title: Set the pull request title.
788 :type title: str
788 :type title: str
789 :param description: Update pull request description.
789 :param description: Update pull request description.
790 :type description: Optional(str)
790 :type description: Optional(str)
791 :type description_renderer: Optional(str)
791 :type description_renderer: Optional(str)
792 :param description_renderer: Update pull request renderer for the description.
792 :param description_renderer: Update pull request renderer for the description.
793 It should be 'rst', 'markdown' or 'plain'
793 It should be 'rst', 'markdown' or 'plain'
794 :param reviewers: Update pull request reviewers list with new value.
794 :param reviewers: Update pull request reviewers list with new value.
795 :type reviewers: Optional(list)
795 :type reviewers: Optional(list)
796 Accepts username strings or objects of the format:
796 Accepts username strings or objects of the format:
797
797
798 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
798 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
799
799
800 :param update_commits: Trigger update of commits for this pull request
800 :param update_commits: Trigger update of commits for this pull request
801 :type: update_commits: Optional(bool)
801 :type: update_commits: Optional(bool)
802
802
803 Example output:
803 Example output:
804
804
805 .. code-block:: bash
805 .. code-block:: bash
806
806
807 id : <id_given_in_input>
807 id : <id_given_in_input>
808 result : {
808 result : {
809 "msg": "Updated pull request `63`",
809 "msg": "Updated pull request `63`",
810 "pull_request": <pull_request_object>,
810 "pull_request": <pull_request_object>,
811 "updated_reviewers": {
811 "updated_reviewers": {
812 "added": [
812 "added": [
813 "username"
813 "username"
814 ],
814 ],
815 "removed": []
815 "removed": []
816 },
816 },
817 "updated_commits": {
817 "updated_commits": {
818 "added": [
818 "added": [
819 "<sha1_hash>"
819 "<sha1_hash>"
820 ],
820 ],
821 "common": [
821 "common": [
822 "<sha1_hash>",
822 "<sha1_hash>",
823 "<sha1_hash>",
823 "<sha1_hash>",
824 ],
824 ],
825 "removed": []
825 "removed": []
826 }
826 }
827 }
827 }
828 error : null
828 error : null
829 """
829 """
830
830
831 pull_request = get_pull_request_or_error(pullrequestid)
831 pull_request = get_pull_request_or_error(pullrequestid)
832 if Optional.extract(repoid):
832 if Optional.extract(repoid):
833 repo = get_repo_or_error(repoid)
833 repo = get_repo_or_error(repoid)
834 else:
834 else:
835 repo = pull_request.target_repo
835 repo = pull_request.target_repo
836
836
837 if not PullRequestModel().check_user_update(
837 if not PullRequestModel().check_user_update(
838 pull_request, apiuser, api=True):
838 pull_request, apiuser, api=True):
839 raise JSONRPCError(
839 raise JSONRPCError(
840 'pull request `%s` update failed, no permission to update.' % (
840 'pull request `%s` update failed, no permission to update.' % (
841 pullrequestid,))
841 pullrequestid,))
842 if pull_request.is_closed():
842 if pull_request.is_closed():
843 raise JSONRPCError(
843 raise JSONRPCError(
844 'pull request `%s` update failed, pull request is closed' % (
844 'pull request `%s` update failed, pull request is closed' % (
845 pullrequestid,))
845 pullrequestid,))
846
846
847 reviewer_objects = Optional.extract(reviewers) or []
847 reviewer_objects = Optional.extract(reviewers) or []
848
848
849 if reviewer_objects:
849 if reviewer_objects:
850 schema = ReviewerListSchema()
850 schema = ReviewerListSchema()
851 try:
851 try:
852 reviewer_objects = schema.deserialize(reviewer_objects)
852 reviewer_objects = schema.deserialize(reviewer_objects)
853 except Invalid as err:
853 except Invalid as err:
854 raise JSONRPCValidationError(colander_exc=err)
854 raise JSONRPCValidationError(colander_exc=err)
855
855
856 # validate users
856 # validate users
857 for reviewer_object in reviewer_objects:
857 for reviewer_object in reviewer_objects:
858 user = get_user_or_error(reviewer_object['username'])
858 user = get_user_or_error(reviewer_object['username'])
859 reviewer_object['user_id'] = user.user_id
859 reviewer_object['user_id'] = user.user_id
860
860
861 get_default_reviewers_data, get_validated_reviewers = \
861 get_default_reviewers_data, get_validated_reviewers = \
862 PullRequestModel().get_reviewer_functions()
862 PullRequestModel().get_reviewer_functions()
863
863
864 # re-use stored rules
864 # re-use stored rules
865 reviewer_rules = pull_request.reviewer_data
865 reviewer_rules = pull_request.reviewer_data
866 try:
866 try:
867 reviewers = get_validated_reviewers(
867 reviewers = get_validated_reviewers(
868 reviewer_objects, reviewer_rules)
868 reviewer_objects, reviewer_rules)
869 except ValueError as e:
869 except ValueError as e:
870 raise JSONRPCError('Reviewers Validation: {}'.format(e))
870 raise JSONRPCError('Reviewers Validation: {}'.format(e))
871 else:
871 else:
872 reviewers = []
872 reviewers = []
873
873
874 title = Optional.extract(title)
874 title = Optional.extract(title)
875 description = Optional.extract(description)
875 description = Optional.extract(description)
876 description_renderer = Optional.extract(description_renderer)
876 description_renderer = Optional.extract(description_renderer)
877
877
878 if title or description:
878 if title or description:
879 PullRequestModel().edit(
879 PullRequestModel().edit(
880 pull_request,
880 pull_request,
881 title or pull_request.title,
881 title or pull_request.title,
882 description or pull_request.description,
882 description or pull_request.description,
883 description_renderer or pull_request.description_renderer,
883 description_renderer or pull_request.description_renderer,
884 apiuser)
884 apiuser)
885 Session().commit()
885 Session().commit()
886
886
887 commit_changes = {"added": [], "common": [], "removed": []}
887 commit_changes = {"added": [], "common": [], "removed": []}
888 if str2bool(Optional.extract(update_commits)):
888 if str2bool(Optional.extract(update_commits)):
889
889
890 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
890 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
891 raise JSONRPCError(
891 raise JSONRPCError(
892 'Operation forbidden because pull request is in state {}, '
892 'Operation forbidden because pull request is in state {}, '
893 'only state {} is allowed.'.format(
893 'only state {} is allowed.'.format(
894 pull_request.pull_request_state, PullRequest.STATE_CREATED))
894 pull_request.pull_request_state, PullRequest.STATE_CREATED))
895
895
896 with pull_request.set_state(PullRequest.STATE_UPDATING):
896 with pull_request.set_state(PullRequest.STATE_UPDATING):
897 if PullRequestModel().has_valid_update_type(pull_request):
897 if PullRequestModel().has_valid_update_type(pull_request):
898 update_response = PullRequestModel().update_commits(pull_request)
898 db_user = apiuser.get_instance()
899 update_response = PullRequestModel().update_commits(
900 pull_request, db_user)
899 commit_changes = update_response.changes or commit_changes
901 commit_changes = update_response.changes or commit_changes
900 Session().commit()
902 Session().commit()
901
903
902 reviewers_changes = {"added": [], "removed": []}
904 reviewers_changes = {"added": [], "removed": []}
903 if reviewers:
905 if reviewers:
904 old_calculated_status = pull_request.calculated_review_status()
906 old_calculated_status = pull_request.calculated_review_status()
905 added_reviewers, removed_reviewers = \
907 added_reviewers, removed_reviewers = \
906 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
908 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
907
909
908 reviewers_changes['added'] = sorted(
910 reviewers_changes['added'] = sorted(
909 [get_user_or_error(n).username for n in added_reviewers])
911 [get_user_or_error(n).username for n in added_reviewers])
910 reviewers_changes['removed'] = sorted(
912 reviewers_changes['removed'] = sorted(
911 [get_user_or_error(n).username for n in removed_reviewers])
913 [get_user_or_error(n).username for n in removed_reviewers])
912 Session().commit()
914 Session().commit()
913
915
914 # trigger status changed if change in reviewers changes the status
916 # trigger status changed if change in reviewers changes the status
915 calculated_status = pull_request.calculated_review_status()
917 calculated_status = pull_request.calculated_review_status()
916 if old_calculated_status != calculated_status:
918 if old_calculated_status != calculated_status:
917 PullRequestModel().trigger_pull_request_hook(
919 PullRequestModel().trigger_pull_request_hook(
918 pull_request, apiuser, 'review_status_change',
920 pull_request, apiuser, 'review_status_change',
919 data={'status': calculated_status})
921 data={'status': calculated_status})
920
922
921 data = {
923 data = {
922 'msg': 'Updated pull request `{}`'.format(
924 'msg': 'Updated pull request `{}`'.format(
923 pull_request.pull_request_id),
925 pull_request.pull_request_id),
924 'pull_request': pull_request.get_api_data(),
926 'pull_request': pull_request.get_api_data(),
925 'updated_commits': commit_changes,
927 'updated_commits': commit_changes,
926 'updated_reviewers': reviewers_changes
928 'updated_reviewers': reviewers_changes
927 }
929 }
928
930
929 return data
931 return data
930
932
931
933
932 @jsonrpc_method()
934 @jsonrpc_method()
933 def close_pull_request(
935 def close_pull_request(
934 request, apiuser, pullrequestid, repoid=Optional(None),
936 request, apiuser, pullrequestid, repoid=Optional(None),
935 userid=Optional(OAttr('apiuser')), message=Optional('')):
937 userid=Optional(OAttr('apiuser')), message=Optional('')):
936 """
938 """
937 Close the pull request specified by `pullrequestid`.
939 Close the pull request specified by `pullrequestid`.
938
940
939 :param apiuser: This is filled automatically from the |authtoken|.
941 :param apiuser: This is filled automatically from the |authtoken|.
940 :type apiuser: AuthUser
942 :type apiuser: AuthUser
941 :param repoid: Repository name or repository ID to which the pull
943 :param repoid: Repository name or repository ID to which the pull
942 request belongs.
944 request belongs.
943 :type repoid: str or int
945 :type repoid: str or int
944 :param pullrequestid: ID of the pull request to be closed.
946 :param pullrequestid: ID of the pull request to be closed.
945 :type pullrequestid: int
947 :type pullrequestid: int
946 :param userid: Close the pull request as this user.
948 :param userid: Close the pull request as this user.
947 :type userid: Optional(str or int)
949 :type userid: Optional(str or int)
948 :param message: Optional message to close the Pull Request with. If not
950 :param message: Optional message to close the Pull Request with. If not
949 specified it will be generated automatically.
951 specified it will be generated automatically.
950 :type message: Optional(str)
952 :type message: Optional(str)
951
953
952 Example output:
954 Example output:
953
955
954 .. code-block:: bash
956 .. code-block:: bash
955
957
956 "id": <id_given_in_input>,
958 "id": <id_given_in_input>,
957 "result": {
959 "result": {
958 "pull_request_id": "<int>",
960 "pull_request_id": "<int>",
959 "close_status": "<str:status_lbl>,
961 "close_status": "<str:status_lbl>,
960 "closed": "<bool>"
962 "closed": "<bool>"
961 },
963 },
962 "error": null
964 "error": null
963
965
964 """
966 """
965 _ = request.translate
967 _ = request.translate
966
968
967 pull_request = get_pull_request_or_error(pullrequestid)
969 pull_request = get_pull_request_or_error(pullrequestid)
968 if Optional.extract(repoid):
970 if Optional.extract(repoid):
969 repo = get_repo_or_error(repoid)
971 repo = get_repo_or_error(repoid)
970 else:
972 else:
971 repo = pull_request.target_repo
973 repo = pull_request.target_repo
972
974
973 if not isinstance(userid, Optional):
975 if not isinstance(userid, Optional):
974 if (has_superadmin_permission(apiuser) or
976 if (has_superadmin_permission(apiuser) or
975 HasRepoPermissionAnyApi('repository.admin')(
977 HasRepoPermissionAnyApi('repository.admin')(
976 user=apiuser, repo_name=repo.repo_name)):
978 user=apiuser, repo_name=repo.repo_name)):
977 apiuser = get_user_or_error(userid)
979 apiuser = get_user_or_error(userid)
978 else:
980 else:
979 raise JSONRPCError('userid is not the same as your user')
981 raise JSONRPCError('userid is not the same as your user')
980
982
981 if pull_request.is_closed():
983 if pull_request.is_closed():
982 raise JSONRPCError(
984 raise JSONRPCError(
983 'pull request `%s` is already closed' % (pullrequestid,))
985 'pull request `%s` is already closed' % (pullrequestid,))
984
986
985 # only owner or admin or person with write permissions
987 # only owner or admin or person with write permissions
986 allowed_to_close = PullRequestModel().check_user_update(
988 allowed_to_close = PullRequestModel().check_user_update(
987 pull_request, apiuser, api=True)
989 pull_request, apiuser, api=True)
988
990
989 if not allowed_to_close:
991 if not allowed_to_close:
990 raise JSONRPCError(
992 raise JSONRPCError(
991 'pull request `%s` close failed, no permission to close.' % (
993 'pull request `%s` close failed, no permission to close.' % (
992 pullrequestid,))
994 pullrequestid,))
993
995
994 # message we're using to close the PR, else it's automatically generated
996 # message we're using to close the PR, else it's automatically generated
995 message = Optional.extract(message)
997 message = Optional.extract(message)
996
998
997 # finally close the PR, with proper message comment
999 # finally close the PR, with proper message comment
998 comment, status = PullRequestModel().close_pull_request_with_comment(
1000 comment, status = PullRequestModel().close_pull_request_with_comment(
999 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1001 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1000 status_lbl = ChangesetStatus.get_status_lbl(status)
1002 status_lbl = ChangesetStatus.get_status_lbl(status)
1001
1003
1002 Session().commit()
1004 Session().commit()
1003
1005
1004 data = {
1006 data = {
1005 'pull_request_id': pull_request.pull_request_id,
1007 'pull_request_id': pull_request.pull_request_id,
1006 'close_status': status_lbl,
1008 'close_status': status_lbl,
1007 'closed': True,
1009 'closed': True,
1008 }
1010 }
1009 return data
1011 return data
@@ -1,783 +1,782 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 import datetime
25 import datetime
26 import formencode
26 import formencode
27 import formencode.htmlfill
27 import formencode.htmlfill
28
28
29 import rhodecode
29 import rhodecode
30 from pyramid.view import view_config
30 from pyramid.view import view_config
31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
32 from pyramid.renderers import render
32 from pyramid.renderers import render
33 from pyramid.response import Response
33 from pyramid.response import Response
34
34
35 from rhodecode.apps._base import BaseAppView
35 from rhodecode.apps._base import BaseAppView
36 from rhodecode.apps._base.navigation import navigation_list
36 from rhodecode.apps._base.navigation import navigation_list
37 from rhodecode.apps.svn_support.config_keys import generate_config
37 from rhodecode.apps.svn_support.config_keys import generate_config
38 from rhodecode.lib import helpers as h
38 from rhodecode.lib import helpers as h
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 from rhodecode.lib.celerylib import tasks, run_task
41 from rhodecode.lib.celerylib import tasks, run_task
42 from rhodecode.lib.utils import repo2db_mapper
42 from rhodecode.lib.utils import repo2db_mapper
43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 from rhodecode.lib.index import searcher_from_config
44 from rhodecode.lib.index import searcher_from_config
45
45
46 from rhodecode.model.db import RhodeCodeUi, Repository
46 from rhodecode.model.db import RhodeCodeUi, Repository
47 from rhodecode.model.forms import (ApplicationSettingsForm,
47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 LabsSettingsForm, IssueTrackerPatternsForm)
49 LabsSettingsForm, IssueTrackerPatternsForm)
50 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.repo_group import RepoGroupModel
51
51
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.notification import EmailNotificationModel
53 from rhodecode.model.notification import EmailNotificationModel
54 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
55 from rhodecode.model.settings import (
55 from rhodecode.model.settings import (
56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 SettingsModel)
57 SettingsModel)
58
58
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class AdminSettingsView(BaseAppView):
63 class AdminSettingsView(BaseAppView):
64
64
65 def load_default_context(self):
65 def load_default_context(self):
66 c = self._get_local_tmpl_context()
66 c = self._get_local_tmpl_context()
67 c.labs_active = str2bool(
67 c.labs_active = str2bool(
68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 c.navlist = navigation_list(self.request)
69 c.navlist = navigation_list(self.request)
70
70
71 return c
71 return c
72
72
73 @classmethod
73 @classmethod
74 def _get_ui_settings(cls):
74 def _get_ui_settings(cls):
75 ret = RhodeCodeUi.query().all()
75 ret = RhodeCodeUi.query().all()
76
76
77 if not ret:
77 if not ret:
78 raise Exception('Could not get application ui settings !')
78 raise Exception('Could not get application ui settings !')
79 settings = {}
79 settings = {}
80 for each in ret:
80 for each in ret:
81 k = each.ui_key
81 k = each.ui_key
82 v = each.ui_value
82 v = each.ui_value
83 if k == '/':
83 if k == '/':
84 k = 'root_path'
84 k = 'root_path'
85
85
86 if k in ['push_ssl', 'publish', 'enabled']:
86 if k in ['push_ssl', 'publish', 'enabled']:
87 v = str2bool(v)
87 v = str2bool(v)
88
88
89 if k.find('.') != -1:
89 if k.find('.') != -1:
90 k = k.replace('.', '_')
90 k = k.replace('.', '_')
91
91
92 if each.ui_section in ['hooks', 'extensions']:
92 if each.ui_section in ['hooks', 'extensions']:
93 v = each.ui_active
93 v = each.ui_active
94
94
95 settings[each.ui_section + '_' + k] = v
95 settings[each.ui_section + '_' + k] = v
96 return settings
96 return settings
97
97
98 @classmethod
98 @classmethod
99 def _form_defaults(cls):
99 def _form_defaults(cls):
100 defaults = SettingsModel().get_all_settings()
100 defaults = SettingsModel().get_all_settings()
101 defaults.update(cls._get_ui_settings())
101 defaults.update(cls._get_ui_settings())
102
102
103 defaults.update({
103 defaults.update({
104 'new_svn_branch': '',
104 'new_svn_branch': '',
105 'new_svn_tag': '',
105 'new_svn_tag': '',
106 })
106 })
107 return defaults
107 return defaults
108
108
109 @LoginRequired()
109 @LoginRequired()
110 @HasPermissionAllDecorator('hg.admin')
110 @HasPermissionAllDecorator('hg.admin')
111 @view_config(
111 @view_config(
112 route_name='admin_settings_vcs', request_method='GET',
112 route_name='admin_settings_vcs', request_method='GET',
113 renderer='rhodecode:templates/admin/settings/settings.mako')
113 renderer='rhodecode:templates/admin/settings/settings.mako')
114 def settings_vcs(self):
114 def settings_vcs(self):
115 c = self.load_default_context()
115 c = self.load_default_context()
116 c.active = 'vcs'
116 c.active = 'vcs'
117 model = VcsSettingsModel()
117 model = VcsSettingsModel()
118 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
118 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
119 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
119 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
120
120
121 settings = self.request.registry.settings
121 settings = self.request.registry.settings
122 c.svn_proxy_generate_config = settings[generate_config]
122 c.svn_proxy_generate_config = settings[generate_config]
123
123
124 defaults = self._form_defaults()
124 defaults = self._form_defaults()
125
125
126 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
126 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
127
127
128 data = render('rhodecode:templates/admin/settings/settings.mako',
128 data = render('rhodecode:templates/admin/settings/settings.mako',
129 self._get_template_context(c), self.request)
129 self._get_template_context(c), self.request)
130 html = formencode.htmlfill.render(
130 html = formencode.htmlfill.render(
131 data,
131 data,
132 defaults=defaults,
132 defaults=defaults,
133 encoding="UTF-8",
133 encoding="UTF-8",
134 force_defaults=False
134 force_defaults=False
135 )
135 )
136 return Response(html)
136 return Response(html)
137
137
138 @LoginRequired()
138 @LoginRequired()
139 @HasPermissionAllDecorator('hg.admin')
139 @HasPermissionAllDecorator('hg.admin')
140 @CSRFRequired()
140 @CSRFRequired()
141 @view_config(
141 @view_config(
142 route_name='admin_settings_vcs_update', request_method='POST',
142 route_name='admin_settings_vcs_update', request_method='POST',
143 renderer='rhodecode:templates/admin/settings/settings.mako')
143 renderer='rhodecode:templates/admin/settings/settings.mako')
144 def settings_vcs_update(self):
144 def settings_vcs_update(self):
145 _ = self.request.translate
145 _ = self.request.translate
146 c = self.load_default_context()
146 c = self.load_default_context()
147 c.active = 'vcs'
147 c.active = 'vcs'
148
148
149 model = VcsSettingsModel()
149 model = VcsSettingsModel()
150 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
150 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
151 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
151 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
152
152
153 settings = self.request.registry.settings
153 settings = self.request.registry.settings
154 c.svn_proxy_generate_config = settings[generate_config]
154 c.svn_proxy_generate_config = settings[generate_config]
155
155
156 application_form = ApplicationUiSettingsForm(self.request.translate)()
156 application_form = ApplicationUiSettingsForm(self.request.translate)()
157
157
158 try:
158 try:
159 form_result = application_form.to_python(dict(self.request.POST))
159 form_result = application_form.to_python(dict(self.request.POST))
160 except formencode.Invalid as errors:
160 except formencode.Invalid as errors:
161 h.flash(
161 h.flash(
162 _("Some form inputs contain invalid data."),
162 _("Some form inputs contain invalid data."),
163 category='error')
163 category='error')
164 data = render('rhodecode:templates/admin/settings/settings.mako',
164 data = render('rhodecode:templates/admin/settings/settings.mako',
165 self._get_template_context(c), self.request)
165 self._get_template_context(c), self.request)
166 html = formencode.htmlfill.render(
166 html = formencode.htmlfill.render(
167 data,
167 data,
168 defaults=errors.value,
168 defaults=errors.value,
169 errors=errors.error_dict or {},
169 errors=errors.error_dict or {},
170 prefix_error=False,
170 prefix_error=False,
171 encoding="UTF-8",
171 encoding="UTF-8",
172 force_defaults=False
172 force_defaults=False
173 )
173 )
174 return Response(html)
174 return Response(html)
175
175
176 try:
176 try:
177 if c.visual.allow_repo_location_change:
177 if c.visual.allow_repo_location_change:
178 model.update_global_path_setting(form_result['paths_root_path'])
178 model.update_global_path_setting(form_result['paths_root_path'])
179
179
180 model.update_global_ssl_setting(form_result['web_push_ssl'])
180 model.update_global_ssl_setting(form_result['web_push_ssl'])
181 model.update_global_hook_settings(form_result)
181 model.update_global_hook_settings(form_result)
182
182
183 model.create_or_update_global_svn_settings(form_result)
183 model.create_or_update_global_svn_settings(form_result)
184 model.create_or_update_global_hg_settings(form_result)
184 model.create_or_update_global_hg_settings(form_result)
185 model.create_or_update_global_git_settings(form_result)
185 model.create_or_update_global_git_settings(form_result)
186 model.create_or_update_global_pr_settings(form_result)
186 model.create_or_update_global_pr_settings(form_result)
187 except Exception:
187 except Exception:
188 log.exception("Exception while updating settings")
188 log.exception("Exception while updating settings")
189 h.flash(_('Error occurred during updating '
189 h.flash(_('Error occurred during updating '
190 'application settings'), category='error')
190 'application settings'), category='error')
191 else:
191 else:
192 Session().commit()
192 Session().commit()
193 h.flash(_('Updated VCS settings'), category='success')
193 h.flash(_('Updated VCS settings'), category='success')
194 raise HTTPFound(h.route_path('admin_settings_vcs'))
194 raise HTTPFound(h.route_path('admin_settings_vcs'))
195
195
196 data = render('rhodecode:templates/admin/settings/settings.mako',
196 data = render('rhodecode:templates/admin/settings/settings.mako',
197 self._get_template_context(c), self.request)
197 self._get_template_context(c), self.request)
198 html = formencode.htmlfill.render(
198 html = formencode.htmlfill.render(
199 data,
199 data,
200 defaults=self._form_defaults(),
200 defaults=self._form_defaults(),
201 encoding="UTF-8",
201 encoding="UTF-8",
202 force_defaults=False
202 force_defaults=False
203 )
203 )
204 return Response(html)
204 return Response(html)
205
205
206 @LoginRequired()
206 @LoginRequired()
207 @HasPermissionAllDecorator('hg.admin')
207 @HasPermissionAllDecorator('hg.admin')
208 @CSRFRequired()
208 @CSRFRequired()
209 @view_config(
209 @view_config(
210 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
210 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
211 renderer='json_ext', xhr=True)
211 renderer='json_ext', xhr=True)
212 def settings_vcs_delete_svn_pattern(self):
212 def settings_vcs_delete_svn_pattern(self):
213 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
213 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
214 model = VcsSettingsModel()
214 model = VcsSettingsModel()
215 try:
215 try:
216 model.delete_global_svn_pattern(delete_pattern_id)
216 model.delete_global_svn_pattern(delete_pattern_id)
217 except SettingNotFound:
217 except SettingNotFound:
218 log.exception(
218 log.exception(
219 'Failed to delete svn_pattern with id %s', delete_pattern_id)
219 'Failed to delete svn_pattern with id %s', delete_pattern_id)
220 raise HTTPNotFound()
220 raise HTTPNotFound()
221
221
222 Session().commit()
222 Session().commit()
223 return True
223 return True
224
224
225 @LoginRequired()
225 @LoginRequired()
226 @HasPermissionAllDecorator('hg.admin')
226 @HasPermissionAllDecorator('hg.admin')
227 @view_config(
227 @view_config(
228 route_name='admin_settings_mapping', request_method='GET',
228 route_name='admin_settings_mapping', request_method='GET',
229 renderer='rhodecode:templates/admin/settings/settings.mako')
229 renderer='rhodecode:templates/admin/settings/settings.mako')
230 def settings_mapping(self):
230 def settings_mapping(self):
231 c = self.load_default_context()
231 c = self.load_default_context()
232 c.active = 'mapping'
232 c.active = 'mapping'
233
233
234 data = render('rhodecode:templates/admin/settings/settings.mako',
234 data = render('rhodecode:templates/admin/settings/settings.mako',
235 self._get_template_context(c), self.request)
235 self._get_template_context(c), self.request)
236 html = formencode.htmlfill.render(
236 html = formencode.htmlfill.render(
237 data,
237 data,
238 defaults=self._form_defaults(),
238 defaults=self._form_defaults(),
239 encoding="UTF-8",
239 encoding="UTF-8",
240 force_defaults=False
240 force_defaults=False
241 )
241 )
242 return Response(html)
242 return Response(html)
243
243
244 @LoginRequired()
244 @LoginRequired()
245 @HasPermissionAllDecorator('hg.admin')
245 @HasPermissionAllDecorator('hg.admin')
246 @CSRFRequired()
246 @CSRFRequired()
247 @view_config(
247 @view_config(
248 route_name='admin_settings_mapping_update', request_method='POST',
248 route_name='admin_settings_mapping_update', request_method='POST',
249 renderer='rhodecode:templates/admin/settings/settings.mako')
249 renderer='rhodecode:templates/admin/settings/settings.mako')
250 def settings_mapping_update(self):
250 def settings_mapping_update(self):
251 _ = self.request.translate
251 _ = self.request.translate
252 c = self.load_default_context()
252 c = self.load_default_context()
253 c.active = 'mapping'
253 c.active = 'mapping'
254 rm_obsolete = self.request.POST.get('destroy', False)
254 rm_obsolete = self.request.POST.get('destroy', False)
255 invalidate_cache = self.request.POST.get('invalidate', False)
255 invalidate_cache = self.request.POST.get('invalidate', False)
256 log.debug(
256 log.debug(
257 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
257 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
258
258
259 if invalidate_cache:
259 if invalidate_cache:
260 log.debug('invalidating all repositories cache')
260 log.debug('invalidating all repositories cache')
261 for repo in Repository.get_all():
261 for repo in Repository.get_all():
262 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
262 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
263
263
264 filesystem_repos = ScmModel().repo_scan()
264 filesystem_repos = ScmModel().repo_scan()
265 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
265 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
266 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
266 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
267 h.flash(_('Repositories successfully '
267 h.flash(_('Repositories successfully '
268 'rescanned added: %s ; removed: %s') %
268 'rescanned added: %s ; removed: %s') %
269 (_repr(added), _repr(removed)),
269 (_repr(added), _repr(removed)),
270 category='success')
270 category='success')
271 raise HTTPFound(h.route_path('admin_settings_mapping'))
271 raise HTTPFound(h.route_path('admin_settings_mapping'))
272
272
273 @LoginRequired()
273 @LoginRequired()
274 @HasPermissionAllDecorator('hg.admin')
274 @HasPermissionAllDecorator('hg.admin')
275 @view_config(
275 @view_config(
276 route_name='admin_settings', request_method='GET',
276 route_name='admin_settings', request_method='GET',
277 renderer='rhodecode:templates/admin/settings/settings.mako')
277 renderer='rhodecode:templates/admin/settings/settings.mako')
278 @view_config(
278 @view_config(
279 route_name='admin_settings_global', request_method='GET',
279 route_name='admin_settings_global', request_method='GET',
280 renderer='rhodecode:templates/admin/settings/settings.mako')
280 renderer='rhodecode:templates/admin/settings/settings.mako')
281 def settings_global(self):
281 def settings_global(self):
282 c = self.load_default_context()
282 c = self.load_default_context()
283 c.active = 'global'
283 c.active = 'global'
284 c.personal_repo_group_default_pattern = RepoGroupModel()\
284 c.personal_repo_group_default_pattern = RepoGroupModel()\
285 .get_personal_group_name_pattern()
285 .get_personal_group_name_pattern()
286
286
287 data = render('rhodecode:templates/admin/settings/settings.mako',
287 data = render('rhodecode:templates/admin/settings/settings.mako',
288 self._get_template_context(c), self.request)
288 self._get_template_context(c), self.request)
289 html = formencode.htmlfill.render(
289 html = formencode.htmlfill.render(
290 data,
290 data,
291 defaults=self._form_defaults(),
291 defaults=self._form_defaults(),
292 encoding="UTF-8",
292 encoding="UTF-8",
293 force_defaults=False
293 force_defaults=False
294 )
294 )
295 return Response(html)
295 return Response(html)
296
296
297 @LoginRequired()
297 @LoginRequired()
298 @HasPermissionAllDecorator('hg.admin')
298 @HasPermissionAllDecorator('hg.admin')
299 @CSRFRequired()
299 @CSRFRequired()
300 @view_config(
300 @view_config(
301 route_name='admin_settings_update', request_method='POST',
301 route_name='admin_settings_update', request_method='POST',
302 renderer='rhodecode:templates/admin/settings/settings.mako')
302 renderer='rhodecode:templates/admin/settings/settings.mako')
303 @view_config(
303 @view_config(
304 route_name='admin_settings_global_update', request_method='POST',
304 route_name='admin_settings_global_update', request_method='POST',
305 renderer='rhodecode:templates/admin/settings/settings.mako')
305 renderer='rhodecode:templates/admin/settings/settings.mako')
306 def settings_global_update(self):
306 def settings_global_update(self):
307 _ = self.request.translate
307 _ = self.request.translate
308 c = self.load_default_context()
308 c = self.load_default_context()
309 c.active = 'global'
309 c.active = 'global'
310 c.personal_repo_group_default_pattern = RepoGroupModel()\
310 c.personal_repo_group_default_pattern = RepoGroupModel()\
311 .get_personal_group_name_pattern()
311 .get_personal_group_name_pattern()
312 application_form = ApplicationSettingsForm(self.request.translate)()
312 application_form = ApplicationSettingsForm(self.request.translate)()
313 try:
313 try:
314 form_result = application_form.to_python(dict(self.request.POST))
314 form_result = application_form.to_python(dict(self.request.POST))
315 except formencode.Invalid as errors:
315 except formencode.Invalid as errors:
316 h.flash(
316 h.flash(
317 _("Some form inputs contain invalid data."),
317 _("Some form inputs contain invalid data."),
318 category='error')
318 category='error')
319 data = render('rhodecode:templates/admin/settings/settings.mako',
319 data = render('rhodecode:templates/admin/settings/settings.mako',
320 self._get_template_context(c), self.request)
320 self._get_template_context(c), self.request)
321 html = formencode.htmlfill.render(
321 html = formencode.htmlfill.render(
322 data,
322 data,
323 defaults=errors.value,
323 defaults=errors.value,
324 errors=errors.error_dict or {},
324 errors=errors.error_dict or {},
325 prefix_error=False,
325 prefix_error=False,
326 encoding="UTF-8",
326 encoding="UTF-8",
327 force_defaults=False
327 force_defaults=False
328 )
328 )
329 return Response(html)
329 return Response(html)
330
330
331 settings = [
331 settings = [
332 ('title', 'rhodecode_title', 'unicode'),
332 ('title', 'rhodecode_title', 'unicode'),
333 ('realm', 'rhodecode_realm', 'unicode'),
333 ('realm', 'rhodecode_realm', 'unicode'),
334 ('pre_code', 'rhodecode_pre_code', 'unicode'),
334 ('pre_code', 'rhodecode_pre_code', 'unicode'),
335 ('post_code', 'rhodecode_post_code', 'unicode'),
335 ('post_code', 'rhodecode_post_code', 'unicode'),
336 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
336 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
337 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
337 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
338 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
338 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
339 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
339 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
340 ]
340 ]
341 try:
341 try:
342 for setting, form_key, type_ in settings:
342 for setting, form_key, type_ in settings:
343 sett = SettingsModel().create_or_update_setting(
343 sett = SettingsModel().create_or_update_setting(
344 setting, form_result[form_key], type_)
344 setting, form_result[form_key], type_)
345 Session().add(sett)
345 Session().add(sett)
346
346
347 Session().commit()
347 Session().commit()
348 SettingsModel().invalidate_settings_cache()
348 SettingsModel().invalidate_settings_cache()
349 h.flash(_('Updated application settings'), category='success')
349 h.flash(_('Updated application settings'), category='success')
350 except Exception:
350 except Exception:
351 log.exception("Exception while updating application settings")
351 log.exception("Exception while updating application settings")
352 h.flash(
352 h.flash(
353 _('Error occurred during updating application settings'),
353 _('Error occurred during updating application settings'),
354 category='error')
354 category='error')
355
355
356 raise HTTPFound(h.route_path('admin_settings_global'))
356 raise HTTPFound(h.route_path('admin_settings_global'))
357
357
358 @LoginRequired()
358 @LoginRequired()
359 @HasPermissionAllDecorator('hg.admin')
359 @HasPermissionAllDecorator('hg.admin')
360 @view_config(
360 @view_config(
361 route_name='admin_settings_visual', request_method='GET',
361 route_name='admin_settings_visual', request_method='GET',
362 renderer='rhodecode:templates/admin/settings/settings.mako')
362 renderer='rhodecode:templates/admin/settings/settings.mako')
363 def settings_visual(self):
363 def settings_visual(self):
364 c = self.load_default_context()
364 c = self.load_default_context()
365 c.active = 'visual'
365 c.active = 'visual'
366
366
367 data = render('rhodecode:templates/admin/settings/settings.mako',
367 data = render('rhodecode:templates/admin/settings/settings.mako',
368 self._get_template_context(c), self.request)
368 self._get_template_context(c), self.request)
369 html = formencode.htmlfill.render(
369 html = formencode.htmlfill.render(
370 data,
370 data,
371 defaults=self._form_defaults(),
371 defaults=self._form_defaults(),
372 encoding="UTF-8",
372 encoding="UTF-8",
373 force_defaults=False
373 force_defaults=False
374 )
374 )
375 return Response(html)
375 return Response(html)
376
376
377 @LoginRequired()
377 @LoginRequired()
378 @HasPermissionAllDecorator('hg.admin')
378 @HasPermissionAllDecorator('hg.admin')
379 @CSRFRequired()
379 @CSRFRequired()
380 @view_config(
380 @view_config(
381 route_name='admin_settings_visual_update', request_method='POST',
381 route_name='admin_settings_visual_update', request_method='POST',
382 renderer='rhodecode:templates/admin/settings/settings.mako')
382 renderer='rhodecode:templates/admin/settings/settings.mako')
383 def settings_visual_update(self):
383 def settings_visual_update(self):
384 _ = self.request.translate
384 _ = self.request.translate
385 c = self.load_default_context()
385 c = self.load_default_context()
386 c.active = 'visual'
386 c.active = 'visual'
387 application_form = ApplicationVisualisationForm(self.request.translate)()
387 application_form = ApplicationVisualisationForm(self.request.translate)()
388 try:
388 try:
389 form_result = application_form.to_python(dict(self.request.POST))
389 form_result = application_form.to_python(dict(self.request.POST))
390 except formencode.Invalid as errors:
390 except formencode.Invalid as errors:
391 h.flash(
391 h.flash(
392 _("Some form inputs contain invalid data."),
392 _("Some form inputs contain invalid data."),
393 category='error')
393 category='error')
394 data = render('rhodecode:templates/admin/settings/settings.mako',
394 data = render('rhodecode:templates/admin/settings/settings.mako',
395 self._get_template_context(c), self.request)
395 self._get_template_context(c), self.request)
396 html = formencode.htmlfill.render(
396 html = formencode.htmlfill.render(
397 data,
397 data,
398 defaults=errors.value,
398 defaults=errors.value,
399 errors=errors.error_dict or {},
399 errors=errors.error_dict or {},
400 prefix_error=False,
400 prefix_error=False,
401 encoding="UTF-8",
401 encoding="UTF-8",
402 force_defaults=False
402 force_defaults=False
403 )
403 )
404 return Response(html)
404 return Response(html)
405
405
406 try:
406 try:
407 settings = [
407 settings = [
408 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
408 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
409 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
409 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
410 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
410 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
411 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
411 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
412 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
412 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
413 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
413 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
414 ('show_version', 'rhodecode_show_version', 'bool'),
414 ('show_version', 'rhodecode_show_version', 'bool'),
415 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
415 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
416 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
416 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
417 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
417 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
418 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
418 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
419 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
419 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
420 ('support_url', 'rhodecode_support_url', 'unicode'),
420 ('support_url', 'rhodecode_support_url', 'unicode'),
421 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
421 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
422 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
422 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
423 ]
423 ]
424 for setting, form_key, type_ in settings:
424 for setting, form_key, type_ in settings:
425 sett = SettingsModel().create_or_update_setting(
425 sett = SettingsModel().create_or_update_setting(
426 setting, form_result[form_key], type_)
426 setting, form_result[form_key], type_)
427 Session().add(sett)
427 Session().add(sett)
428
428
429 Session().commit()
429 Session().commit()
430 SettingsModel().invalidate_settings_cache()
430 SettingsModel().invalidate_settings_cache()
431 h.flash(_('Updated visualisation settings'), category='success')
431 h.flash(_('Updated visualisation settings'), category='success')
432 except Exception:
432 except Exception:
433 log.exception("Exception updating visualization settings")
433 log.exception("Exception updating visualization settings")
434 h.flash(_('Error occurred during updating '
434 h.flash(_('Error occurred during updating '
435 'visualisation settings'),
435 'visualisation settings'),
436 category='error')
436 category='error')
437
437
438 raise HTTPFound(h.route_path('admin_settings_visual'))
438 raise HTTPFound(h.route_path('admin_settings_visual'))
439
439
440 @LoginRequired()
440 @LoginRequired()
441 @HasPermissionAllDecorator('hg.admin')
441 @HasPermissionAllDecorator('hg.admin')
442 @view_config(
442 @view_config(
443 route_name='admin_settings_issuetracker', request_method='GET',
443 route_name='admin_settings_issuetracker', request_method='GET',
444 renderer='rhodecode:templates/admin/settings/settings.mako')
444 renderer='rhodecode:templates/admin/settings/settings.mako')
445 def settings_issuetracker(self):
445 def settings_issuetracker(self):
446 c = self.load_default_context()
446 c = self.load_default_context()
447 c.active = 'issuetracker'
447 c.active = 'issuetracker'
448 defaults = c.rc_config
448 defaults = c.rc_config
449
449
450 entry_key = 'rhodecode_issuetracker_pat_'
450 entry_key = 'rhodecode_issuetracker_pat_'
451
451
452 c.issuetracker_entries = {}
452 c.issuetracker_entries = {}
453 for k, v in defaults.items():
453 for k, v in defaults.items():
454 if k.startswith(entry_key):
454 if k.startswith(entry_key):
455 uid = k[len(entry_key):]
455 uid = k[len(entry_key):]
456 c.issuetracker_entries[uid] = None
456 c.issuetracker_entries[uid] = None
457
457
458 for uid in c.issuetracker_entries:
458 for uid in c.issuetracker_entries:
459 c.issuetracker_entries[uid] = AttributeDict({
459 c.issuetracker_entries[uid] = AttributeDict({
460 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
460 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
461 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
461 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
462 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
462 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
463 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
463 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
464 })
464 })
465
465
466 return self._get_template_context(c)
466 return self._get_template_context(c)
467
467
468 @LoginRequired()
468 @LoginRequired()
469 @HasPermissionAllDecorator('hg.admin')
469 @HasPermissionAllDecorator('hg.admin')
470 @CSRFRequired()
470 @CSRFRequired()
471 @view_config(
471 @view_config(
472 route_name='admin_settings_issuetracker_test', request_method='POST',
472 route_name='admin_settings_issuetracker_test', request_method='POST',
473 renderer='string', xhr=True)
473 renderer='string', xhr=True)
474 def settings_issuetracker_test(self):
474 def settings_issuetracker_test(self):
475 return h.urlify_commit_message(
475 return h.urlify_commit_message(
476 self.request.POST.get('test_text', ''),
476 self.request.POST.get('test_text', ''),
477 'repo_group/test_repo1')
477 'repo_group/test_repo1')
478
478
479 @LoginRequired()
479 @LoginRequired()
480 @HasPermissionAllDecorator('hg.admin')
480 @HasPermissionAllDecorator('hg.admin')
481 @CSRFRequired()
481 @CSRFRequired()
482 @view_config(
482 @view_config(
483 route_name='admin_settings_issuetracker_update', request_method='POST',
483 route_name='admin_settings_issuetracker_update', request_method='POST',
484 renderer='rhodecode:templates/admin/settings/settings.mako')
484 renderer='rhodecode:templates/admin/settings/settings.mako')
485 def settings_issuetracker_update(self):
485 def settings_issuetracker_update(self):
486 _ = self.request.translate
486 _ = self.request.translate
487 self.load_default_context()
487 self.load_default_context()
488 settings_model = IssueTrackerSettingsModel()
488 settings_model = IssueTrackerSettingsModel()
489
489
490 try:
490 try:
491 form = IssueTrackerPatternsForm(self.request.translate)()
491 form = IssueTrackerPatternsForm(self.request.translate)()
492 data = form.to_python(self.request.POST)
492 data = form.to_python(self.request.POST)
493 except formencode.Invalid as errors:
493 except formencode.Invalid as errors:
494 log.exception('Failed to add new pattern')
494 log.exception('Failed to add new pattern')
495 error = errors
495 error = errors
496 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
496 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
497 category='error')
497 category='error')
498 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
498 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
499
499
500 if data:
500 if data:
501 for uid in data.get('delete_patterns', []):
501 for uid in data.get('delete_patterns', []):
502 settings_model.delete_entries(uid)
502 settings_model.delete_entries(uid)
503
503
504 for pattern in data.get('patterns', []):
504 for pattern in data.get('patterns', []):
505 for setting, value, type_ in pattern:
505 for setting, value, type_ in pattern:
506 sett = settings_model.create_or_update_setting(
506 sett = settings_model.create_or_update_setting(
507 setting, value, type_)
507 setting, value, type_)
508 Session().add(sett)
508 Session().add(sett)
509
509
510 Session().commit()
510 Session().commit()
511
511
512 SettingsModel().invalidate_settings_cache()
512 SettingsModel().invalidate_settings_cache()
513 h.flash(_('Updated issue tracker entries'), category='success')
513 h.flash(_('Updated issue tracker entries'), category='success')
514 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
514 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
515
515
516 @LoginRequired()
516 @LoginRequired()
517 @HasPermissionAllDecorator('hg.admin')
517 @HasPermissionAllDecorator('hg.admin')
518 @CSRFRequired()
518 @CSRFRequired()
519 @view_config(
519 @view_config(
520 route_name='admin_settings_issuetracker_delete', request_method='POST',
520 route_name='admin_settings_issuetracker_delete', request_method='POST',
521 renderer='json_ext', xhr=True)
521 renderer='json_ext', xhr=True)
522 def settings_issuetracker_delete(self):
522 def settings_issuetracker_delete(self):
523 _ = self.request.translate
523 _ = self.request.translate
524 self.load_default_context()
524 self.load_default_context()
525 uid = self.request.POST.get('uid')
525 uid = self.request.POST.get('uid')
526 try:
526 try:
527 IssueTrackerSettingsModel().delete_entries(uid)
527 IssueTrackerSettingsModel().delete_entries(uid)
528 except Exception:
528 except Exception:
529 log.exception('Failed to delete issue tracker setting %s', uid)
529 log.exception('Failed to delete issue tracker setting %s', uid)
530 raise HTTPNotFound()
530 raise HTTPNotFound()
531
531
532 SettingsModel().invalidate_settings_cache()
532 SettingsModel().invalidate_settings_cache()
533 h.flash(_('Removed issue tracker entry.'), category='success')
533 h.flash(_('Removed issue tracker entry.'), category='success')
534
534
535 return {'deleted': uid}
535 return {'deleted': uid}
536
536
537 @LoginRequired()
537 @LoginRequired()
538 @HasPermissionAllDecorator('hg.admin')
538 @HasPermissionAllDecorator('hg.admin')
539 @view_config(
539 @view_config(
540 route_name='admin_settings_email', request_method='GET',
540 route_name='admin_settings_email', request_method='GET',
541 renderer='rhodecode:templates/admin/settings/settings.mako')
541 renderer='rhodecode:templates/admin/settings/settings.mako')
542 def settings_email(self):
542 def settings_email(self):
543 c = self.load_default_context()
543 c = self.load_default_context()
544 c.active = 'email'
544 c.active = 'email'
545 c.rhodecode_ini = rhodecode.CONFIG
545 c.rhodecode_ini = rhodecode.CONFIG
546
546
547 data = render('rhodecode:templates/admin/settings/settings.mako',
547 data = render('rhodecode:templates/admin/settings/settings.mako',
548 self._get_template_context(c), self.request)
548 self._get_template_context(c), self.request)
549 html = formencode.htmlfill.render(
549 html = formencode.htmlfill.render(
550 data,
550 data,
551 defaults=self._form_defaults(),
551 defaults=self._form_defaults(),
552 encoding="UTF-8",
552 encoding="UTF-8",
553 force_defaults=False
553 force_defaults=False
554 )
554 )
555 return Response(html)
555 return Response(html)
556
556
557 @LoginRequired()
557 @LoginRequired()
558 @HasPermissionAllDecorator('hg.admin')
558 @HasPermissionAllDecorator('hg.admin')
559 @CSRFRequired()
559 @CSRFRequired()
560 @view_config(
560 @view_config(
561 route_name='admin_settings_email_update', request_method='POST',
561 route_name='admin_settings_email_update', request_method='POST',
562 renderer='rhodecode:templates/admin/settings/settings.mako')
562 renderer='rhodecode:templates/admin/settings/settings.mako')
563 def settings_email_update(self):
563 def settings_email_update(self):
564 _ = self.request.translate
564 _ = self.request.translate
565 c = self.load_default_context()
565 c = self.load_default_context()
566 c.active = 'email'
566 c.active = 'email'
567
567
568 test_email = self.request.POST.get('test_email')
568 test_email = self.request.POST.get('test_email')
569
569
570 if not test_email:
570 if not test_email:
571 h.flash(_('Please enter email address'), category='error')
571 h.flash(_('Please enter email address'), category='error')
572 raise HTTPFound(h.route_path('admin_settings_email'))
572 raise HTTPFound(h.route_path('admin_settings_email'))
573
573
574 email_kwargs = {
574 email_kwargs = {
575 'date': datetime.datetime.now(),
575 'date': datetime.datetime.now(),
576 'user': c.rhodecode_user,
576 'user': c.rhodecode_user
577 'rhodecode_version': c.rhodecode_version
578 }
577 }
579
578
580 (subject, headers, email_body,
579 (subject, headers, email_body,
581 email_body_plaintext) = EmailNotificationModel().render_email(
580 email_body_plaintext) = EmailNotificationModel().render_email(
582 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
581 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
583
582
584 recipients = [test_email] if test_email else None
583 recipients = [test_email] if test_email else None
585
584
586 run_task(tasks.send_email, recipients, subject,
585 run_task(tasks.send_email, recipients, subject,
587 email_body_plaintext, email_body)
586 email_body_plaintext, email_body)
588
587
589 h.flash(_('Send email task created'), category='success')
588 h.flash(_('Send email task created'), category='success')
590 raise HTTPFound(h.route_path('admin_settings_email'))
589 raise HTTPFound(h.route_path('admin_settings_email'))
591
590
592 @LoginRequired()
591 @LoginRequired()
593 @HasPermissionAllDecorator('hg.admin')
592 @HasPermissionAllDecorator('hg.admin')
594 @view_config(
593 @view_config(
595 route_name='admin_settings_hooks', request_method='GET',
594 route_name='admin_settings_hooks', request_method='GET',
596 renderer='rhodecode:templates/admin/settings/settings.mako')
595 renderer='rhodecode:templates/admin/settings/settings.mako')
597 def settings_hooks(self):
596 def settings_hooks(self):
598 c = self.load_default_context()
597 c = self.load_default_context()
599 c.active = 'hooks'
598 c.active = 'hooks'
600
599
601 model = SettingsModel()
600 model = SettingsModel()
602 c.hooks = model.get_builtin_hooks()
601 c.hooks = model.get_builtin_hooks()
603 c.custom_hooks = model.get_custom_hooks()
602 c.custom_hooks = model.get_custom_hooks()
604
603
605 data = render('rhodecode:templates/admin/settings/settings.mako',
604 data = render('rhodecode:templates/admin/settings/settings.mako',
606 self._get_template_context(c), self.request)
605 self._get_template_context(c), self.request)
607 html = formencode.htmlfill.render(
606 html = formencode.htmlfill.render(
608 data,
607 data,
609 defaults=self._form_defaults(),
608 defaults=self._form_defaults(),
610 encoding="UTF-8",
609 encoding="UTF-8",
611 force_defaults=False
610 force_defaults=False
612 )
611 )
613 return Response(html)
612 return Response(html)
614
613
615 @LoginRequired()
614 @LoginRequired()
616 @HasPermissionAllDecorator('hg.admin')
615 @HasPermissionAllDecorator('hg.admin')
617 @CSRFRequired()
616 @CSRFRequired()
618 @view_config(
617 @view_config(
619 route_name='admin_settings_hooks_update', request_method='POST',
618 route_name='admin_settings_hooks_update', request_method='POST',
620 renderer='rhodecode:templates/admin/settings/settings.mako')
619 renderer='rhodecode:templates/admin/settings/settings.mako')
621 @view_config(
620 @view_config(
622 route_name='admin_settings_hooks_delete', request_method='POST',
621 route_name='admin_settings_hooks_delete', request_method='POST',
623 renderer='rhodecode:templates/admin/settings/settings.mako')
622 renderer='rhodecode:templates/admin/settings/settings.mako')
624 def settings_hooks_update(self):
623 def settings_hooks_update(self):
625 _ = self.request.translate
624 _ = self.request.translate
626 c = self.load_default_context()
625 c = self.load_default_context()
627 c.active = 'hooks'
626 c.active = 'hooks'
628 if c.visual.allow_custom_hooks_settings:
627 if c.visual.allow_custom_hooks_settings:
629 ui_key = self.request.POST.get('new_hook_ui_key')
628 ui_key = self.request.POST.get('new_hook_ui_key')
630 ui_value = self.request.POST.get('new_hook_ui_value')
629 ui_value = self.request.POST.get('new_hook_ui_value')
631
630
632 hook_id = self.request.POST.get('hook_id')
631 hook_id = self.request.POST.get('hook_id')
633 new_hook = False
632 new_hook = False
634
633
635 model = SettingsModel()
634 model = SettingsModel()
636 try:
635 try:
637 if ui_value and ui_key:
636 if ui_value and ui_key:
638 model.create_or_update_hook(ui_key, ui_value)
637 model.create_or_update_hook(ui_key, ui_value)
639 h.flash(_('Added new hook'), category='success')
638 h.flash(_('Added new hook'), category='success')
640 new_hook = True
639 new_hook = True
641 elif hook_id:
640 elif hook_id:
642 RhodeCodeUi.delete(hook_id)
641 RhodeCodeUi.delete(hook_id)
643 Session().commit()
642 Session().commit()
644
643
645 # check for edits
644 # check for edits
646 update = False
645 update = False
647 _d = self.request.POST.dict_of_lists()
646 _d = self.request.POST.dict_of_lists()
648 for k, v in zip(_d.get('hook_ui_key', []),
647 for k, v in zip(_d.get('hook_ui_key', []),
649 _d.get('hook_ui_value_new', [])):
648 _d.get('hook_ui_value_new', [])):
650 model.create_or_update_hook(k, v)
649 model.create_or_update_hook(k, v)
651 update = True
650 update = True
652
651
653 if update and not new_hook:
652 if update and not new_hook:
654 h.flash(_('Updated hooks'), category='success')
653 h.flash(_('Updated hooks'), category='success')
655 Session().commit()
654 Session().commit()
656 except Exception:
655 except Exception:
657 log.exception("Exception during hook creation")
656 log.exception("Exception during hook creation")
658 h.flash(_('Error occurred during hook creation'),
657 h.flash(_('Error occurred during hook creation'),
659 category='error')
658 category='error')
660
659
661 raise HTTPFound(h.route_path('admin_settings_hooks'))
660 raise HTTPFound(h.route_path('admin_settings_hooks'))
662
661
663 @LoginRequired()
662 @LoginRequired()
664 @HasPermissionAllDecorator('hg.admin')
663 @HasPermissionAllDecorator('hg.admin')
665 @view_config(
664 @view_config(
666 route_name='admin_settings_search', request_method='GET',
665 route_name='admin_settings_search', request_method='GET',
667 renderer='rhodecode:templates/admin/settings/settings.mako')
666 renderer='rhodecode:templates/admin/settings/settings.mako')
668 def settings_search(self):
667 def settings_search(self):
669 c = self.load_default_context()
668 c = self.load_default_context()
670 c.active = 'search'
669 c.active = 'search'
671
670
672 c.searcher = searcher_from_config(self.request.registry.settings)
671 c.searcher = searcher_from_config(self.request.registry.settings)
673 c.statistics = c.searcher.statistics(self.request.translate)
672 c.statistics = c.searcher.statistics(self.request.translate)
674
673
675 return self._get_template_context(c)
674 return self._get_template_context(c)
676
675
677 @LoginRequired()
676 @LoginRequired()
678 @HasPermissionAllDecorator('hg.admin')
677 @HasPermissionAllDecorator('hg.admin')
679 @view_config(
678 @view_config(
680 route_name='admin_settings_automation', request_method='GET',
679 route_name='admin_settings_automation', request_method='GET',
681 renderer='rhodecode:templates/admin/settings/settings.mako')
680 renderer='rhodecode:templates/admin/settings/settings.mako')
682 def settings_automation(self):
681 def settings_automation(self):
683 c = self.load_default_context()
682 c = self.load_default_context()
684 c.active = 'automation'
683 c.active = 'automation'
685
684
686 return self._get_template_context(c)
685 return self._get_template_context(c)
687
686
688 @LoginRequired()
687 @LoginRequired()
689 @HasPermissionAllDecorator('hg.admin')
688 @HasPermissionAllDecorator('hg.admin')
690 @view_config(
689 @view_config(
691 route_name='admin_settings_labs', request_method='GET',
690 route_name='admin_settings_labs', request_method='GET',
692 renderer='rhodecode:templates/admin/settings/settings.mako')
691 renderer='rhodecode:templates/admin/settings/settings.mako')
693 def settings_labs(self):
692 def settings_labs(self):
694 c = self.load_default_context()
693 c = self.load_default_context()
695 if not c.labs_active:
694 if not c.labs_active:
696 raise HTTPFound(h.route_path('admin_settings'))
695 raise HTTPFound(h.route_path('admin_settings'))
697
696
698 c.active = 'labs'
697 c.active = 'labs'
699 c.lab_settings = _LAB_SETTINGS
698 c.lab_settings = _LAB_SETTINGS
700
699
701 data = render('rhodecode:templates/admin/settings/settings.mako',
700 data = render('rhodecode:templates/admin/settings/settings.mako',
702 self._get_template_context(c), self.request)
701 self._get_template_context(c), self.request)
703 html = formencode.htmlfill.render(
702 html = formencode.htmlfill.render(
704 data,
703 data,
705 defaults=self._form_defaults(),
704 defaults=self._form_defaults(),
706 encoding="UTF-8",
705 encoding="UTF-8",
707 force_defaults=False
706 force_defaults=False
708 )
707 )
709 return Response(html)
708 return Response(html)
710
709
711 @LoginRequired()
710 @LoginRequired()
712 @HasPermissionAllDecorator('hg.admin')
711 @HasPermissionAllDecorator('hg.admin')
713 @CSRFRequired()
712 @CSRFRequired()
714 @view_config(
713 @view_config(
715 route_name='admin_settings_labs_update', request_method='POST',
714 route_name='admin_settings_labs_update', request_method='POST',
716 renderer='rhodecode:templates/admin/settings/settings.mako')
715 renderer='rhodecode:templates/admin/settings/settings.mako')
717 def settings_labs_update(self):
716 def settings_labs_update(self):
718 _ = self.request.translate
717 _ = self.request.translate
719 c = self.load_default_context()
718 c = self.load_default_context()
720 c.active = 'labs'
719 c.active = 'labs'
721
720
722 application_form = LabsSettingsForm(self.request.translate)()
721 application_form = LabsSettingsForm(self.request.translate)()
723 try:
722 try:
724 form_result = application_form.to_python(dict(self.request.POST))
723 form_result = application_form.to_python(dict(self.request.POST))
725 except formencode.Invalid as errors:
724 except formencode.Invalid as errors:
726 h.flash(
725 h.flash(
727 _("Some form inputs contain invalid data."),
726 _("Some form inputs contain invalid data."),
728 category='error')
727 category='error')
729 data = render('rhodecode:templates/admin/settings/settings.mako',
728 data = render('rhodecode:templates/admin/settings/settings.mako',
730 self._get_template_context(c), self.request)
729 self._get_template_context(c), self.request)
731 html = formencode.htmlfill.render(
730 html = formencode.htmlfill.render(
732 data,
731 data,
733 defaults=errors.value,
732 defaults=errors.value,
734 errors=errors.error_dict or {},
733 errors=errors.error_dict or {},
735 prefix_error=False,
734 prefix_error=False,
736 encoding="UTF-8",
735 encoding="UTF-8",
737 force_defaults=False
736 force_defaults=False
738 )
737 )
739 return Response(html)
738 return Response(html)
740
739
741 try:
740 try:
742 session = Session()
741 session = Session()
743 for setting in _LAB_SETTINGS:
742 for setting in _LAB_SETTINGS:
744 setting_name = setting.key[len('rhodecode_'):]
743 setting_name = setting.key[len('rhodecode_'):]
745 sett = SettingsModel().create_or_update_setting(
744 sett = SettingsModel().create_or_update_setting(
746 setting_name, form_result[setting.key], setting.type)
745 setting_name, form_result[setting.key], setting.type)
747 session.add(sett)
746 session.add(sett)
748
747
749 except Exception:
748 except Exception:
750 log.exception('Exception while updating lab settings')
749 log.exception('Exception while updating lab settings')
751 h.flash(_('Error occurred during updating labs settings'),
750 h.flash(_('Error occurred during updating labs settings'),
752 category='error')
751 category='error')
753 else:
752 else:
754 Session().commit()
753 Session().commit()
755 SettingsModel().invalidate_settings_cache()
754 SettingsModel().invalidate_settings_cache()
756 h.flash(_('Updated Labs settings'), category='success')
755 h.flash(_('Updated Labs settings'), category='success')
757 raise HTTPFound(h.route_path('admin_settings_labs'))
756 raise HTTPFound(h.route_path('admin_settings_labs'))
758
757
759 data = render('rhodecode:templates/admin/settings/settings.mako',
758 data = render('rhodecode:templates/admin/settings/settings.mako',
760 self._get_template_context(c), self.request)
759 self._get_template_context(c), self.request)
761 html = formencode.htmlfill.render(
760 html = formencode.htmlfill.render(
762 data,
761 data,
763 defaults=self._form_defaults(),
762 defaults=self._form_defaults(),
764 encoding="UTF-8",
763 encoding="UTF-8",
765 force_defaults=False
764 force_defaults=False
766 )
765 )
767 return Response(html)
766 return Response(html)
768
767
769
768
770 # :param key: name of the setting including the 'rhodecode_' prefix
769 # :param key: name of the setting including the 'rhodecode_' prefix
771 # :param type: the RhodeCodeSetting type to use.
770 # :param type: the RhodeCodeSetting type to use.
772 # :param group: the i18ned group in which we should dispaly this setting
771 # :param group: the i18ned group in which we should dispaly this setting
773 # :param label: the i18ned label we should display for this setting
772 # :param label: the i18ned label we should display for this setting
774 # :param help: the i18ned help we should dispaly for this setting
773 # :param help: the i18ned help we should dispaly for this setting
775 LabSetting = collections.namedtuple(
774 LabSetting = collections.namedtuple(
776 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
775 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
777
776
778
777
779 # This list has to be kept in sync with the form
778 # This list has to be kept in sync with the form
780 # rhodecode.model.forms.LabsSettingsForm.
779 # rhodecode.model.forms.LabsSettingsForm.
781 _LAB_SETTINGS = [
780 _LAB_SETTINGS = [
782
781
783 ]
782 ]
@@ -1,347 +1,386 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 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 os
21 import os
22 import logging
22 import logging
23 import datetime
23 import datetime
24
24
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render_to_response
26 from pyramid.renderers import render_to_response
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib.celerylib import run_task, tasks
28 from rhodecode.lib.celerylib import run_task, tasks
29 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.model.db import User
30 from rhodecode.model.db import User
31 from rhodecode.model.notification import EmailNotificationModel
31 from rhodecode.model.notification import EmailNotificationModel
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class DebugStyleView(BaseAppView):
36 class DebugStyleView(BaseAppView):
37 def load_default_context(self):
37 def load_default_context(self):
38 c = self._get_local_tmpl_context()
38 c = self._get_local_tmpl_context()
39
39
40 return c
40 return c
41
41
42 @view_config(
42 @view_config(
43 route_name='debug_style_home', request_method='GET',
43 route_name='debug_style_home', request_method='GET',
44 renderer=None)
44 renderer=None)
45 def index(self):
45 def index(self):
46 c = self.load_default_context()
46 c = self.load_default_context()
47 c.active = 'index'
47 c.active = 'index'
48
48
49 return render_to_response(
49 return render_to_response(
50 'debug_style/index.html', self._get_template_context(c),
50 'debug_style/index.html', self._get_template_context(c),
51 request=self.request)
51 request=self.request)
52
52
53 @view_config(
53 @view_config(
54 route_name='debug_style_email', request_method='GET',
54 route_name='debug_style_email', request_method='GET',
55 renderer=None)
55 renderer=None)
56 @view_config(
56 @view_config(
57 route_name='debug_style_email_plain_rendered', request_method='GET',
57 route_name='debug_style_email_plain_rendered', request_method='GET',
58 renderer=None)
58 renderer=None)
59 def render_email(self):
59 def render_email(self):
60 c = self.load_default_context()
60 c = self.load_default_context()
61 email_id = self.request.matchdict['email_id']
61 email_id = self.request.matchdict['email_id']
62 c.active = 'emails'
62 c.active = 'emails'
63
63
64 pr = AttributeDict(
64 pr = AttributeDict(
65 pull_request_id=123,
65 pull_request_id=123,
66 title='digital_ocean: fix redis, elastic search start on boot, '
66 title='digital_ocean: fix redis, elastic search start on boot, '
67 'fix fd limits on supervisor, set postgres 11 version',
67 'fix fd limits on supervisor, set postgres 11 version',
68 description='''
68 description='''
69 Check if we should use full-topic or mini-topic.
69 Check if we should use full-topic or mini-topic.
70
70
71 - full topic produces some problems with merge states etc
71 - full topic produces some problems with merge states etc
72 - server-mini-topic needs probably tweeks.
72 - server-mini-topic needs probably tweeks.
73 ''',
73 ''',
74 repo_name='foobar',
74 repo_name='foobar',
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
77 )
77 )
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
81
81 # file/commit changes for PR update
82 commit_changes = AttributeDict({
83 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
84 'removed': ['eeeeeeeeeee'],
85 })
86 file_changes = AttributeDict({
87 'added': ['a/file1.md', 'file2.py'],
88 'modified': ['b/modified_file.rst'],
89 'removed': ['.idea'],
90 })
82 email_kwargs = {
91 email_kwargs = {
83 'test': {},
92 'test': {},
84 'message': {
93 'message': {
85 'body': 'message body !'
94 'body': 'message body !'
86 },
95 },
87 'email_test': {
96 'email_test': {
88 'user': user,
97 'user': user,
89 'date': datetime.datetime.now(),
98 'date': datetime.datetime.now(),
90 'rhodecode_version': c.rhodecode_version
91 },
99 },
92 'password_reset': {
100 'password_reset': {
93 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
101 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
94
102
95 'user': user,
103 'user': user,
96 'date': datetime.datetime.now(),
104 'date': datetime.datetime.now(),
97 'email': 'test@rhodecode.com',
105 'email': 'test@rhodecode.com',
98 'first_admin_email': User.get_first_super_admin().email
106 'first_admin_email': User.get_first_super_admin().email
99 },
107 },
100 'password_reset_confirmation': {
108 'password_reset_confirmation': {
101 'new_password': 'new-password-example',
109 'new_password': 'new-password-example',
102 'user': user,
110 'user': user,
103 'date': datetime.datetime.now(),
111 'date': datetime.datetime.now(),
104 'email': 'test@rhodecode.com',
112 'email': 'test@rhodecode.com',
105 'first_admin_email': User.get_first_super_admin().email
113 'first_admin_email': User.get_first_super_admin().email
106 },
114 },
107 'registration': {
115 'registration': {
108 'user': user,
116 'user': user,
109 'date': datetime.datetime.now(),
117 'date': datetime.datetime.now(),
110 },
118 },
111
119
112 'pull_request_comment': {
120 'pull_request_comment': {
113 'user': user,
121 'user': user,
114
122
115 'status_change': None,
123 'status_change': None,
116 'status_change_type': None,
124 'status_change_type': None,
117
125
118 'pull_request': pr,
126 'pull_request': pr,
119 'pull_request_commits': [],
127 'pull_request_commits': [],
120
128
121 'pull_request_target_repo': target_repo,
129 'pull_request_target_repo': target_repo,
122 'pull_request_target_repo_url': 'http://target-repo/url',
130 'pull_request_target_repo_url': 'http://target-repo/url',
123
131
124 'pull_request_source_repo': source_repo,
132 'pull_request_source_repo': source_repo,
125 'pull_request_source_repo_url': 'http://source-repo/url',
133 'pull_request_source_repo_url': 'http://source-repo/url',
126
134
127 'pull_request_url': 'http://localhost/pr1',
135 'pull_request_url': 'http://localhost/pr1',
128 'pr_comment_url': 'http://comment-url',
136 'pr_comment_url': 'http://comment-url',
129 'pr_comment_reply_url': 'http://comment-url#reply',
137 'pr_comment_reply_url': 'http://comment-url#reply',
130
138
131 'comment_file': None,
139 'comment_file': None,
132 'comment_line': None,
140 'comment_line': None,
133 'comment_type': 'note',
141 'comment_type': 'note',
134 'comment_body': 'This is my comment body. *I like !*',
142 'comment_body': 'This is my comment body. *I like !*',
135 'comment_id': 2048,
143 'comment_id': 2048,
136 'renderer_type': 'markdown',
144 'renderer_type': 'markdown',
137 'mention': True,
145 'mention': True,
138
146
139 },
147 },
140 'pull_request_comment+status': {
148 'pull_request_comment+status': {
141 'user': user,
149 'user': user,
142
150
143 'status_change': 'approved',
151 'status_change': 'approved',
144 'status_change_type': 'approved',
152 'status_change_type': 'approved',
145
153
146 'pull_request': pr,
154 'pull_request': pr,
147 'pull_request_commits': [],
155 'pull_request_commits': [],
148
156
149 'pull_request_target_repo': target_repo,
157 'pull_request_target_repo': target_repo,
150 'pull_request_target_repo_url': 'http://target-repo/url',
158 'pull_request_target_repo_url': 'http://target-repo/url',
151
159
152 'pull_request_source_repo': source_repo,
160 'pull_request_source_repo': source_repo,
153 'pull_request_source_repo_url': 'http://source-repo/url',
161 'pull_request_source_repo_url': 'http://source-repo/url',
154
162
155 'pull_request_url': 'http://localhost/pr1',
163 'pull_request_url': 'http://localhost/pr1',
156 'pr_comment_url': 'http://comment-url',
164 'pr_comment_url': 'http://comment-url',
157 'pr_comment_reply_url': 'http://comment-url#reply',
165 'pr_comment_reply_url': 'http://comment-url#reply',
158
166
159 'comment_type': 'todo',
167 'comment_type': 'todo',
160 'comment_file': None,
168 'comment_file': None,
161 'comment_line': None,
169 'comment_line': None,
162 'comment_body': '''
170 'comment_body': '''
163 I think something like this would be better
171 I think something like this would be better
164
172
165 ```py
173 ```py
166
174
167 def db():
175 def db():
168 global connection
176 global connection
169 return connection
177 return connection
170
178
171 ```
179 ```
172
180
173 ''',
181 ''',
174 'comment_id': 2048,
182 'comment_id': 2048,
175 'renderer_type': 'markdown',
183 'renderer_type': 'markdown',
176 'mention': True,
184 'mention': True,
177
185
178 },
186 },
179 'pull_request_comment+file': {
187 'pull_request_comment+file': {
180 'user': user,
188 'user': user,
181
189
182 'status_change': None,
190 'status_change': None,
183 'status_change_type': None,
191 'status_change_type': None,
184
192
185 'pull_request': pr,
193 'pull_request': pr,
186 'pull_request_commits': [],
194 'pull_request_commits': [],
187
195
188 'pull_request_target_repo': target_repo,
196 'pull_request_target_repo': target_repo,
189 'pull_request_target_repo_url': 'http://target-repo/url',
197 'pull_request_target_repo_url': 'http://target-repo/url',
190
198
191 'pull_request_source_repo': source_repo,
199 'pull_request_source_repo': source_repo,
192 'pull_request_source_repo_url': 'http://source-repo/url',
200 'pull_request_source_repo_url': 'http://source-repo/url',
193
201
194 'pull_request_url': 'http://localhost/pr1',
202 'pull_request_url': 'http://localhost/pr1',
195
203
196 'pr_comment_url': 'http://comment-url',
204 'pr_comment_url': 'http://comment-url',
197 'pr_comment_reply_url': 'http://comment-url#reply',
205 'pr_comment_reply_url': 'http://comment-url#reply',
198
206
199 'comment_file': 'rhodecode/model/db.py',
207 'comment_file': 'rhodecode/model/db.py',
200 'comment_line': 'o1210',
208 'comment_line': 'o1210',
201 'comment_type': 'todo',
209 'comment_type': 'todo',
202 'comment_body': '''
210 'comment_body': '''
203 I like this !
211 I like this !
204
212
205 But please check this code::
213 But please check this code::
206
214
207 def main():
215 def main():
208 print 'ok'
216 print 'ok'
209
217
210 This should work better !
218 This should work better !
211 ''',
219 ''',
212 'comment_id': 2048,
220 'comment_id': 2048,
213 'renderer_type': 'rst',
221 'renderer_type': 'rst',
214 'mention': True,
222 'mention': True,
215
223
216 },
224 },
217
225
226 'pull_request_update': {
227 'updating_user': user,
228
229 'status_change': None,
230 'status_change_type': None,
231
232 'pull_request': pr,
233 'pull_request_commits': [],
234
235 'pull_request_target_repo': target_repo,
236 'pull_request_target_repo_url': 'http://target-repo/url',
237
238 'pull_request_source_repo': source_repo,
239 'pull_request_source_repo_url': 'http://source-repo/url',
240
241 'pull_request_url': 'http://localhost/pr1',
242
243 # update comment links
244 'pr_comment_url': 'http://comment-url',
245 'pr_comment_reply_url': 'http://comment-url#reply',
246 'ancestor_commit_id': 'f39bd443',
247 'added_commits': commit_changes.added,
248 'removed_commits': commit_changes.removed,
249 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
250 'added_files': file_changes.added,
251 'modified_files': file_changes.modified,
252 'removed_files': file_changes.removed,
253 },
254
218 'cs_comment': {
255 'cs_comment': {
219 'user': user,
256 'user': user,
220 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
257 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
221 'status_change': None,
258 'status_change': None,
222 'status_change_type': None,
259 'status_change_type': None,
223
260
224 'commit_target_repo_url': 'http://foo.example.com/#comment1',
261 'commit_target_repo_url': 'http://foo.example.com/#comment1',
225 'repo_name': 'test-repo',
262 'repo_name': 'test-repo',
226 'comment_type': 'note',
263 'comment_type': 'note',
227 'comment_file': None,
264 'comment_file': None,
228 'comment_line': None,
265 'comment_line': None,
229 'commit_comment_url': 'http://comment-url',
266 'commit_comment_url': 'http://comment-url',
230 'commit_comment_reply_url': 'http://comment-url#reply',
267 'commit_comment_reply_url': 'http://comment-url#reply',
231 'comment_body': 'This is my comment body. *I like !*',
268 'comment_body': 'This is my comment body. *I like !*',
232 'comment_id': 2048,
269 'comment_id': 2048,
233 'renderer_type': 'markdown',
270 'renderer_type': 'markdown',
234 'mention': True,
271 'mention': True,
235 },
272 },
236 'cs_comment+status': {
273 'cs_comment+status': {
237 'user': user,
274 'user': user,
238 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
275 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
239 'status_change': 'approved',
276 'status_change': 'approved',
240 'status_change_type': 'approved',
277 'status_change_type': 'approved',
241
278
242 'commit_target_repo_url': 'http://foo.example.com/#comment1',
279 'commit_target_repo_url': 'http://foo.example.com/#comment1',
243 'repo_name': 'test-repo',
280 'repo_name': 'test-repo',
244 'comment_type': 'note',
281 'comment_type': 'note',
245 'comment_file': None,
282 'comment_file': None,
246 'comment_line': None,
283 'comment_line': None,
247 'commit_comment_url': 'http://comment-url',
284 'commit_comment_url': 'http://comment-url',
248 'commit_comment_reply_url': 'http://comment-url#reply',
285 'commit_comment_reply_url': 'http://comment-url#reply',
249 'comment_body': '''
286 'comment_body': '''
250 Hello **world**
287 Hello **world**
251
288
252 This is a multiline comment :)
289 This is a multiline comment :)
253
290
254 - list
291 - list
255 - list2
292 - list2
256 ''',
293 ''',
257 'comment_id': 2048,
294 'comment_id': 2048,
258 'renderer_type': 'markdown',
295 'renderer_type': 'markdown',
259 'mention': True,
296 'mention': True,
260 },
297 },
261 'cs_comment+file': {
298 'cs_comment+file': {
262 'user': user,
299 'user': user,
263 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
300 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
264 'status_change': None,
301 'status_change': None,
265 'status_change_type': None,
302 'status_change_type': None,
266
303
267 'commit_target_repo_url': 'http://foo.example.com/#comment1',
304 'commit_target_repo_url': 'http://foo.example.com/#comment1',
268 'repo_name': 'test-repo',
305 'repo_name': 'test-repo',
269
306
270 'comment_type': 'note',
307 'comment_type': 'note',
271 'comment_file': 'test-file.py',
308 'comment_file': 'test-file.py',
272 'comment_line': 'n100',
309 'comment_line': 'n100',
273
310
274 'commit_comment_url': 'http://comment-url',
311 'commit_comment_url': 'http://comment-url',
275 'commit_comment_reply_url': 'http://comment-url#reply',
312 'commit_comment_reply_url': 'http://comment-url#reply',
276 'comment_body': 'This is my comment body. *I like !*',
313 'comment_body': 'This is my comment body. *I like !*',
277 'comment_id': 2048,
314 'comment_id': 2048,
278 'renderer_type': 'markdown',
315 'renderer_type': 'markdown',
279 'mention': True,
316 'mention': True,
280 },
317 },
281
318
282 'pull_request': {
319 'pull_request': {
283 'user': user,
320 'user': user,
284 'pull_request': pr,
321 'pull_request': pr,
285 'pull_request_commits': [
322 'pull_request_commits': [
286 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
323 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
287 my-account: moved email closer to profile as it's similar data just moved outside.
324 my-account: moved email closer to profile as it's similar data just moved outside.
288 '''),
325 '''),
289 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
326 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
290 users: description edit fixes
327 users: description edit fixes
291
328
292 - tests
329 - tests
293 - added metatags info
330 - added metatags info
294 '''),
331 '''),
295 ],
332 ],
296
333
297 'pull_request_target_repo': target_repo,
334 'pull_request_target_repo': target_repo,
298 'pull_request_target_repo_url': 'http://target-repo/url',
335 'pull_request_target_repo_url': 'http://target-repo/url',
299
336
300 'pull_request_source_repo': source_repo,
337 'pull_request_source_repo': source_repo,
301 'pull_request_source_repo_url': 'http://source-repo/url',
338 'pull_request_source_repo_url': 'http://source-repo/url',
302
339
303 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
340 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
304 }
341 }
305
342
306 }
343 }
307
344
308 template_type = email_id.split('+')[0]
345 template_type = email_id.split('+')[0]
309 (c.subject, c.headers, c.email_body,
346 (c.subject, c.headers, c.email_body,
310 c.email_body_plaintext) = EmailNotificationModel().render_email(
347 c.email_body_plaintext) = EmailNotificationModel().render_email(
311 template_type, **email_kwargs.get(email_id, {}))
348 template_type, **email_kwargs.get(email_id, {}))
312
349
313 test_email = self.request.GET.get('email')
350 test_email = self.request.GET.get('email')
314 if test_email:
351 if test_email:
315 recipients = [test_email]
352 recipients = [test_email]
316 run_task(tasks.send_email, recipients, c.subject,
353 run_task(tasks.send_email, recipients, c.subject,
317 c.email_body_plaintext, c.email_body)
354 c.email_body_plaintext, c.email_body)
318
355
319 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
356 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
320 template = 'debug_style/email_plain_rendered.mako'
357 template = 'debug_style/email_plain_rendered.mako'
321 else:
358 else:
322 template = 'debug_style/email.mako'
359 template = 'debug_style/email.mako'
323 return render_to_response(
360 return render_to_response(
324 template, self._get_template_context(c),
361 template, self._get_template_context(c),
325 request=self.request)
362 request=self.request)
326
363
327 @view_config(
364 @view_config(
328 route_name='debug_style_template', request_method='GET',
365 route_name='debug_style_template', request_method='GET',
329 renderer=None)
366 renderer=None)
330 def template(self):
367 def template(self):
331 t_path = self.request.matchdict['t_path']
368 t_path = self.request.matchdict['t_path']
332 c = self.load_default_context()
369 c = self.load_default_context()
333 c.active = os.path.splitext(t_path)[0]
370 c.active = os.path.splitext(t_path)[0]
334 c.came_from = ''
371 c.came_from = ''
335 c.email_types = {
372 c.email_types = {
336 'cs_comment+file': {},
373 'cs_comment+file': {},
337 'cs_comment+status': {},
374 'cs_comment+status': {},
338
375
339 'pull_request_comment+file': {},
376 'pull_request_comment+file': {},
340 'pull_request_comment+status': {},
377 'pull_request_comment+status': {},
378
379 'pull_request_update': {},
341 }
380 }
342 c.email_types.update(EmailNotificationModel.email_types)
381 c.email_types.update(EmailNotificationModel.email_types)
343
382
344 return render_to_response(
383 return render_to_response(
345 'debug_style/' + t_path, self._get_template_context(c),
384 'debug_style/' + t_path, self._get_template_context(c),
346 request=self.request)
385 request=self.request)
347
386
@@ -1,1476 +1,1477 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 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 logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, EmptyRepositoryError)
44 RepositoryRequirementError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 # backward compat., we use for OLD PRs a plain renderer
63 # backward compat., we use for OLD PRs a plain renderer
64 c.renderer = 'plain'
64 c.renderer = 'plain'
65 return c
65 return c
66
66
67 def _get_pull_requests_list(
67 def _get_pull_requests_list(
68 self, repo_name, source, filter_type, opened_by, statuses):
68 self, repo_name, source, filter_type, opened_by, statuses):
69
69
70 draw, start, limit = self._extract_chunk(self.request)
70 draw, start, limit = self._extract_chunk(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 _render = self.request.get_partial_renderer(
72 _render = self.request.get_partial_renderer(
73 'rhodecode:templates/data_table/_dt_elements.mako')
73 'rhodecode:templates/data_table/_dt_elements.mako')
74
74
75 # pagination
75 # pagination
76
76
77 if filter_type == 'awaiting_review':
77 if filter_type == 'awaiting_review':
78 pull_requests = PullRequestModel().get_awaiting_review(
78 pull_requests = PullRequestModel().get_awaiting_review(
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
79 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 statuses=statuses, offset=start, length=limit,
80 statuses=statuses, offset=start, length=limit,
81 order_by=order_by, order_dir=order_dir)
81 order_by=order_by, order_dir=order_dir)
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 repo_name, search_q=search_q, source=source, statuses=statuses,
83 repo_name, search_q=search_q, source=source, statuses=statuses,
84 opened_by=opened_by)
84 opened_by=opened_by)
85 elif filter_type == 'awaiting_my_review':
85 elif filter_type == 'awaiting_my_review':
86 pull_requests = PullRequestModel().get_awaiting_my_review(
86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
87 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 offset=start, length=limit, order_by=order_by,
89 offset=start, length=limit, order_by=order_by,
90 order_dir=order_dir)
90 order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
92 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 statuses=statuses, opened_by=opened_by)
93 statuses=statuses, opened_by=opened_by)
94 else:
94 else:
95 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, search_q=search_q, source=source, statuses=statuses,
100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 opened_by=opened_by)
101 opened_by=opened_by)
102
102
103 data = []
103 data = []
104 comments_model = CommentsModel()
104 comments_model = CommentsModel()
105 for pr in pull_requests:
105 for pr in pull_requests:
106 comments = comments_model.get_all_comments(
106 comments = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr)
107 self.db_repo.repo_id, pull_request=pr)
108
108
109 data.append({
109 data.append({
110 'name': _render('pullrequest_name',
110 'name': _render('pullrequest_name',
111 pr.pull_request_id, pr.pull_request_state,
111 pr.pull_request_id, pr.pull_request_state,
112 pr.work_in_progress, pr.target_repo.repo_name),
112 pr.work_in_progress, pr.target_repo.repo_name),
113 'name_raw': pr.pull_request_id,
113 'name_raw': pr.pull_request_id,
114 'status': _render('pullrequest_status',
114 'status': _render('pullrequest_status',
115 pr.calculated_review_status()),
115 pr.calculated_review_status()),
116 'title': _render('pullrequest_title', pr.title, pr.description),
116 'title': _render('pullrequest_title', pr.title, pr.description),
117 'description': h.escape(pr.description),
117 'description': h.escape(pr.description),
118 'updated_on': _render('pullrequest_updated_on',
118 'updated_on': _render('pullrequest_updated_on',
119 h.datetime_to_time(pr.updated_on)),
119 h.datetime_to_time(pr.updated_on)),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'created_on': _render('pullrequest_updated_on',
121 'created_on': _render('pullrequest_updated_on',
122 h.datetime_to_time(pr.created_on)),
122 h.datetime_to_time(pr.created_on)),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'state': pr.pull_request_state,
124 'state': pr.pull_request_state,
125 'author': _render('pullrequest_author',
125 'author': _render('pullrequest_author',
126 pr.author.full_contact, ),
126 pr.author.full_contact, ),
127 'author_raw': pr.author.full_name,
127 'author_raw': pr.author.full_name,
128 'comments': _render('pullrequest_comments', len(comments)),
128 'comments': _render('pullrequest_comments', len(comments)),
129 'comments_raw': len(comments),
129 'comments_raw': len(comments),
130 'closed': pr.is_closed(),
130 'closed': pr.is_closed(),
131 })
131 })
132
132
133 data = ({
133 data = ({
134 'draw': draw,
134 'draw': draw,
135 'data': data,
135 'data': data,
136 'recordsTotal': pull_requests_total_count,
136 'recordsTotal': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
137 'recordsFiltered': pull_requests_total_count,
138 })
138 })
139 return data
139 return data
140
140
141 @LoginRequired()
141 @LoginRequired()
142 @HasRepoPermissionAnyDecorator(
142 @HasRepoPermissionAnyDecorator(
143 'repository.read', 'repository.write', 'repository.admin')
143 'repository.read', 'repository.write', 'repository.admin')
144 @view_config(
144 @view_config(
145 route_name='pullrequest_show_all', request_method='GET',
145 route_name='pullrequest_show_all', request_method='GET',
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 def pull_request_list(self):
147 def pull_request_list(self):
148 c = self.load_default_context()
148 c = self.load_default_context()
149
149
150 req_get = self.request.GET
150 req_get = self.request.GET
151 c.source = str2bool(req_get.get('source'))
151 c.source = str2bool(req_get.get('source'))
152 c.closed = str2bool(req_get.get('closed'))
152 c.closed = str2bool(req_get.get('closed'))
153 c.my = str2bool(req_get.get('my'))
153 c.my = str2bool(req_get.get('my'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156
156
157 c.active = 'open'
157 c.active = 'open'
158 if c.my:
158 if c.my:
159 c.active = 'my'
159 c.active = 'my'
160 if c.closed:
160 if c.closed:
161 c.active = 'closed'
161 c.active = 'closed'
162 if c.awaiting_review and not c.source:
162 if c.awaiting_review and not c.source:
163 c.active = 'awaiting'
163 c.active = 'awaiting'
164 if c.source and not c.awaiting_review:
164 if c.source and not c.awaiting_review:
165 c.active = 'source'
165 c.active = 'source'
166 if c.awaiting_my_review:
166 if c.awaiting_my_review:
167 c.active = 'awaiting_my'
167 c.active = 'awaiting_my'
168
168
169 return self._get_template_context(c)
169 return self._get_template_context(c)
170
170
171 @LoginRequired()
171 @LoginRequired()
172 @HasRepoPermissionAnyDecorator(
172 @HasRepoPermissionAnyDecorator(
173 'repository.read', 'repository.write', 'repository.admin')
173 'repository.read', 'repository.write', 'repository.admin')
174 @view_config(
174 @view_config(
175 route_name='pullrequest_show_all_data', request_method='GET',
175 route_name='pullrequest_show_all_data', request_method='GET',
176 renderer='json_ext', xhr=True)
176 renderer='json_ext', xhr=True)
177 def pull_request_list_data(self):
177 def pull_request_list_data(self):
178 self.load_default_context()
178 self.load_default_context()
179
179
180 # additional filters
180 # additional filters
181 req_get = self.request.GET
181 req_get = self.request.GET
182 source = str2bool(req_get.get('source'))
182 source = str2bool(req_get.get('source'))
183 closed = str2bool(req_get.get('closed'))
183 closed = str2bool(req_get.get('closed'))
184 my = str2bool(req_get.get('my'))
184 my = str2bool(req_get.get('my'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187
187
188 filter_type = 'awaiting_review' if awaiting_review \
188 filter_type = 'awaiting_review' if awaiting_review \
189 else 'awaiting_my_review' if awaiting_my_review \
189 else 'awaiting_my_review' if awaiting_my_review \
190 else None
190 else None
191
191
192 opened_by = None
192 opened_by = None
193 if my:
193 if my:
194 opened_by = [self._rhodecode_user.user_id]
194 opened_by = [self._rhodecode_user.user_id]
195
195
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 if closed:
197 if closed:
198 statuses = [PullRequest.STATUS_CLOSED]
198 statuses = [PullRequest.STATUS_CLOSED]
199
199
200 data = self._get_pull_requests_list(
200 data = self._get_pull_requests_list(
201 repo_name=self.db_repo_name, source=source,
201 repo_name=self.db_repo_name, source=source,
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203
203
204 return data
204 return data
205
205
206 def _is_diff_cache_enabled(self, target_repo):
206 def _is_diff_cache_enabled(self, target_repo):
207 caching_enabled = self._get_general_setting(
207 caching_enabled = self._get_general_setting(
208 target_repo, 'rhodecode_diff_cache')
208 target_repo, 'rhodecode_diff_cache')
209 log.debug('Diff caching enabled: %s', caching_enabled)
209 log.debug('Diff caching enabled: %s', caching_enabled)
210 return caching_enabled
210 return caching_enabled
211
211
212 def _get_diffset(self, source_repo_name, source_repo,
212 def _get_diffset(self, source_repo_name, source_repo,
213 source_ref_id, target_ref_id,
213 source_ref_id, target_ref_id,
214 target_commit, source_commit, diff_limit, file_limit,
214 target_commit, source_commit, diff_limit, file_limit,
215 fulldiff, hide_whitespace_changes, diff_context):
215 fulldiff, hide_whitespace_changes, diff_context):
216
216
217 vcs_diff = PullRequestModel().get_diff(
217 vcs_diff = PullRequestModel().get_diff(
218 source_repo, source_ref_id, target_ref_id,
218 source_repo, source_ref_id, target_ref_id,
219 hide_whitespace_changes, diff_context)
219 hide_whitespace_changes, diff_context)
220
220
221 diff_processor = diffs.DiffProcessor(
221 diff_processor = diffs.DiffProcessor(
222 vcs_diff, format='newdiff', diff_limit=diff_limit,
222 vcs_diff, format='newdiff', diff_limit=diff_limit,
223 file_limit=file_limit, show_full_diff=fulldiff)
223 file_limit=file_limit, show_full_diff=fulldiff)
224
224
225 _parsed = diff_processor.prepare()
225 _parsed = diff_processor.prepare()
226
226
227 diffset = codeblocks.DiffSet(
227 diffset = codeblocks.DiffSet(
228 repo_name=self.db_repo_name,
228 repo_name=self.db_repo_name,
229 source_repo_name=source_repo_name,
229 source_repo_name=source_repo_name,
230 source_node_getter=codeblocks.diffset_node_getter(target_commit),
230 source_node_getter=codeblocks.diffset_node_getter(target_commit),
231 target_node_getter=codeblocks.diffset_node_getter(source_commit),
231 target_node_getter=codeblocks.diffset_node_getter(source_commit),
232 )
232 )
233 diffset = self.path_filter.render_patchset_filtered(
233 diffset = self.path_filter.render_patchset_filtered(
234 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
234 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
235
235
236 return diffset
236 return diffset
237
237
238 def _get_range_diffset(self, source_scm, source_repo,
238 def _get_range_diffset(self, source_scm, source_repo,
239 commit1, commit2, diff_limit, file_limit,
239 commit1, commit2, diff_limit, file_limit,
240 fulldiff, hide_whitespace_changes, diff_context):
240 fulldiff, hide_whitespace_changes, diff_context):
241 vcs_diff = source_scm.get_diff(
241 vcs_diff = source_scm.get_diff(
242 commit1, commit2,
242 commit1, commit2,
243 ignore_whitespace=hide_whitespace_changes,
243 ignore_whitespace=hide_whitespace_changes,
244 context=diff_context)
244 context=diff_context)
245
245
246 diff_processor = diffs.DiffProcessor(
246 diff_processor = diffs.DiffProcessor(
247 vcs_diff, format='newdiff', diff_limit=diff_limit,
247 vcs_diff, format='newdiff', diff_limit=diff_limit,
248 file_limit=file_limit, show_full_diff=fulldiff)
248 file_limit=file_limit, show_full_diff=fulldiff)
249
249
250 _parsed = diff_processor.prepare()
250 _parsed = diff_processor.prepare()
251
251
252 diffset = codeblocks.DiffSet(
252 diffset = codeblocks.DiffSet(
253 repo_name=source_repo.repo_name,
253 repo_name=source_repo.repo_name,
254 source_node_getter=codeblocks.diffset_node_getter(commit1),
254 source_node_getter=codeblocks.diffset_node_getter(commit1),
255 target_node_getter=codeblocks.diffset_node_getter(commit2))
255 target_node_getter=codeblocks.diffset_node_getter(commit2))
256
256
257 diffset = self.path_filter.render_patchset_filtered(
257 diffset = self.path_filter.render_patchset_filtered(
258 diffset, _parsed, commit1.raw_id, commit2.raw_id)
258 diffset, _parsed, commit1.raw_id, commit2.raw_id)
259
259
260 return diffset
260 return diffset
261
261
262 @LoginRequired()
262 @LoginRequired()
263 @HasRepoPermissionAnyDecorator(
263 @HasRepoPermissionAnyDecorator(
264 'repository.read', 'repository.write', 'repository.admin')
264 'repository.read', 'repository.write', 'repository.admin')
265 @view_config(
265 @view_config(
266 route_name='pullrequest_show', request_method='GET',
266 route_name='pullrequest_show', request_method='GET',
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
268 def pull_request_show(self):
268 def pull_request_show(self):
269 _ = self.request.translate
269 _ = self.request.translate
270 c = self.load_default_context()
270 c = self.load_default_context()
271
271
272 pull_request = PullRequest.get_or_404(
272 pull_request = PullRequest.get_or_404(
273 self.request.matchdict['pull_request_id'])
273 self.request.matchdict['pull_request_id'])
274 pull_request_id = pull_request.pull_request_id
274 pull_request_id = pull_request.pull_request_id
275
275
276 c.state_progressing = pull_request.is_state_changing()
276 c.state_progressing = pull_request.is_state_changing()
277
277
278 version = self.request.GET.get('version')
278 version = self.request.GET.get('version')
279 from_version = self.request.GET.get('from_version') or version
279 from_version = self.request.GET.get('from_version') or version
280 merge_checks = self.request.GET.get('merge_checks')
280 merge_checks = self.request.GET.get('merge_checks')
281 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
281 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
282
282
283 # fetch global flags of ignore ws or context lines
283 # fetch global flags of ignore ws or context lines
284 diff_context = diffs.get_diff_context(self.request)
284 diff_context = diffs.get_diff_context(self.request)
285 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
285 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
286
286
287 force_refresh = str2bool(self.request.GET.get('force_refresh'))
287 force_refresh = str2bool(self.request.GET.get('force_refresh'))
288
288
289 (pull_request_latest,
289 (pull_request_latest,
290 pull_request_at_ver,
290 pull_request_at_ver,
291 pull_request_display_obj,
291 pull_request_display_obj,
292 at_version) = PullRequestModel().get_pr_version(
292 at_version) = PullRequestModel().get_pr_version(
293 pull_request_id, version=version)
293 pull_request_id, version=version)
294 pr_closed = pull_request_latest.is_closed()
294 pr_closed = pull_request_latest.is_closed()
295
295
296 if pr_closed and (version or from_version):
296 if pr_closed and (version or from_version):
297 # not allow to browse versions
297 # not allow to browse versions
298 raise HTTPFound(h.route_path(
298 raise HTTPFound(h.route_path(
299 'pullrequest_show', repo_name=self.db_repo_name,
299 'pullrequest_show', repo_name=self.db_repo_name,
300 pull_request_id=pull_request_id))
300 pull_request_id=pull_request_id))
301
301
302 versions = pull_request_display_obj.versions()
302 versions = pull_request_display_obj.versions()
303 # used to store per-commit range diffs
303 # used to store per-commit range diffs
304 c.changes = collections.OrderedDict()
304 c.changes = collections.OrderedDict()
305 c.range_diff_on = self.request.GET.get('range-diff') == "1"
305 c.range_diff_on = self.request.GET.get('range-diff') == "1"
306
306
307 c.at_version = at_version
307 c.at_version = at_version
308 c.at_version_num = (at_version
308 c.at_version_num = (at_version
309 if at_version and at_version != 'latest'
309 if at_version and at_version != 'latest'
310 else None)
310 else None)
311 c.at_version_pos = ChangesetComment.get_index_from_version(
311 c.at_version_pos = ChangesetComment.get_index_from_version(
312 c.at_version_num, versions)
312 c.at_version_num, versions)
313
313
314 (prev_pull_request_latest,
314 (prev_pull_request_latest,
315 prev_pull_request_at_ver,
315 prev_pull_request_at_ver,
316 prev_pull_request_display_obj,
316 prev_pull_request_display_obj,
317 prev_at_version) = PullRequestModel().get_pr_version(
317 prev_at_version) = PullRequestModel().get_pr_version(
318 pull_request_id, version=from_version)
318 pull_request_id, version=from_version)
319
319
320 c.from_version = prev_at_version
320 c.from_version = prev_at_version
321 c.from_version_num = (prev_at_version
321 c.from_version_num = (prev_at_version
322 if prev_at_version and prev_at_version != 'latest'
322 if prev_at_version and prev_at_version != 'latest'
323 else None)
323 else None)
324 c.from_version_pos = ChangesetComment.get_index_from_version(
324 c.from_version_pos = ChangesetComment.get_index_from_version(
325 c.from_version_num, versions)
325 c.from_version_num, versions)
326
326
327 # define if we're in COMPARE mode or VIEW at version mode
327 # define if we're in COMPARE mode or VIEW at version mode
328 compare = at_version != prev_at_version
328 compare = at_version != prev_at_version
329
329
330 # pull_requests repo_name we opened it against
330 # pull_requests repo_name we opened it against
331 # ie. target_repo must match
331 # ie. target_repo must match
332 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
332 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
333 raise HTTPNotFound()
333 raise HTTPNotFound()
334
334
335 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
335 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
336 pull_request_at_ver)
336 pull_request_at_ver)
337
337
338 c.pull_request = pull_request_display_obj
338 c.pull_request = pull_request_display_obj
339 c.renderer = pull_request_at_ver.description_renderer or c.renderer
339 c.renderer = pull_request_at_ver.description_renderer or c.renderer
340 c.pull_request_latest = pull_request_latest
340 c.pull_request_latest = pull_request_latest
341
341
342 if compare or (at_version and not at_version == 'latest'):
342 if compare or (at_version and not at_version == 'latest'):
343 c.allowed_to_change_status = False
343 c.allowed_to_change_status = False
344 c.allowed_to_update = False
344 c.allowed_to_update = False
345 c.allowed_to_merge = False
345 c.allowed_to_merge = False
346 c.allowed_to_delete = False
346 c.allowed_to_delete = False
347 c.allowed_to_comment = False
347 c.allowed_to_comment = False
348 c.allowed_to_close = False
348 c.allowed_to_close = False
349 else:
349 else:
350 can_change_status = PullRequestModel().check_user_change_status(
350 can_change_status = PullRequestModel().check_user_change_status(
351 pull_request_at_ver, self._rhodecode_user)
351 pull_request_at_ver, self._rhodecode_user)
352 c.allowed_to_change_status = can_change_status and not pr_closed
352 c.allowed_to_change_status = can_change_status and not pr_closed
353
353
354 c.allowed_to_update = PullRequestModel().check_user_update(
354 c.allowed_to_update = PullRequestModel().check_user_update(
355 pull_request_latest, self._rhodecode_user) and not pr_closed
355 pull_request_latest, self._rhodecode_user) and not pr_closed
356 c.allowed_to_merge = PullRequestModel().check_user_merge(
356 c.allowed_to_merge = PullRequestModel().check_user_merge(
357 pull_request_latest, self._rhodecode_user) and not pr_closed
357 pull_request_latest, self._rhodecode_user) and not pr_closed
358 c.allowed_to_delete = PullRequestModel().check_user_delete(
358 c.allowed_to_delete = PullRequestModel().check_user_delete(
359 pull_request_latest, self._rhodecode_user) and not pr_closed
359 pull_request_latest, self._rhodecode_user) and not pr_closed
360 c.allowed_to_comment = not pr_closed
360 c.allowed_to_comment = not pr_closed
361 c.allowed_to_close = c.allowed_to_merge and not pr_closed
361 c.allowed_to_close = c.allowed_to_merge and not pr_closed
362
362
363 c.forbid_adding_reviewers = False
363 c.forbid_adding_reviewers = False
364 c.forbid_author_to_review = False
364 c.forbid_author_to_review = False
365 c.forbid_commit_author_to_review = False
365 c.forbid_commit_author_to_review = False
366
366
367 if pull_request_latest.reviewer_data and \
367 if pull_request_latest.reviewer_data and \
368 'rules' in pull_request_latest.reviewer_data:
368 'rules' in pull_request_latest.reviewer_data:
369 rules = pull_request_latest.reviewer_data['rules'] or {}
369 rules = pull_request_latest.reviewer_data['rules'] or {}
370 try:
370 try:
371 c.forbid_adding_reviewers = rules.get(
371 c.forbid_adding_reviewers = rules.get(
372 'forbid_adding_reviewers')
372 'forbid_adding_reviewers')
373 c.forbid_author_to_review = rules.get(
373 c.forbid_author_to_review = rules.get(
374 'forbid_author_to_review')
374 'forbid_author_to_review')
375 c.forbid_commit_author_to_review = rules.get(
375 c.forbid_commit_author_to_review = rules.get(
376 'forbid_commit_author_to_review')
376 'forbid_commit_author_to_review')
377 except Exception:
377 except Exception:
378 pass
378 pass
379
379
380 # check merge capabilities
380 # check merge capabilities
381 _merge_check = MergeCheck.validate(
381 _merge_check = MergeCheck.validate(
382 pull_request_latest, auth_user=self._rhodecode_user,
382 pull_request_latest, auth_user=self._rhodecode_user,
383 translator=self.request.translate,
383 translator=self.request.translate,
384 force_shadow_repo_refresh=force_refresh)
384 force_shadow_repo_refresh=force_refresh)
385 c.pr_merge_errors = _merge_check.error_details
385 c.pr_merge_errors = _merge_check.error_details
386 c.pr_merge_possible = not _merge_check.failed
386 c.pr_merge_possible = not _merge_check.failed
387 c.pr_merge_message = _merge_check.merge_msg
387 c.pr_merge_message = _merge_check.merge_msg
388
388
389 c.pr_merge_info = MergeCheck.get_merge_conditions(
389 c.pr_merge_info = MergeCheck.get_merge_conditions(
390 pull_request_latest, translator=self.request.translate)
390 pull_request_latest, translator=self.request.translate)
391
391
392 c.pull_request_review_status = _merge_check.review_status
392 c.pull_request_review_status = _merge_check.review_status
393 if merge_checks:
393 if merge_checks:
394 self.request.override_renderer = \
394 self.request.override_renderer = \
395 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
395 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
396 return self._get_template_context(c)
396 return self._get_template_context(c)
397
397
398 comments_model = CommentsModel()
398 comments_model = CommentsModel()
399
399
400 # reviewers and statuses
400 # reviewers and statuses
401 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
401 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
402 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
402 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
403
403
404 # GENERAL COMMENTS with versions #
404 # GENERAL COMMENTS with versions #
405 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
405 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
406 q = q.order_by(ChangesetComment.comment_id.asc())
406 q = q.order_by(ChangesetComment.comment_id.asc())
407 general_comments = q
407 general_comments = q
408
408
409 # pick comments we want to render at current version
409 # pick comments we want to render at current version
410 c.comment_versions = comments_model.aggregate_comments(
410 c.comment_versions = comments_model.aggregate_comments(
411 general_comments, versions, c.at_version_num)
411 general_comments, versions, c.at_version_num)
412 c.comments = c.comment_versions[c.at_version_num]['until']
412 c.comments = c.comment_versions[c.at_version_num]['until']
413
413
414 # INLINE COMMENTS with versions #
414 # INLINE COMMENTS with versions #
415 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
415 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
416 q = q.order_by(ChangesetComment.comment_id.asc())
416 q = q.order_by(ChangesetComment.comment_id.asc())
417 inline_comments = q
417 inline_comments = q
418
418
419 c.inline_versions = comments_model.aggregate_comments(
419 c.inline_versions = comments_model.aggregate_comments(
420 inline_comments, versions, c.at_version_num, inline=True)
420 inline_comments, versions, c.at_version_num, inline=True)
421
421
422 # TODOs
422 # TODOs
423 c.unresolved_comments = CommentsModel() \
423 c.unresolved_comments = CommentsModel() \
424 .get_pull_request_unresolved_todos(pull_request)
424 .get_pull_request_unresolved_todos(pull_request)
425 c.resolved_comments = CommentsModel() \
425 c.resolved_comments = CommentsModel() \
426 .get_pull_request_resolved_todos(pull_request)
426 .get_pull_request_resolved_todos(pull_request)
427
427
428 # inject latest version
428 # inject latest version
429 latest_ver = PullRequest.get_pr_display_object(
429 latest_ver = PullRequest.get_pr_display_object(
430 pull_request_latest, pull_request_latest)
430 pull_request_latest, pull_request_latest)
431
431
432 c.versions = versions + [latest_ver]
432 c.versions = versions + [latest_ver]
433
433
434 # if we use version, then do not show later comments
434 # if we use version, then do not show later comments
435 # than current version
435 # than current version
436 display_inline_comments = collections.defaultdict(
436 display_inline_comments = collections.defaultdict(
437 lambda: collections.defaultdict(list))
437 lambda: collections.defaultdict(list))
438 for co in inline_comments:
438 for co in inline_comments:
439 if c.at_version_num:
439 if c.at_version_num:
440 # pick comments that are at least UPTO given version, so we
440 # pick comments that are at least UPTO given version, so we
441 # don't render comments for higher version
441 # don't render comments for higher version
442 should_render = co.pull_request_version_id and \
442 should_render = co.pull_request_version_id and \
443 co.pull_request_version_id <= c.at_version_num
443 co.pull_request_version_id <= c.at_version_num
444 else:
444 else:
445 # showing all, for 'latest'
445 # showing all, for 'latest'
446 should_render = True
446 should_render = True
447
447
448 if should_render:
448 if should_render:
449 display_inline_comments[co.f_path][co.line_no].append(co)
449 display_inline_comments[co.f_path][co.line_no].append(co)
450
450
451 # load diff data into template context, if we use compare mode then
451 # load diff data into template context, if we use compare mode then
452 # diff is calculated based on changes between versions of PR
452 # diff is calculated based on changes between versions of PR
453
453
454 source_repo = pull_request_at_ver.source_repo
454 source_repo = pull_request_at_ver.source_repo
455 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
455 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
456
456
457 target_repo = pull_request_at_ver.target_repo
457 target_repo = pull_request_at_ver.target_repo
458 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
458 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
459
459
460 if compare:
460 if compare:
461 # in compare switch the diff base to latest commit from prev version
461 # in compare switch the diff base to latest commit from prev version
462 target_ref_id = prev_pull_request_display_obj.revisions[0]
462 target_ref_id = prev_pull_request_display_obj.revisions[0]
463
463
464 # despite opening commits for bookmarks/branches/tags, we always
464 # despite opening commits for bookmarks/branches/tags, we always
465 # convert this to rev to prevent changes after bookmark or branch change
465 # convert this to rev to prevent changes after bookmark or branch change
466 c.source_ref_type = 'rev'
466 c.source_ref_type = 'rev'
467 c.source_ref = source_ref_id
467 c.source_ref = source_ref_id
468
468
469 c.target_ref_type = 'rev'
469 c.target_ref_type = 'rev'
470 c.target_ref = target_ref_id
470 c.target_ref = target_ref_id
471
471
472 c.source_repo = source_repo
472 c.source_repo = source_repo
473 c.target_repo = target_repo
473 c.target_repo = target_repo
474
474
475 c.commit_ranges = []
475 c.commit_ranges = []
476 source_commit = EmptyCommit()
476 source_commit = EmptyCommit()
477 target_commit = EmptyCommit()
477 target_commit = EmptyCommit()
478 c.missing_requirements = False
478 c.missing_requirements = False
479
479
480 source_scm = source_repo.scm_instance()
480 source_scm = source_repo.scm_instance()
481 target_scm = target_repo.scm_instance()
481 target_scm = target_repo.scm_instance()
482
482
483 shadow_scm = None
483 shadow_scm = None
484 try:
484 try:
485 shadow_scm = pull_request_latest.get_shadow_repo()
485 shadow_scm = pull_request_latest.get_shadow_repo()
486 except Exception:
486 except Exception:
487 log.debug('Failed to get shadow repo', exc_info=True)
487 log.debug('Failed to get shadow repo', exc_info=True)
488 # try first the existing source_repo, and then shadow
488 # try first the existing source_repo, and then shadow
489 # repo if we can obtain one
489 # repo if we can obtain one
490 commits_source_repo = source_scm or shadow_scm
490 commits_source_repo = source_scm or shadow_scm
491
491
492 c.commits_source_repo = commits_source_repo
492 c.commits_source_repo = commits_source_repo
493 c.ancestor = None # set it to None, to hide it from PR view
493 c.ancestor = None # set it to None, to hide it from PR view
494
494
495 # empty version means latest, so we keep this to prevent
495 # empty version means latest, so we keep this to prevent
496 # double caching
496 # double caching
497 version_normalized = version or 'latest'
497 version_normalized = version or 'latest'
498 from_version_normalized = from_version or 'latest'
498 from_version_normalized = from_version or 'latest'
499
499
500 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
500 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
501 cache_file_path = diff_cache_exist(
501 cache_file_path = diff_cache_exist(
502 cache_path, 'pull_request', pull_request_id, version_normalized,
502 cache_path, 'pull_request', pull_request_id, version_normalized,
503 from_version_normalized, source_ref_id, target_ref_id,
503 from_version_normalized, source_ref_id, target_ref_id,
504 hide_whitespace_changes, diff_context, c.fulldiff)
504 hide_whitespace_changes, diff_context, c.fulldiff)
505
505
506 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
506 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
507 force_recache = self.get_recache_flag()
507 force_recache = self.get_recache_flag()
508
508
509 cached_diff = None
509 cached_diff = None
510 if caching_enabled:
510 if caching_enabled:
511 cached_diff = load_cached_diff(cache_file_path)
511 cached_diff = load_cached_diff(cache_file_path)
512
512
513 has_proper_commit_cache = (
513 has_proper_commit_cache = (
514 cached_diff and cached_diff.get('commits')
514 cached_diff and cached_diff.get('commits')
515 and len(cached_diff.get('commits', [])) == 5
515 and len(cached_diff.get('commits', [])) == 5
516 and cached_diff.get('commits')[0]
516 and cached_diff.get('commits')[0]
517 and cached_diff.get('commits')[3])
517 and cached_diff.get('commits')[3])
518
518
519 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
519 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
520 diff_commit_cache = \
520 diff_commit_cache = \
521 (ancestor_commit, commit_cache, missing_requirements,
521 (ancestor_commit, commit_cache, missing_requirements,
522 source_commit, target_commit) = cached_diff['commits']
522 source_commit, target_commit) = cached_diff['commits']
523 else:
523 else:
524 diff_commit_cache = \
524 diff_commit_cache = \
525 (ancestor_commit, commit_cache, missing_requirements,
525 (ancestor_commit, commit_cache, missing_requirements,
526 source_commit, target_commit) = self.get_commits(
526 source_commit, target_commit) = self.get_commits(
527 commits_source_repo,
527 commits_source_repo,
528 pull_request_at_ver,
528 pull_request_at_ver,
529 source_commit,
529 source_commit,
530 source_ref_id,
530 source_ref_id,
531 source_scm,
531 source_scm,
532 target_commit,
532 target_commit,
533 target_ref_id,
533 target_ref_id,
534 target_scm)
534 target_scm)
535
535
536 # register our commit range
536 # register our commit range
537 for comm in commit_cache.values():
537 for comm in commit_cache.values():
538 c.commit_ranges.append(comm)
538 c.commit_ranges.append(comm)
539
539
540 c.missing_requirements = missing_requirements
540 c.missing_requirements = missing_requirements
541 c.ancestor_commit = ancestor_commit
541 c.ancestor_commit = ancestor_commit
542 c.statuses = source_repo.statuses(
542 c.statuses = source_repo.statuses(
543 [x.raw_id for x in c.commit_ranges])
543 [x.raw_id for x in c.commit_ranges])
544
544
545 # auto collapse if we have more than limit
545 # auto collapse if we have more than limit
546 collapse_limit = diffs.DiffProcessor._collapse_commits_over
546 collapse_limit = diffs.DiffProcessor._collapse_commits_over
547 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
547 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
548 c.compare_mode = compare
548 c.compare_mode = compare
549
549
550 # diff_limit is the old behavior, will cut off the whole diff
550 # diff_limit is the old behavior, will cut off the whole diff
551 # if the limit is applied otherwise will just hide the
551 # if the limit is applied otherwise will just hide the
552 # big files from the front-end
552 # big files from the front-end
553 diff_limit = c.visual.cut_off_limit_diff
553 diff_limit = c.visual.cut_off_limit_diff
554 file_limit = c.visual.cut_off_limit_file
554 file_limit = c.visual.cut_off_limit_file
555
555
556 c.missing_commits = False
556 c.missing_commits = False
557 if (c.missing_requirements
557 if (c.missing_requirements
558 or isinstance(source_commit, EmptyCommit)
558 or isinstance(source_commit, EmptyCommit)
559 or source_commit == target_commit):
559 or source_commit == target_commit):
560
560
561 c.missing_commits = True
561 c.missing_commits = True
562 else:
562 else:
563 c.inline_comments = display_inline_comments
563 c.inline_comments = display_inline_comments
564
564
565 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
565 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
566 if not force_recache and has_proper_diff_cache:
566 if not force_recache and has_proper_diff_cache:
567 c.diffset = cached_diff['diff']
567 c.diffset = cached_diff['diff']
568 (ancestor_commit, commit_cache, missing_requirements,
568 (ancestor_commit, commit_cache, missing_requirements,
569 source_commit, target_commit) = cached_diff['commits']
569 source_commit, target_commit) = cached_diff['commits']
570 else:
570 else:
571 c.diffset = self._get_diffset(
571 c.diffset = self._get_diffset(
572 c.source_repo.repo_name, commits_source_repo,
572 c.source_repo.repo_name, commits_source_repo,
573 source_ref_id, target_ref_id,
573 source_ref_id, target_ref_id,
574 target_commit, source_commit,
574 target_commit, source_commit,
575 diff_limit, file_limit, c.fulldiff,
575 diff_limit, file_limit, c.fulldiff,
576 hide_whitespace_changes, diff_context)
576 hide_whitespace_changes, diff_context)
577
577
578 # save cached diff
578 # save cached diff
579 if caching_enabled:
579 if caching_enabled:
580 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
580 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
581
581
582 c.limited_diff = c.diffset.limited_diff
582 c.limited_diff = c.diffset.limited_diff
583
583
584 # calculate removed files that are bound to comments
584 # calculate removed files that are bound to comments
585 comment_deleted_files = [
585 comment_deleted_files = [
586 fname for fname in display_inline_comments
586 fname for fname in display_inline_comments
587 if fname not in c.diffset.file_stats]
587 if fname not in c.diffset.file_stats]
588
588
589 c.deleted_files_comments = collections.defaultdict(dict)
589 c.deleted_files_comments = collections.defaultdict(dict)
590 for fname, per_line_comments in display_inline_comments.items():
590 for fname, per_line_comments in display_inline_comments.items():
591 if fname in comment_deleted_files:
591 if fname in comment_deleted_files:
592 c.deleted_files_comments[fname]['stats'] = 0
592 c.deleted_files_comments[fname]['stats'] = 0
593 c.deleted_files_comments[fname]['comments'] = list()
593 c.deleted_files_comments[fname]['comments'] = list()
594 for lno, comments in per_line_comments.items():
594 for lno, comments in per_line_comments.items():
595 c.deleted_files_comments[fname]['comments'].extend(comments)
595 c.deleted_files_comments[fname]['comments'].extend(comments)
596
596
597 # maybe calculate the range diff
597 # maybe calculate the range diff
598 if c.range_diff_on:
598 if c.range_diff_on:
599 # TODO(marcink): set whitespace/context
599 # TODO(marcink): set whitespace/context
600 context_lcl = 3
600 context_lcl = 3
601 ign_whitespace_lcl = False
601 ign_whitespace_lcl = False
602
602
603 for commit in c.commit_ranges:
603 for commit in c.commit_ranges:
604 commit2 = commit
604 commit2 = commit
605 commit1 = commit.first_parent
605 commit1 = commit.first_parent
606
606
607 range_diff_cache_file_path = diff_cache_exist(
607 range_diff_cache_file_path = diff_cache_exist(
608 cache_path, 'diff', commit.raw_id,
608 cache_path, 'diff', commit.raw_id,
609 ign_whitespace_lcl, context_lcl, c.fulldiff)
609 ign_whitespace_lcl, context_lcl, c.fulldiff)
610
610
611 cached_diff = None
611 cached_diff = None
612 if caching_enabled:
612 if caching_enabled:
613 cached_diff = load_cached_diff(range_diff_cache_file_path)
613 cached_diff = load_cached_diff(range_diff_cache_file_path)
614
614
615 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
615 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
616 if not force_recache and has_proper_diff_cache:
616 if not force_recache and has_proper_diff_cache:
617 diffset = cached_diff['diff']
617 diffset = cached_diff['diff']
618 else:
618 else:
619 diffset = self._get_range_diffset(
619 diffset = self._get_range_diffset(
620 source_scm, source_repo,
620 source_scm, source_repo,
621 commit1, commit2, diff_limit, file_limit,
621 commit1, commit2, diff_limit, file_limit,
622 c.fulldiff, ign_whitespace_lcl, context_lcl
622 c.fulldiff, ign_whitespace_lcl, context_lcl
623 )
623 )
624
624
625 # save cached diff
625 # save cached diff
626 if caching_enabled:
626 if caching_enabled:
627 cache_diff(range_diff_cache_file_path, diffset, None)
627 cache_diff(range_diff_cache_file_path, diffset, None)
628
628
629 c.changes[commit.raw_id] = diffset
629 c.changes[commit.raw_id] = diffset
630
630
631 # this is a hack to properly display links, when creating PR, the
631 # this is a hack to properly display links, when creating PR, the
632 # compare view and others uses different notation, and
632 # compare view and others uses different notation, and
633 # compare_commits.mako renders links based on the target_repo.
633 # compare_commits.mako renders links based on the target_repo.
634 # We need to swap that here to generate it properly on the html side
634 # We need to swap that here to generate it properly on the html side
635 c.target_repo = c.source_repo
635 c.target_repo = c.source_repo
636
636
637 c.commit_statuses = ChangesetStatus.STATUSES
637 c.commit_statuses = ChangesetStatus.STATUSES
638
638
639 c.show_version_changes = not pr_closed
639 c.show_version_changes = not pr_closed
640 if c.show_version_changes:
640 if c.show_version_changes:
641 cur_obj = pull_request_at_ver
641 cur_obj = pull_request_at_ver
642 prev_obj = prev_pull_request_at_ver
642 prev_obj = prev_pull_request_at_ver
643
643
644 old_commit_ids = prev_obj.revisions
644 old_commit_ids = prev_obj.revisions
645 new_commit_ids = cur_obj.revisions
645 new_commit_ids = cur_obj.revisions
646 commit_changes = PullRequestModel()._calculate_commit_id_changes(
646 commit_changes = PullRequestModel()._calculate_commit_id_changes(
647 old_commit_ids, new_commit_ids)
647 old_commit_ids, new_commit_ids)
648 c.commit_changes_summary = commit_changes
648 c.commit_changes_summary = commit_changes
649
649
650 # calculate the diff for commits between versions
650 # calculate the diff for commits between versions
651 c.commit_changes = []
651 c.commit_changes = []
652 mark = lambda cs, fw: list(
652 mark = lambda cs, fw: list(
653 h.itertools.izip_longest([], cs, fillvalue=fw))
653 h.itertools.izip_longest([], cs, fillvalue=fw))
654 for c_type, raw_id in mark(commit_changes.added, 'a') \
654 for c_type, raw_id in mark(commit_changes.added, 'a') \
655 + mark(commit_changes.removed, 'r') \
655 + mark(commit_changes.removed, 'r') \
656 + mark(commit_changes.common, 'c'):
656 + mark(commit_changes.common, 'c'):
657
657
658 if raw_id in commit_cache:
658 if raw_id in commit_cache:
659 commit = commit_cache[raw_id]
659 commit = commit_cache[raw_id]
660 else:
660 else:
661 try:
661 try:
662 commit = commits_source_repo.get_commit(raw_id)
662 commit = commits_source_repo.get_commit(raw_id)
663 except CommitDoesNotExistError:
663 except CommitDoesNotExistError:
664 # in case we fail extracting still use "dummy" commit
664 # in case we fail extracting still use "dummy" commit
665 # for display in commit diff
665 # for display in commit diff
666 commit = h.AttributeDict(
666 commit = h.AttributeDict(
667 {'raw_id': raw_id,
667 {'raw_id': raw_id,
668 'message': 'EMPTY or MISSING COMMIT'})
668 'message': 'EMPTY or MISSING COMMIT'})
669 c.commit_changes.append([c_type, commit])
669 c.commit_changes.append([c_type, commit])
670
670
671 # current user review statuses for each version
671 # current user review statuses for each version
672 c.review_versions = {}
672 c.review_versions = {}
673 if self._rhodecode_user.user_id in allowed_reviewers:
673 if self._rhodecode_user.user_id in allowed_reviewers:
674 for co in general_comments:
674 for co in general_comments:
675 if co.author.user_id == self._rhodecode_user.user_id:
675 if co.author.user_id == self._rhodecode_user.user_id:
676 status = co.status_change
676 status = co.status_change
677 if status:
677 if status:
678 _ver_pr = status[0].comment.pull_request_version_id
678 _ver_pr = status[0].comment.pull_request_version_id
679 c.review_versions[_ver_pr] = status[0]
679 c.review_versions[_ver_pr] = status[0]
680
680
681 return self._get_template_context(c)
681 return self._get_template_context(c)
682
682
683 def get_commits(
683 def get_commits(
684 self, commits_source_repo, pull_request_at_ver, source_commit,
684 self, commits_source_repo, pull_request_at_ver, source_commit,
685 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
685 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
686 commit_cache = collections.OrderedDict()
686 commit_cache = collections.OrderedDict()
687 missing_requirements = False
687 missing_requirements = False
688 try:
688 try:
689 pre_load = ["author", "date", "message", "branch", "parents"]
689 pre_load = ["author", "date", "message", "branch", "parents"]
690 show_revs = pull_request_at_ver.revisions
690 show_revs = pull_request_at_ver.revisions
691 for rev in show_revs:
691 for rev in show_revs:
692 comm = commits_source_repo.get_commit(
692 comm = commits_source_repo.get_commit(
693 commit_id=rev, pre_load=pre_load)
693 commit_id=rev, pre_load=pre_load)
694 commit_cache[comm.raw_id] = comm
694 commit_cache[comm.raw_id] = comm
695
695
696 # Order here matters, we first need to get target, and then
696 # Order here matters, we first need to get target, and then
697 # the source
697 # the source
698 target_commit = commits_source_repo.get_commit(
698 target_commit = commits_source_repo.get_commit(
699 commit_id=safe_str(target_ref_id))
699 commit_id=safe_str(target_ref_id))
700
700
701 source_commit = commits_source_repo.get_commit(
701 source_commit = commits_source_repo.get_commit(
702 commit_id=safe_str(source_ref_id))
702 commit_id=safe_str(source_ref_id))
703 except CommitDoesNotExistError:
703 except CommitDoesNotExistError:
704 log.warning(
704 log.warning(
705 'Failed to get commit from `{}` repo'.format(
705 'Failed to get commit from `{}` repo'.format(
706 commits_source_repo), exc_info=True)
706 commits_source_repo), exc_info=True)
707 except RepositoryRequirementError:
707 except RepositoryRequirementError:
708 log.warning(
708 log.warning(
709 'Failed to get all required data from repo', exc_info=True)
709 'Failed to get all required data from repo', exc_info=True)
710 missing_requirements = True
710 missing_requirements = True
711 ancestor_commit = None
711 ancestor_commit = None
712 try:
712 try:
713 ancestor_id = source_scm.get_common_ancestor(
713 ancestor_id = source_scm.get_common_ancestor(
714 source_commit.raw_id, target_commit.raw_id, target_scm)
714 source_commit.raw_id, target_commit.raw_id, target_scm)
715 ancestor_commit = source_scm.get_commit(ancestor_id)
715 ancestor_commit = source_scm.get_commit(ancestor_id)
716 except Exception:
716 except Exception:
717 ancestor_commit = None
717 ancestor_commit = None
718 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
718 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
719
719
720 def assure_not_empty_repo(self):
720 def assure_not_empty_repo(self):
721 _ = self.request.translate
721 _ = self.request.translate
722
722
723 try:
723 try:
724 self.db_repo.scm_instance().get_commit()
724 self.db_repo.scm_instance().get_commit()
725 except EmptyRepositoryError:
725 except EmptyRepositoryError:
726 h.flash(h.literal(_('There are no commits yet')),
726 h.flash(h.literal(_('There are no commits yet')),
727 category='warning')
727 category='warning')
728 raise HTTPFound(
728 raise HTTPFound(
729 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
729 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
730
730
731 @LoginRequired()
731 @LoginRequired()
732 @NotAnonymous()
732 @NotAnonymous()
733 @HasRepoPermissionAnyDecorator(
733 @HasRepoPermissionAnyDecorator(
734 'repository.read', 'repository.write', 'repository.admin')
734 'repository.read', 'repository.write', 'repository.admin')
735 @view_config(
735 @view_config(
736 route_name='pullrequest_new', request_method='GET',
736 route_name='pullrequest_new', request_method='GET',
737 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
737 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
738 def pull_request_new(self):
738 def pull_request_new(self):
739 _ = self.request.translate
739 _ = self.request.translate
740 c = self.load_default_context()
740 c = self.load_default_context()
741
741
742 self.assure_not_empty_repo()
742 self.assure_not_empty_repo()
743 source_repo = self.db_repo
743 source_repo = self.db_repo
744
744
745 commit_id = self.request.GET.get('commit')
745 commit_id = self.request.GET.get('commit')
746 branch_ref = self.request.GET.get('branch')
746 branch_ref = self.request.GET.get('branch')
747 bookmark_ref = self.request.GET.get('bookmark')
747 bookmark_ref = self.request.GET.get('bookmark')
748
748
749 try:
749 try:
750 source_repo_data = PullRequestModel().generate_repo_data(
750 source_repo_data = PullRequestModel().generate_repo_data(
751 source_repo, commit_id=commit_id,
751 source_repo, commit_id=commit_id,
752 branch=branch_ref, bookmark=bookmark_ref,
752 branch=branch_ref, bookmark=bookmark_ref,
753 translator=self.request.translate)
753 translator=self.request.translate)
754 except CommitDoesNotExistError as e:
754 except CommitDoesNotExistError as e:
755 log.exception(e)
755 log.exception(e)
756 h.flash(_('Commit does not exist'), 'error')
756 h.flash(_('Commit does not exist'), 'error')
757 raise HTTPFound(
757 raise HTTPFound(
758 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
758 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
759
759
760 default_target_repo = source_repo
760 default_target_repo = source_repo
761
761
762 if source_repo.parent and c.has_origin_repo_read_perm:
762 if source_repo.parent and c.has_origin_repo_read_perm:
763 parent_vcs_obj = source_repo.parent.scm_instance()
763 parent_vcs_obj = source_repo.parent.scm_instance()
764 if parent_vcs_obj and not parent_vcs_obj.is_empty():
764 if parent_vcs_obj and not parent_vcs_obj.is_empty():
765 # change default if we have a parent repo
765 # change default if we have a parent repo
766 default_target_repo = source_repo.parent
766 default_target_repo = source_repo.parent
767
767
768 target_repo_data = PullRequestModel().generate_repo_data(
768 target_repo_data = PullRequestModel().generate_repo_data(
769 default_target_repo, translator=self.request.translate)
769 default_target_repo, translator=self.request.translate)
770
770
771 selected_source_ref = source_repo_data['refs']['selected_ref']
771 selected_source_ref = source_repo_data['refs']['selected_ref']
772 title_source_ref = ''
772 title_source_ref = ''
773 if selected_source_ref:
773 if selected_source_ref:
774 title_source_ref = selected_source_ref.split(':', 2)[1]
774 title_source_ref = selected_source_ref.split(':', 2)[1]
775 c.default_title = PullRequestModel().generate_pullrequest_title(
775 c.default_title = PullRequestModel().generate_pullrequest_title(
776 source=source_repo.repo_name,
776 source=source_repo.repo_name,
777 source_ref=title_source_ref,
777 source_ref=title_source_ref,
778 target=default_target_repo.repo_name
778 target=default_target_repo.repo_name
779 )
779 )
780
780
781 c.default_repo_data = {
781 c.default_repo_data = {
782 'source_repo_name': source_repo.repo_name,
782 'source_repo_name': source_repo.repo_name,
783 'source_refs_json': json.dumps(source_repo_data),
783 'source_refs_json': json.dumps(source_repo_data),
784 'target_repo_name': default_target_repo.repo_name,
784 'target_repo_name': default_target_repo.repo_name,
785 'target_refs_json': json.dumps(target_repo_data),
785 'target_refs_json': json.dumps(target_repo_data),
786 }
786 }
787 c.default_source_ref = selected_source_ref
787 c.default_source_ref = selected_source_ref
788
788
789 return self._get_template_context(c)
789 return self._get_template_context(c)
790
790
791 @LoginRequired()
791 @LoginRequired()
792 @NotAnonymous()
792 @NotAnonymous()
793 @HasRepoPermissionAnyDecorator(
793 @HasRepoPermissionAnyDecorator(
794 'repository.read', 'repository.write', 'repository.admin')
794 'repository.read', 'repository.write', 'repository.admin')
795 @view_config(
795 @view_config(
796 route_name='pullrequest_repo_refs', request_method='GET',
796 route_name='pullrequest_repo_refs', request_method='GET',
797 renderer='json_ext', xhr=True)
797 renderer='json_ext', xhr=True)
798 def pull_request_repo_refs(self):
798 def pull_request_repo_refs(self):
799 self.load_default_context()
799 self.load_default_context()
800 target_repo_name = self.request.matchdict['target_repo_name']
800 target_repo_name = self.request.matchdict['target_repo_name']
801 repo = Repository.get_by_repo_name(target_repo_name)
801 repo = Repository.get_by_repo_name(target_repo_name)
802 if not repo:
802 if not repo:
803 raise HTTPNotFound()
803 raise HTTPNotFound()
804
804
805 target_perm = HasRepoPermissionAny(
805 target_perm = HasRepoPermissionAny(
806 'repository.read', 'repository.write', 'repository.admin')(
806 'repository.read', 'repository.write', 'repository.admin')(
807 target_repo_name)
807 target_repo_name)
808 if not target_perm:
808 if not target_perm:
809 raise HTTPNotFound()
809 raise HTTPNotFound()
810
810
811 return PullRequestModel().generate_repo_data(
811 return PullRequestModel().generate_repo_data(
812 repo, translator=self.request.translate)
812 repo, translator=self.request.translate)
813
813
814 @LoginRequired()
814 @LoginRequired()
815 @NotAnonymous()
815 @NotAnonymous()
816 @HasRepoPermissionAnyDecorator(
816 @HasRepoPermissionAnyDecorator(
817 'repository.read', 'repository.write', 'repository.admin')
817 'repository.read', 'repository.write', 'repository.admin')
818 @view_config(
818 @view_config(
819 route_name='pullrequest_repo_targets', request_method='GET',
819 route_name='pullrequest_repo_targets', request_method='GET',
820 renderer='json_ext', xhr=True)
820 renderer='json_ext', xhr=True)
821 def pullrequest_repo_targets(self):
821 def pullrequest_repo_targets(self):
822 _ = self.request.translate
822 _ = self.request.translate
823 filter_query = self.request.GET.get('query')
823 filter_query = self.request.GET.get('query')
824
824
825 # get the parents
825 # get the parents
826 parent_target_repos = []
826 parent_target_repos = []
827 if self.db_repo.parent:
827 if self.db_repo.parent:
828 parents_query = Repository.query() \
828 parents_query = Repository.query() \
829 .order_by(func.length(Repository.repo_name)) \
829 .order_by(func.length(Repository.repo_name)) \
830 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
830 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
831
831
832 if filter_query:
832 if filter_query:
833 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
833 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
834 parents_query = parents_query.filter(
834 parents_query = parents_query.filter(
835 Repository.repo_name.ilike(ilike_expression))
835 Repository.repo_name.ilike(ilike_expression))
836 parents = parents_query.limit(20).all()
836 parents = parents_query.limit(20).all()
837
837
838 for parent in parents:
838 for parent in parents:
839 parent_vcs_obj = parent.scm_instance()
839 parent_vcs_obj = parent.scm_instance()
840 if parent_vcs_obj and not parent_vcs_obj.is_empty():
840 if parent_vcs_obj and not parent_vcs_obj.is_empty():
841 parent_target_repos.append(parent)
841 parent_target_repos.append(parent)
842
842
843 # get other forks, and repo itself
843 # get other forks, and repo itself
844 query = Repository.query() \
844 query = Repository.query() \
845 .order_by(func.length(Repository.repo_name)) \
845 .order_by(func.length(Repository.repo_name)) \
846 .filter(
846 .filter(
847 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
847 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
848 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
848 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
849 ) \
849 ) \
850 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
850 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
851
851
852 if filter_query:
852 if filter_query:
853 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
853 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
854 query = query.filter(Repository.repo_name.ilike(ilike_expression))
854 query = query.filter(Repository.repo_name.ilike(ilike_expression))
855
855
856 limit = max(20 - len(parent_target_repos), 5) # not less then 5
856 limit = max(20 - len(parent_target_repos), 5) # not less then 5
857 target_repos = query.limit(limit).all()
857 target_repos = query.limit(limit).all()
858
858
859 all_target_repos = target_repos + parent_target_repos
859 all_target_repos = target_repos + parent_target_repos
860
860
861 repos = []
861 repos = []
862 # This checks permissions to the repositories
862 # This checks permissions to the repositories
863 for obj in ScmModel().get_repos(all_target_repos):
863 for obj in ScmModel().get_repos(all_target_repos):
864 repos.append({
864 repos.append({
865 'id': obj['name'],
865 'id': obj['name'],
866 'text': obj['name'],
866 'text': obj['name'],
867 'type': 'repo',
867 'type': 'repo',
868 'repo_id': obj['dbrepo']['repo_id'],
868 'repo_id': obj['dbrepo']['repo_id'],
869 'repo_type': obj['dbrepo']['repo_type'],
869 'repo_type': obj['dbrepo']['repo_type'],
870 'private': obj['dbrepo']['private'],
870 'private': obj['dbrepo']['private'],
871
871
872 })
872 })
873
873
874 data = {
874 data = {
875 'more': False,
875 'more': False,
876 'results': [{
876 'results': [{
877 'text': _('Repositories'),
877 'text': _('Repositories'),
878 'children': repos
878 'children': repos
879 }] if repos else []
879 }] if repos else []
880 }
880 }
881 return data
881 return data
882
882
883 @LoginRequired()
883 @LoginRequired()
884 @NotAnonymous()
884 @NotAnonymous()
885 @HasRepoPermissionAnyDecorator(
885 @HasRepoPermissionAnyDecorator(
886 'repository.read', 'repository.write', 'repository.admin')
886 'repository.read', 'repository.write', 'repository.admin')
887 @CSRFRequired()
887 @CSRFRequired()
888 @view_config(
888 @view_config(
889 route_name='pullrequest_create', request_method='POST',
889 route_name='pullrequest_create', request_method='POST',
890 renderer=None)
890 renderer=None)
891 def pull_request_create(self):
891 def pull_request_create(self):
892 _ = self.request.translate
892 _ = self.request.translate
893 self.assure_not_empty_repo()
893 self.assure_not_empty_repo()
894 self.load_default_context()
894 self.load_default_context()
895
895
896 controls = peppercorn.parse(self.request.POST.items())
896 controls = peppercorn.parse(self.request.POST.items())
897
897
898 try:
898 try:
899 form = PullRequestForm(
899 form = PullRequestForm(
900 self.request.translate, self.db_repo.repo_id)()
900 self.request.translate, self.db_repo.repo_id)()
901 _form = form.to_python(controls)
901 _form = form.to_python(controls)
902 except formencode.Invalid as errors:
902 except formencode.Invalid as errors:
903 if errors.error_dict.get('revisions'):
903 if errors.error_dict.get('revisions'):
904 msg = 'Revisions: %s' % errors.error_dict['revisions']
904 msg = 'Revisions: %s' % errors.error_dict['revisions']
905 elif errors.error_dict.get('pullrequest_title'):
905 elif errors.error_dict.get('pullrequest_title'):
906 msg = errors.error_dict.get('pullrequest_title')
906 msg = errors.error_dict.get('pullrequest_title')
907 else:
907 else:
908 msg = _('Error creating pull request: {}').format(errors)
908 msg = _('Error creating pull request: {}').format(errors)
909 log.exception(msg)
909 log.exception(msg)
910 h.flash(msg, 'error')
910 h.flash(msg, 'error')
911
911
912 # would rather just go back to form ...
912 # would rather just go back to form ...
913 raise HTTPFound(
913 raise HTTPFound(
914 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
914 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
915
915
916 source_repo = _form['source_repo']
916 source_repo = _form['source_repo']
917 source_ref = _form['source_ref']
917 source_ref = _form['source_ref']
918 target_repo = _form['target_repo']
918 target_repo = _form['target_repo']
919 target_ref = _form['target_ref']
919 target_ref = _form['target_ref']
920 commit_ids = _form['revisions'][::-1]
920 commit_ids = _form['revisions'][::-1]
921
921
922 # find the ancestor for this pr
922 # find the ancestor for this pr
923 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
923 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
924 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
924 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
925
925
926 if not (source_db_repo or target_db_repo):
926 if not (source_db_repo or target_db_repo):
927 h.flash(_('source_repo or target repo not found'), category='error')
927 h.flash(_('source_repo or target repo not found'), category='error')
928 raise HTTPFound(
928 raise HTTPFound(
929 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
929 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
930
930
931 # re-check permissions again here
931 # re-check permissions again here
932 # source_repo we must have read permissions
932 # source_repo we must have read permissions
933
933
934 source_perm = HasRepoPermissionAny(
934 source_perm = HasRepoPermissionAny(
935 'repository.read', 'repository.write', 'repository.admin')(
935 'repository.read', 'repository.write', 'repository.admin')(
936 source_db_repo.repo_name)
936 source_db_repo.repo_name)
937 if not source_perm:
937 if not source_perm:
938 msg = _('Not Enough permissions to source repo `{}`.'.format(
938 msg = _('Not Enough permissions to source repo `{}`.'.format(
939 source_db_repo.repo_name))
939 source_db_repo.repo_name))
940 h.flash(msg, category='error')
940 h.flash(msg, category='error')
941 # copy the args back to redirect
941 # copy the args back to redirect
942 org_query = self.request.GET.mixed()
942 org_query = self.request.GET.mixed()
943 raise HTTPFound(
943 raise HTTPFound(
944 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
944 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
945 _query=org_query))
945 _query=org_query))
946
946
947 # target repo we must have read permissions, and also later on
947 # target repo we must have read permissions, and also later on
948 # we want to check branch permissions here
948 # we want to check branch permissions here
949 target_perm = HasRepoPermissionAny(
949 target_perm = HasRepoPermissionAny(
950 'repository.read', 'repository.write', 'repository.admin')(
950 'repository.read', 'repository.write', 'repository.admin')(
951 target_db_repo.repo_name)
951 target_db_repo.repo_name)
952 if not target_perm:
952 if not target_perm:
953 msg = _('Not Enough permissions to target repo `{}`.'.format(
953 msg = _('Not Enough permissions to target repo `{}`.'.format(
954 target_db_repo.repo_name))
954 target_db_repo.repo_name))
955 h.flash(msg, category='error')
955 h.flash(msg, category='error')
956 # copy the args back to redirect
956 # copy the args back to redirect
957 org_query = self.request.GET.mixed()
957 org_query = self.request.GET.mixed()
958 raise HTTPFound(
958 raise HTTPFound(
959 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
959 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
960 _query=org_query))
960 _query=org_query))
961
961
962 source_scm = source_db_repo.scm_instance()
962 source_scm = source_db_repo.scm_instance()
963 target_scm = target_db_repo.scm_instance()
963 target_scm = target_db_repo.scm_instance()
964
964
965 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
965 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
966 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
966 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
967
967
968 ancestor = source_scm.get_common_ancestor(
968 ancestor = source_scm.get_common_ancestor(
969 source_commit.raw_id, target_commit.raw_id, target_scm)
969 source_commit.raw_id, target_commit.raw_id, target_scm)
970
970
971 # recalculate target ref based on ancestor
971 # recalculate target ref based on ancestor
972 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
972 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
973 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
973 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
974
974
975 get_default_reviewers_data, validate_default_reviewers = \
975 get_default_reviewers_data, validate_default_reviewers = \
976 PullRequestModel().get_reviewer_functions()
976 PullRequestModel().get_reviewer_functions()
977
977
978 # recalculate reviewers logic, to make sure we can validate this
978 # recalculate reviewers logic, to make sure we can validate this
979 reviewer_rules = get_default_reviewers_data(
979 reviewer_rules = get_default_reviewers_data(
980 self._rhodecode_db_user, source_db_repo,
980 self._rhodecode_db_user, source_db_repo,
981 source_commit, target_db_repo, target_commit)
981 source_commit, target_db_repo, target_commit)
982
982
983 given_reviewers = _form['review_members']
983 given_reviewers = _form['review_members']
984 reviewers = validate_default_reviewers(
984 reviewers = validate_default_reviewers(
985 given_reviewers, reviewer_rules)
985 given_reviewers, reviewer_rules)
986
986
987 pullrequest_title = _form['pullrequest_title']
987 pullrequest_title = _form['pullrequest_title']
988 title_source_ref = source_ref.split(':', 2)[1]
988 title_source_ref = source_ref.split(':', 2)[1]
989 if not pullrequest_title:
989 if not pullrequest_title:
990 pullrequest_title = PullRequestModel().generate_pullrequest_title(
990 pullrequest_title = PullRequestModel().generate_pullrequest_title(
991 source=source_repo,
991 source=source_repo,
992 source_ref=title_source_ref,
992 source_ref=title_source_ref,
993 target=target_repo
993 target=target_repo
994 )
994 )
995
995
996 description = _form['pullrequest_desc']
996 description = _form['pullrequest_desc']
997 description_renderer = _form['description_renderer']
997 description_renderer = _form['description_renderer']
998
998
999 try:
999 try:
1000 pull_request = PullRequestModel().create(
1000 pull_request = PullRequestModel().create(
1001 created_by=self._rhodecode_user.user_id,
1001 created_by=self._rhodecode_user.user_id,
1002 source_repo=source_repo,
1002 source_repo=source_repo,
1003 source_ref=source_ref,
1003 source_ref=source_ref,
1004 target_repo=target_repo,
1004 target_repo=target_repo,
1005 target_ref=target_ref,
1005 target_ref=target_ref,
1006 revisions=commit_ids,
1006 revisions=commit_ids,
1007 reviewers=reviewers,
1007 reviewers=reviewers,
1008 title=pullrequest_title,
1008 title=pullrequest_title,
1009 description=description,
1009 description=description,
1010 description_renderer=description_renderer,
1010 description_renderer=description_renderer,
1011 reviewer_data=reviewer_rules,
1011 reviewer_data=reviewer_rules,
1012 auth_user=self._rhodecode_user
1012 auth_user=self._rhodecode_user
1013 )
1013 )
1014 Session().commit()
1014 Session().commit()
1015
1015
1016 h.flash(_('Successfully opened new pull request'),
1016 h.flash(_('Successfully opened new pull request'),
1017 category='success')
1017 category='success')
1018 except Exception:
1018 except Exception:
1019 msg = _('Error occurred during creation of this pull request.')
1019 msg = _('Error occurred during creation of this pull request.')
1020 log.exception(msg)
1020 log.exception(msg)
1021 h.flash(msg, category='error')
1021 h.flash(msg, category='error')
1022
1022
1023 # copy the args back to redirect
1023 # copy the args back to redirect
1024 org_query = self.request.GET.mixed()
1024 org_query = self.request.GET.mixed()
1025 raise HTTPFound(
1025 raise HTTPFound(
1026 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1026 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1027 _query=org_query))
1027 _query=org_query))
1028
1028
1029 raise HTTPFound(
1029 raise HTTPFound(
1030 h.route_path('pullrequest_show', repo_name=target_repo,
1030 h.route_path('pullrequest_show', repo_name=target_repo,
1031 pull_request_id=pull_request.pull_request_id))
1031 pull_request_id=pull_request.pull_request_id))
1032
1032
1033 @LoginRequired()
1033 @LoginRequired()
1034 @NotAnonymous()
1034 @NotAnonymous()
1035 @HasRepoPermissionAnyDecorator(
1035 @HasRepoPermissionAnyDecorator(
1036 'repository.read', 'repository.write', 'repository.admin')
1036 'repository.read', 'repository.write', 'repository.admin')
1037 @CSRFRequired()
1037 @CSRFRequired()
1038 @view_config(
1038 @view_config(
1039 route_name='pullrequest_update', request_method='POST',
1039 route_name='pullrequest_update', request_method='POST',
1040 renderer='json_ext')
1040 renderer='json_ext')
1041 def pull_request_update(self):
1041 def pull_request_update(self):
1042 pull_request = PullRequest.get_or_404(
1042 pull_request = PullRequest.get_or_404(
1043 self.request.matchdict['pull_request_id'])
1043 self.request.matchdict['pull_request_id'])
1044 _ = self.request.translate
1044 _ = self.request.translate
1045
1045
1046 self.load_default_context()
1046 self.load_default_context()
1047 redirect_url = None
1047 redirect_url = None
1048
1048
1049 if pull_request.is_closed():
1049 if pull_request.is_closed():
1050 log.debug('update: forbidden because pull request is closed')
1050 log.debug('update: forbidden because pull request is closed')
1051 msg = _(u'Cannot update closed pull requests.')
1051 msg = _(u'Cannot update closed pull requests.')
1052 h.flash(msg, category='error')
1052 h.flash(msg, category='error')
1053 return {'response': True,
1053 return {'response': True,
1054 'redirect_url': redirect_url}
1054 'redirect_url': redirect_url}
1055
1055
1056 is_state_changing = pull_request.is_state_changing()
1056 is_state_changing = pull_request.is_state_changing()
1057
1057
1058 # only owner or admin can update it
1058 # only owner or admin can update it
1059 allowed_to_update = PullRequestModel().check_user_update(
1059 allowed_to_update = PullRequestModel().check_user_update(
1060 pull_request, self._rhodecode_user)
1060 pull_request, self._rhodecode_user)
1061 if allowed_to_update:
1061 if allowed_to_update:
1062 controls = peppercorn.parse(self.request.POST.items())
1062 controls = peppercorn.parse(self.request.POST.items())
1063 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1063 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1064
1064
1065 if 'review_members' in controls:
1065 if 'review_members' in controls:
1066 self._update_reviewers(
1066 self._update_reviewers(
1067 pull_request, controls['review_members'],
1067 pull_request, controls['review_members'],
1068 pull_request.reviewer_data)
1068 pull_request.reviewer_data)
1069 elif str2bool(self.request.POST.get('update_commits', 'false')):
1069 elif str2bool(self.request.POST.get('update_commits', 'false')):
1070 if is_state_changing:
1070 if is_state_changing:
1071 log.debug('commits update: forbidden because pull request is in state %s',
1071 log.debug('commits update: forbidden because pull request is in state %s',
1072 pull_request.pull_request_state)
1072 pull_request.pull_request_state)
1073 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1073 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1074 u'Current state is: `{}`').format(
1074 u'Current state is: `{}`').format(
1075 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1075 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1076 h.flash(msg, category='error')
1076 h.flash(msg, category='error')
1077 return {'response': True,
1077 return {'response': True,
1078 'redirect_url': redirect_url}
1078 'redirect_url': redirect_url}
1079
1079
1080 self._update_commits(pull_request)
1080 self._update_commits(pull_request)
1081 if force_refresh:
1081 if force_refresh:
1082 redirect_url = h.route_path(
1082 redirect_url = h.route_path(
1083 'pullrequest_show', repo_name=self.db_repo_name,
1083 'pullrequest_show', repo_name=self.db_repo_name,
1084 pull_request_id=pull_request.pull_request_id,
1084 pull_request_id=pull_request.pull_request_id,
1085 _query={"force_refresh": 1})
1085 _query={"force_refresh": 1})
1086 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1086 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1087 self._edit_pull_request(pull_request)
1087 self._edit_pull_request(pull_request)
1088 else:
1088 else:
1089 raise HTTPBadRequest()
1089 raise HTTPBadRequest()
1090
1090
1091 return {'response': True,
1091 return {'response': True,
1092 'redirect_url': redirect_url}
1092 'redirect_url': redirect_url}
1093 raise HTTPForbidden()
1093 raise HTTPForbidden()
1094
1094
1095 def _edit_pull_request(self, pull_request):
1095 def _edit_pull_request(self, pull_request):
1096 _ = self.request.translate
1096 _ = self.request.translate
1097
1097
1098 try:
1098 try:
1099 PullRequestModel().edit(
1099 PullRequestModel().edit(
1100 pull_request,
1100 pull_request,
1101 self.request.POST.get('title'),
1101 self.request.POST.get('title'),
1102 self.request.POST.get('description'),
1102 self.request.POST.get('description'),
1103 self.request.POST.get('description_renderer'),
1103 self.request.POST.get('description_renderer'),
1104 self._rhodecode_user)
1104 self._rhodecode_user)
1105 except ValueError:
1105 except ValueError:
1106 msg = _(u'Cannot update closed pull requests.')
1106 msg = _(u'Cannot update closed pull requests.')
1107 h.flash(msg, category='error')
1107 h.flash(msg, category='error')
1108 return
1108 return
1109 else:
1109 else:
1110 Session().commit()
1110 Session().commit()
1111
1111
1112 msg = _(u'Pull request title & description updated.')
1112 msg = _(u'Pull request title & description updated.')
1113 h.flash(msg, category='success')
1113 h.flash(msg, category='success')
1114 return
1114 return
1115
1115
1116 def _update_commits(self, pull_request):
1116 def _update_commits(self, pull_request):
1117 _ = self.request.translate
1117 _ = self.request.translate
1118
1118
1119 with pull_request.set_state(PullRequest.STATE_UPDATING):
1119 with pull_request.set_state(PullRequest.STATE_UPDATING):
1120 resp = PullRequestModel().update_commits(pull_request)
1120 resp = PullRequestModel().update_commits(
1121 pull_request, self._rhodecode_db_user)
1121
1122
1122 if resp.executed:
1123 if resp.executed:
1123
1124
1124 if resp.target_changed and resp.source_changed:
1125 if resp.target_changed and resp.source_changed:
1125 changed = 'target and source repositories'
1126 changed = 'target and source repositories'
1126 elif resp.target_changed and not resp.source_changed:
1127 elif resp.target_changed and not resp.source_changed:
1127 changed = 'target repository'
1128 changed = 'target repository'
1128 elif not resp.target_changed and resp.source_changed:
1129 elif not resp.target_changed and resp.source_changed:
1129 changed = 'source repository'
1130 changed = 'source repository'
1130 else:
1131 else:
1131 changed = 'nothing'
1132 changed = 'nothing'
1132
1133
1133 msg = _(u'Pull request updated to "{source_commit_id}" with '
1134 msg = _(u'Pull request updated to "{source_commit_id}" with '
1134 u'{count_added} added, {count_removed} removed commits. '
1135 u'{count_added} added, {count_removed} removed commits. '
1135 u'Source of changes: {change_source}')
1136 u'Source of changes: {change_source}')
1136 msg = msg.format(
1137 msg = msg.format(
1137 source_commit_id=pull_request.source_ref_parts.commit_id,
1138 source_commit_id=pull_request.source_ref_parts.commit_id,
1138 count_added=len(resp.changes.added),
1139 count_added=len(resp.changes.added),
1139 count_removed=len(resp.changes.removed),
1140 count_removed=len(resp.changes.removed),
1140 change_source=changed)
1141 change_source=changed)
1141 h.flash(msg, category='success')
1142 h.flash(msg, category='success')
1142
1143
1143 channel = '/repo${}$/pr/{}'.format(
1144 channel = '/repo${}$/pr/{}'.format(
1144 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1145 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1145 message = msg + (
1146 message = msg + (
1146 ' - <a onclick="window.location.reload()">'
1147 ' - <a onclick="window.location.reload()">'
1147 '<strong>{}</strong></a>'.format(_('Reload page')))
1148 '<strong>{}</strong></a>'.format(_('Reload page')))
1148 channelstream.post_message(
1149 channelstream.post_message(
1149 channel, message, self._rhodecode_user.username,
1150 channel, message, self._rhodecode_user.username,
1150 registry=self.request.registry)
1151 registry=self.request.registry)
1151 else:
1152 else:
1152 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1153 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1153 warning_reasons = [
1154 warning_reasons = [
1154 UpdateFailureReason.NO_CHANGE,
1155 UpdateFailureReason.NO_CHANGE,
1155 UpdateFailureReason.WRONG_REF_TYPE,
1156 UpdateFailureReason.WRONG_REF_TYPE,
1156 ]
1157 ]
1157 category = 'warning' if resp.reason in warning_reasons else 'error'
1158 category = 'warning' if resp.reason in warning_reasons else 'error'
1158 h.flash(msg, category=category)
1159 h.flash(msg, category=category)
1159
1160
1160 @LoginRequired()
1161 @LoginRequired()
1161 @NotAnonymous()
1162 @NotAnonymous()
1162 @HasRepoPermissionAnyDecorator(
1163 @HasRepoPermissionAnyDecorator(
1163 'repository.read', 'repository.write', 'repository.admin')
1164 'repository.read', 'repository.write', 'repository.admin')
1164 @CSRFRequired()
1165 @CSRFRequired()
1165 @view_config(
1166 @view_config(
1166 route_name='pullrequest_merge', request_method='POST',
1167 route_name='pullrequest_merge', request_method='POST',
1167 renderer='json_ext')
1168 renderer='json_ext')
1168 def pull_request_merge(self):
1169 def pull_request_merge(self):
1169 """
1170 """
1170 Merge will perform a server-side merge of the specified
1171 Merge will perform a server-side merge of the specified
1171 pull request, if the pull request is approved and mergeable.
1172 pull request, if the pull request is approved and mergeable.
1172 After successful merging, the pull request is automatically
1173 After successful merging, the pull request is automatically
1173 closed, with a relevant comment.
1174 closed, with a relevant comment.
1174 """
1175 """
1175 pull_request = PullRequest.get_or_404(
1176 pull_request = PullRequest.get_or_404(
1176 self.request.matchdict['pull_request_id'])
1177 self.request.matchdict['pull_request_id'])
1177 _ = self.request.translate
1178 _ = self.request.translate
1178
1179
1179 if pull_request.is_state_changing():
1180 if pull_request.is_state_changing():
1180 log.debug('show: forbidden because pull request is in state %s',
1181 log.debug('show: forbidden because pull request is in state %s',
1181 pull_request.pull_request_state)
1182 pull_request.pull_request_state)
1182 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1183 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1183 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1184 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1184 pull_request.pull_request_state)
1185 pull_request.pull_request_state)
1185 h.flash(msg, category='error')
1186 h.flash(msg, category='error')
1186 raise HTTPFound(
1187 raise HTTPFound(
1187 h.route_path('pullrequest_show',
1188 h.route_path('pullrequest_show',
1188 repo_name=pull_request.target_repo.repo_name,
1189 repo_name=pull_request.target_repo.repo_name,
1189 pull_request_id=pull_request.pull_request_id))
1190 pull_request_id=pull_request.pull_request_id))
1190
1191
1191 self.load_default_context()
1192 self.load_default_context()
1192
1193
1193 with pull_request.set_state(PullRequest.STATE_UPDATING):
1194 with pull_request.set_state(PullRequest.STATE_UPDATING):
1194 check = MergeCheck.validate(
1195 check = MergeCheck.validate(
1195 pull_request, auth_user=self._rhodecode_user,
1196 pull_request, auth_user=self._rhodecode_user,
1196 translator=self.request.translate)
1197 translator=self.request.translate)
1197 merge_possible = not check.failed
1198 merge_possible = not check.failed
1198
1199
1199 for err_type, error_msg in check.errors:
1200 for err_type, error_msg in check.errors:
1200 h.flash(error_msg, category=err_type)
1201 h.flash(error_msg, category=err_type)
1201
1202
1202 if merge_possible:
1203 if merge_possible:
1203 log.debug("Pre-conditions checked, trying to merge.")
1204 log.debug("Pre-conditions checked, trying to merge.")
1204 extras = vcs_operation_context(
1205 extras = vcs_operation_context(
1205 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1206 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1206 username=self._rhodecode_db_user.username, action='push',
1207 username=self._rhodecode_db_user.username, action='push',
1207 scm=pull_request.target_repo.repo_type)
1208 scm=pull_request.target_repo.repo_type)
1208 with pull_request.set_state(PullRequest.STATE_UPDATING):
1209 with pull_request.set_state(PullRequest.STATE_UPDATING):
1209 self._merge_pull_request(
1210 self._merge_pull_request(
1210 pull_request, self._rhodecode_db_user, extras)
1211 pull_request, self._rhodecode_db_user, extras)
1211 else:
1212 else:
1212 log.debug("Pre-conditions failed, NOT merging.")
1213 log.debug("Pre-conditions failed, NOT merging.")
1213
1214
1214 raise HTTPFound(
1215 raise HTTPFound(
1215 h.route_path('pullrequest_show',
1216 h.route_path('pullrequest_show',
1216 repo_name=pull_request.target_repo.repo_name,
1217 repo_name=pull_request.target_repo.repo_name,
1217 pull_request_id=pull_request.pull_request_id))
1218 pull_request_id=pull_request.pull_request_id))
1218
1219
1219 def _merge_pull_request(self, pull_request, user, extras):
1220 def _merge_pull_request(self, pull_request, user, extras):
1220 _ = self.request.translate
1221 _ = self.request.translate
1221 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1222 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1222
1223
1223 if merge_resp.executed:
1224 if merge_resp.executed:
1224 log.debug("The merge was successful, closing the pull request.")
1225 log.debug("The merge was successful, closing the pull request.")
1225 PullRequestModel().close_pull_request(
1226 PullRequestModel().close_pull_request(
1226 pull_request.pull_request_id, user)
1227 pull_request.pull_request_id, user)
1227 Session().commit()
1228 Session().commit()
1228 msg = _('Pull request was successfully merged and closed.')
1229 msg = _('Pull request was successfully merged and closed.')
1229 h.flash(msg, category='success')
1230 h.flash(msg, category='success')
1230 else:
1231 else:
1231 log.debug(
1232 log.debug(
1232 "The merge was not successful. Merge response: %s", merge_resp)
1233 "The merge was not successful. Merge response: %s", merge_resp)
1233 msg = merge_resp.merge_status_message
1234 msg = merge_resp.merge_status_message
1234 h.flash(msg, category='error')
1235 h.flash(msg, category='error')
1235
1236
1236 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1237 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1237 _ = self.request.translate
1238 _ = self.request.translate
1238
1239
1239 get_default_reviewers_data, validate_default_reviewers = \
1240 get_default_reviewers_data, validate_default_reviewers = \
1240 PullRequestModel().get_reviewer_functions()
1241 PullRequestModel().get_reviewer_functions()
1241
1242
1242 try:
1243 try:
1243 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1244 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1244 except ValueError as e:
1245 except ValueError as e:
1245 log.error('Reviewers Validation: {}'.format(e))
1246 log.error('Reviewers Validation: {}'.format(e))
1246 h.flash(e, category='error')
1247 h.flash(e, category='error')
1247 return
1248 return
1248
1249
1249 old_calculated_status = pull_request.calculated_review_status()
1250 old_calculated_status = pull_request.calculated_review_status()
1250 PullRequestModel().update_reviewers(
1251 PullRequestModel().update_reviewers(
1251 pull_request, reviewers, self._rhodecode_user)
1252 pull_request, reviewers, self._rhodecode_user)
1252 h.flash(_('Pull request reviewers updated.'), category='success')
1253 h.flash(_('Pull request reviewers updated.'), category='success')
1253 Session().commit()
1254 Session().commit()
1254
1255
1255 # trigger status changed if change in reviewers changes the status
1256 # trigger status changed if change in reviewers changes the status
1256 calculated_status = pull_request.calculated_review_status()
1257 calculated_status = pull_request.calculated_review_status()
1257 if old_calculated_status != calculated_status:
1258 if old_calculated_status != calculated_status:
1258 PullRequestModel().trigger_pull_request_hook(
1259 PullRequestModel().trigger_pull_request_hook(
1259 pull_request, self._rhodecode_user, 'review_status_change',
1260 pull_request, self._rhodecode_user, 'review_status_change',
1260 data={'status': calculated_status})
1261 data={'status': calculated_status})
1261
1262
1262 @LoginRequired()
1263 @LoginRequired()
1263 @NotAnonymous()
1264 @NotAnonymous()
1264 @HasRepoPermissionAnyDecorator(
1265 @HasRepoPermissionAnyDecorator(
1265 'repository.read', 'repository.write', 'repository.admin')
1266 'repository.read', 'repository.write', 'repository.admin')
1266 @CSRFRequired()
1267 @CSRFRequired()
1267 @view_config(
1268 @view_config(
1268 route_name='pullrequest_delete', request_method='POST',
1269 route_name='pullrequest_delete', request_method='POST',
1269 renderer='json_ext')
1270 renderer='json_ext')
1270 def pull_request_delete(self):
1271 def pull_request_delete(self):
1271 _ = self.request.translate
1272 _ = self.request.translate
1272
1273
1273 pull_request = PullRequest.get_or_404(
1274 pull_request = PullRequest.get_or_404(
1274 self.request.matchdict['pull_request_id'])
1275 self.request.matchdict['pull_request_id'])
1275 self.load_default_context()
1276 self.load_default_context()
1276
1277
1277 pr_closed = pull_request.is_closed()
1278 pr_closed = pull_request.is_closed()
1278 allowed_to_delete = PullRequestModel().check_user_delete(
1279 allowed_to_delete = PullRequestModel().check_user_delete(
1279 pull_request, self._rhodecode_user) and not pr_closed
1280 pull_request, self._rhodecode_user) and not pr_closed
1280
1281
1281 # only owner can delete it !
1282 # only owner can delete it !
1282 if allowed_to_delete:
1283 if allowed_to_delete:
1283 PullRequestModel().delete(pull_request, self._rhodecode_user)
1284 PullRequestModel().delete(pull_request, self._rhodecode_user)
1284 Session().commit()
1285 Session().commit()
1285 h.flash(_('Successfully deleted pull request'),
1286 h.flash(_('Successfully deleted pull request'),
1286 category='success')
1287 category='success')
1287 raise HTTPFound(h.route_path('pullrequest_show_all',
1288 raise HTTPFound(h.route_path('pullrequest_show_all',
1288 repo_name=self.db_repo_name))
1289 repo_name=self.db_repo_name))
1289
1290
1290 log.warning('user %s tried to delete pull request without access',
1291 log.warning('user %s tried to delete pull request without access',
1291 self._rhodecode_user)
1292 self._rhodecode_user)
1292 raise HTTPNotFound()
1293 raise HTTPNotFound()
1293
1294
1294 @LoginRequired()
1295 @LoginRequired()
1295 @NotAnonymous()
1296 @NotAnonymous()
1296 @HasRepoPermissionAnyDecorator(
1297 @HasRepoPermissionAnyDecorator(
1297 'repository.read', 'repository.write', 'repository.admin')
1298 'repository.read', 'repository.write', 'repository.admin')
1298 @CSRFRequired()
1299 @CSRFRequired()
1299 @view_config(
1300 @view_config(
1300 route_name='pullrequest_comment_create', request_method='POST',
1301 route_name='pullrequest_comment_create', request_method='POST',
1301 renderer='json_ext')
1302 renderer='json_ext')
1302 def pull_request_comment_create(self):
1303 def pull_request_comment_create(self):
1303 _ = self.request.translate
1304 _ = self.request.translate
1304
1305
1305 pull_request = PullRequest.get_or_404(
1306 pull_request = PullRequest.get_or_404(
1306 self.request.matchdict['pull_request_id'])
1307 self.request.matchdict['pull_request_id'])
1307 pull_request_id = pull_request.pull_request_id
1308 pull_request_id = pull_request.pull_request_id
1308
1309
1309 if pull_request.is_closed():
1310 if pull_request.is_closed():
1310 log.debug('comment: forbidden because pull request is closed')
1311 log.debug('comment: forbidden because pull request is closed')
1311 raise HTTPForbidden()
1312 raise HTTPForbidden()
1312
1313
1313 allowed_to_comment = PullRequestModel().check_user_comment(
1314 allowed_to_comment = PullRequestModel().check_user_comment(
1314 pull_request, self._rhodecode_user)
1315 pull_request, self._rhodecode_user)
1315 if not allowed_to_comment:
1316 if not allowed_to_comment:
1316 log.debug(
1317 log.debug(
1317 'comment: forbidden because pull request is from forbidden repo')
1318 'comment: forbidden because pull request is from forbidden repo')
1318 raise HTTPForbidden()
1319 raise HTTPForbidden()
1319
1320
1320 c = self.load_default_context()
1321 c = self.load_default_context()
1321
1322
1322 status = self.request.POST.get('changeset_status', None)
1323 status = self.request.POST.get('changeset_status', None)
1323 text = self.request.POST.get('text')
1324 text = self.request.POST.get('text')
1324 comment_type = self.request.POST.get('comment_type')
1325 comment_type = self.request.POST.get('comment_type')
1325 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1326 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1326 close_pull_request = self.request.POST.get('close_pull_request')
1327 close_pull_request = self.request.POST.get('close_pull_request')
1327
1328
1328 # the logic here should work like following, if we submit close
1329 # the logic here should work like following, if we submit close
1329 # pr comment, use `close_pull_request_with_comment` function
1330 # pr comment, use `close_pull_request_with_comment` function
1330 # else handle regular comment logic
1331 # else handle regular comment logic
1331
1332
1332 if close_pull_request:
1333 if close_pull_request:
1333 # only owner or admin or person with write permissions
1334 # only owner or admin or person with write permissions
1334 allowed_to_close = PullRequestModel().check_user_update(
1335 allowed_to_close = PullRequestModel().check_user_update(
1335 pull_request, self._rhodecode_user)
1336 pull_request, self._rhodecode_user)
1336 if not allowed_to_close:
1337 if not allowed_to_close:
1337 log.debug('comment: forbidden because not allowed to close '
1338 log.debug('comment: forbidden because not allowed to close '
1338 'pull request %s', pull_request_id)
1339 'pull request %s', pull_request_id)
1339 raise HTTPForbidden()
1340 raise HTTPForbidden()
1340
1341
1341 # This also triggers `review_status_change`
1342 # This also triggers `review_status_change`
1342 comment, status = PullRequestModel().close_pull_request_with_comment(
1343 comment, status = PullRequestModel().close_pull_request_with_comment(
1343 pull_request, self._rhodecode_user, self.db_repo, message=text,
1344 pull_request, self._rhodecode_user, self.db_repo, message=text,
1344 auth_user=self._rhodecode_user)
1345 auth_user=self._rhodecode_user)
1345 Session().flush()
1346 Session().flush()
1346
1347
1347 PullRequestModel().trigger_pull_request_hook(
1348 PullRequestModel().trigger_pull_request_hook(
1348 pull_request, self._rhodecode_user, 'comment',
1349 pull_request, self._rhodecode_user, 'comment',
1349 data={'comment': comment})
1350 data={'comment': comment})
1350
1351
1351 else:
1352 else:
1352 # regular comment case, could be inline, or one with status.
1353 # regular comment case, could be inline, or one with status.
1353 # for that one we check also permissions
1354 # for that one we check also permissions
1354
1355
1355 allowed_to_change_status = PullRequestModel().check_user_change_status(
1356 allowed_to_change_status = PullRequestModel().check_user_change_status(
1356 pull_request, self._rhodecode_user)
1357 pull_request, self._rhodecode_user)
1357
1358
1358 if status and allowed_to_change_status:
1359 if status and allowed_to_change_status:
1359 message = (_('Status change %(transition_icon)s %(status)s')
1360 message = (_('Status change %(transition_icon)s %(status)s')
1360 % {'transition_icon': '>',
1361 % {'transition_icon': '>',
1361 'status': ChangesetStatus.get_status_lbl(status)})
1362 'status': ChangesetStatus.get_status_lbl(status)})
1362 text = text or message
1363 text = text or message
1363
1364
1364 comment = CommentsModel().create(
1365 comment = CommentsModel().create(
1365 text=text,
1366 text=text,
1366 repo=self.db_repo.repo_id,
1367 repo=self.db_repo.repo_id,
1367 user=self._rhodecode_user.user_id,
1368 user=self._rhodecode_user.user_id,
1368 pull_request=pull_request,
1369 pull_request=pull_request,
1369 f_path=self.request.POST.get('f_path'),
1370 f_path=self.request.POST.get('f_path'),
1370 line_no=self.request.POST.get('line'),
1371 line_no=self.request.POST.get('line'),
1371 status_change=(ChangesetStatus.get_status_lbl(status)
1372 status_change=(ChangesetStatus.get_status_lbl(status)
1372 if status and allowed_to_change_status else None),
1373 if status and allowed_to_change_status else None),
1373 status_change_type=(status
1374 status_change_type=(status
1374 if status and allowed_to_change_status else None),
1375 if status and allowed_to_change_status else None),
1375 comment_type=comment_type,
1376 comment_type=comment_type,
1376 resolves_comment_id=resolves_comment_id,
1377 resolves_comment_id=resolves_comment_id,
1377 auth_user=self._rhodecode_user
1378 auth_user=self._rhodecode_user
1378 )
1379 )
1379
1380
1380 if allowed_to_change_status:
1381 if allowed_to_change_status:
1381 # calculate old status before we change it
1382 # calculate old status before we change it
1382 old_calculated_status = pull_request.calculated_review_status()
1383 old_calculated_status = pull_request.calculated_review_status()
1383
1384
1384 # get status if set !
1385 # get status if set !
1385 if status:
1386 if status:
1386 ChangesetStatusModel().set_status(
1387 ChangesetStatusModel().set_status(
1387 self.db_repo.repo_id,
1388 self.db_repo.repo_id,
1388 status,
1389 status,
1389 self._rhodecode_user.user_id,
1390 self._rhodecode_user.user_id,
1390 comment,
1391 comment,
1391 pull_request=pull_request
1392 pull_request=pull_request
1392 )
1393 )
1393
1394
1394 Session().flush()
1395 Session().flush()
1395 # this is somehow required to get access to some relationship
1396 # this is somehow required to get access to some relationship
1396 # loaded on comment
1397 # loaded on comment
1397 Session().refresh(comment)
1398 Session().refresh(comment)
1398
1399
1399 PullRequestModel().trigger_pull_request_hook(
1400 PullRequestModel().trigger_pull_request_hook(
1400 pull_request, self._rhodecode_user, 'comment',
1401 pull_request, self._rhodecode_user, 'comment',
1401 data={'comment': comment})
1402 data={'comment': comment})
1402
1403
1403 # we now calculate the status of pull request, and based on that
1404 # we now calculate the status of pull request, and based on that
1404 # calculation we set the commits status
1405 # calculation we set the commits status
1405 calculated_status = pull_request.calculated_review_status()
1406 calculated_status = pull_request.calculated_review_status()
1406 if old_calculated_status != calculated_status:
1407 if old_calculated_status != calculated_status:
1407 PullRequestModel().trigger_pull_request_hook(
1408 PullRequestModel().trigger_pull_request_hook(
1408 pull_request, self._rhodecode_user, 'review_status_change',
1409 pull_request, self._rhodecode_user, 'review_status_change',
1409 data={'status': calculated_status})
1410 data={'status': calculated_status})
1410
1411
1411 Session().commit()
1412 Session().commit()
1412
1413
1413 data = {
1414 data = {
1414 'target_id': h.safeid(h.safe_unicode(
1415 'target_id': h.safeid(h.safe_unicode(
1415 self.request.POST.get('f_path'))),
1416 self.request.POST.get('f_path'))),
1416 }
1417 }
1417 if comment:
1418 if comment:
1418 c.co = comment
1419 c.co = comment
1419 rendered_comment = render(
1420 rendered_comment = render(
1420 'rhodecode:templates/changeset/changeset_comment_block.mako',
1421 'rhodecode:templates/changeset/changeset_comment_block.mako',
1421 self._get_template_context(c), self.request)
1422 self._get_template_context(c), self.request)
1422
1423
1423 data.update(comment.get_dict())
1424 data.update(comment.get_dict())
1424 data.update({'rendered_text': rendered_comment})
1425 data.update({'rendered_text': rendered_comment})
1425
1426
1426 return data
1427 return data
1427
1428
1428 @LoginRequired()
1429 @LoginRequired()
1429 @NotAnonymous()
1430 @NotAnonymous()
1430 @HasRepoPermissionAnyDecorator(
1431 @HasRepoPermissionAnyDecorator(
1431 'repository.read', 'repository.write', 'repository.admin')
1432 'repository.read', 'repository.write', 'repository.admin')
1432 @CSRFRequired()
1433 @CSRFRequired()
1433 @view_config(
1434 @view_config(
1434 route_name='pullrequest_comment_delete', request_method='POST',
1435 route_name='pullrequest_comment_delete', request_method='POST',
1435 renderer='json_ext')
1436 renderer='json_ext')
1436 def pull_request_comment_delete(self):
1437 def pull_request_comment_delete(self):
1437 pull_request = PullRequest.get_or_404(
1438 pull_request = PullRequest.get_or_404(
1438 self.request.matchdict['pull_request_id'])
1439 self.request.matchdict['pull_request_id'])
1439
1440
1440 comment = ChangesetComment.get_or_404(
1441 comment = ChangesetComment.get_or_404(
1441 self.request.matchdict['comment_id'])
1442 self.request.matchdict['comment_id'])
1442 comment_id = comment.comment_id
1443 comment_id = comment.comment_id
1443
1444
1444 if pull_request.is_closed():
1445 if pull_request.is_closed():
1445 log.debug('comment: forbidden because pull request is closed')
1446 log.debug('comment: forbidden because pull request is closed')
1446 raise HTTPForbidden()
1447 raise HTTPForbidden()
1447
1448
1448 if not comment:
1449 if not comment:
1449 log.debug('Comment with id:%s not found, skipping', comment_id)
1450 log.debug('Comment with id:%s not found, skipping', comment_id)
1450 # comment already deleted in another call probably
1451 # comment already deleted in another call probably
1451 return True
1452 return True
1452
1453
1453 if comment.pull_request.is_closed():
1454 if comment.pull_request.is_closed():
1454 # don't allow deleting comments on closed pull request
1455 # don't allow deleting comments on closed pull request
1455 raise HTTPForbidden()
1456 raise HTTPForbidden()
1456
1457
1457 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1458 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1458 super_admin = h.HasPermissionAny('hg.admin')()
1459 super_admin = h.HasPermissionAny('hg.admin')()
1459 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1460 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1460 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1461 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1461 comment_repo_admin = is_repo_admin and is_repo_comment
1462 comment_repo_admin = is_repo_admin and is_repo_comment
1462
1463
1463 if super_admin or comment_owner or comment_repo_admin:
1464 if super_admin or comment_owner or comment_repo_admin:
1464 old_calculated_status = comment.pull_request.calculated_review_status()
1465 old_calculated_status = comment.pull_request.calculated_review_status()
1465 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1466 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1466 Session().commit()
1467 Session().commit()
1467 calculated_status = comment.pull_request.calculated_review_status()
1468 calculated_status = comment.pull_request.calculated_review_status()
1468 if old_calculated_status != calculated_status:
1469 if old_calculated_status != calculated_status:
1469 PullRequestModel().trigger_pull_request_hook(
1470 PullRequestModel().trigger_pull_request_hook(
1470 comment.pull_request, self._rhodecode_user, 'review_status_change',
1471 comment.pull_request, self._rhodecode_user, 'review_status_change',
1471 data={'status': calculated_status})
1472 data={'status': calculated_status})
1472 return True
1473 return True
1473 else:
1474 else:
1474 log.warning('No permissions for user %s to delete comment_id: %s',
1475 log.warning('No permissions for user %s to delete comment_id: %s',
1475 self._rhodecode_db_user, comment_id)
1476 self._rhodecode_db_user, comment_id)
1476 raise HTTPNotFound()
1477 raise HTTPNotFound()
@@ -1,1942 +1,1943 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27
27
28 import os
28 import os
29 import random
29 import random
30 import hashlib
30 import hashlib
31 import StringIO
31 import StringIO
32 import textwrap
32 import textwrap
33 import urllib
33 import urllib
34 import math
34 import math
35 import logging
35 import logging
36 import re
36 import re
37 import time
37 import time
38 import string
38 import string
39 import hashlib
39 import hashlib
40 from collections import OrderedDict
40 from collections import OrderedDict
41
41
42 import pygments
42 import pygments
43 import itertools
43 import itertools
44 import fnmatch
44 import fnmatch
45 import bleach
45 import bleach
46
46
47 from pyramid import compat
47 from pyramid import compat
48 from datetime import datetime
48 from datetime import datetime
49 from functools import partial
49 from functools import partial
50 from pygments.formatters.html import HtmlFormatter
50 from pygments.formatters.html import HtmlFormatter
51 from pygments.lexers import (
51 from pygments.lexers import (
52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
52 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53
53
54 from pyramid.threadlocal import get_current_request
54 from pyramid.threadlocal import get_current_request
55
55
56 from webhelpers2.html import literal, HTML, escape
56 from webhelpers2.html import literal, HTML, escape
57 from webhelpers2.html._autolink import _auto_link_urls
57 from webhelpers2.html._autolink import _auto_link_urls
58 from webhelpers2.html.tools import (
58 from webhelpers2.html.tools import (
59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
59 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60
60
61 from webhelpers2.text import (
61 from webhelpers2.text import (
62 chop_at, collapse, convert_accented_entities,
62 chop_at, collapse, convert_accented_entities,
63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
63 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 replace_whitespace, urlify, truncate, wrap_paragraphs)
64 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 from webhelpers2.date import time_ago_in_words
65 from webhelpers2.date import time_ago_in_words
66
66
67 from webhelpers2.html.tags import (
67 from webhelpers2.html.tags import (
68 _input, NotGiven, _make_safe_id_component as safeid,
68 _input, NotGiven, _make_safe_id_component as safeid,
69 form as insecure_form,
69 form as insecure_form,
70 auto_discovery_link, checkbox, end_form, file,
70 auto_discovery_link, checkbox, end_form, file,
71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
71 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 select as raw_select, stylesheet_link, submit, text, password, textarea,
72 select as raw_select, stylesheet_link, submit, text, password, textarea,
73 ul, radio, Options)
73 ul, radio, Options)
74
74
75 from webhelpers2.number import format_byte_size
75 from webhelpers2.number import format_byte_size
76
76
77 from rhodecode.lib.action_parser import action_parser
77 from rhodecode.lib.action_parser import action_parser
78 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
78 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
79 from rhodecode.lib.ext_json import json
79 from rhodecode.lib.ext_json import json
80 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
80 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
81 from rhodecode.lib.utils2 import (
81 from rhodecode.lib.utils2 import (
82 str2bool, safe_unicode, safe_str,
82 str2bool, safe_unicode, safe_str,
83 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
83 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
84 AttributeDict, safe_int, md5, md5_safe, get_host_info)
84 AttributeDict, safe_int, md5, md5_safe, get_host_info)
85 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
85 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
86 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
87 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
88 from rhodecode.lib.index.search_utils import get_matching_line_offsets
88 from rhodecode.lib.index.search_utils import get_matching_line_offsets
89 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
89 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
90 from rhodecode.model.changeset_status import ChangesetStatusModel
90 from rhodecode.model.changeset_status import ChangesetStatusModel
91 from rhodecode.model.db import Permission, User, Repository
91 from rhodecode.model.db import Permission, User, Repository
92 from rhodecode.model.repo_group import RepoGroupModel
92 from rhodecode.model.repo_group import RepoGroupModel
93 from rhodecode.model.settings import IssueTrackerSettingsModel
93 from rhodecode.model.settings import IssueTrackerSettingsModel
94
94
95
95
96 log = logging.getLogger(__name__)
96 log = logging.getLogger(__name__)
97
97
98
98
99 DEFAULT_USER = User.DEFAULT_USER
99 DEFAULT_USER = User.DEFAULT_USER
100 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
100 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
101
101
102
102
103 def asset(path, ver=None, **kwargs):
103 def asset(path, ver=None, **kwargs):
104 """
104 """
105 Helper to generate a static asset file path for rhodecode assets
105 Helper to generate a static asset file path for rhodecode assets
106
106
107 eg. h.asset('images/image.png', ver='3923')
107 eg. h.asset('images/image.png', ver='3923')
108
108
109 :param path: path of asset
109 :param path: path of asset
110 :param ver: optional version query param to append as ?ver=
110 :param ver: optional version query param to append as ?ver=
111 """
111 """
112 request = get_current_request()
112 request = get_current_request()
113 query = {}
113 query = {}
114 query.update(kwargs)
114 query.update(kwargs)
115 if ver:
115 if ver:
116 query = {'ver': ver}
116 query = {'ver': ver}
117 return request.static_path(
117 return request.static_path(
118 'rhodecode:public/{}'.format(path), _query=query)
118 'rhodecode:public/{}'.format(path), _query=query)
119
119
120
120
121 default_html_escape_table = {
121 default_html_escape_table = {
122 ord('&'): u'&amp;',
122 ord('&'): u'&amp;',
123 ord('<'): u'&lt;',
123 ord('<'): u'&lt;',
124 ord('>'): u'&gt;',
124 ord('>'): u'&gt;',
125 ord('"'): u'&quot;',
125 ord('"'): u'&quot;',
126 ord("'"): u'&#39;',
126 ord("'"): u'&#39;',
127 }
127 }
128
128
129
129
130 def html_escape(text, html_escape_table=default_html_escape_table):
130 def html_escape(text, html_escape_table=default_html_escape_table):
131 """Produce entities within text."""
131 """Produce entities within text."""
132 return text.translate(html_escape_table)
132 return text.translate(html_escape_table)
133
133
134
134
135 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
135 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
136 """
136 """
137 Truncate string ``s`` at the first occurrence of ``sub``.
137 Truncate string ``s`` at the first occurrence of ``sub``.
138
138
139 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
139 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
140 """
140 """
141 suffix_if_chopped = suffix_if_chopped or ''
141 suffix_if_chopped = suffix_if_chopped or ''
142 pos = s.find(sub)
142 pos = s.find(sub)
143 if pos == -1:
143 if pos == -1:
144 return s
144 return s
145
145
146 if inclusive:
146 if inclusive:
147 pos += len(sub)
147 pos += len(sub)
148
148
149 chopped = s[:pos]
149 chopped = s[:pos]
150 left = s[pos:].strip()
150 left = s[pos:].strip()
151
151
152 if left and suffix_if_chopped:
152 if left and suffix_if_chopped:
153 chopped += suffix_if_chopped
153 chopped += suffix_if_chopped
154
154
155 return chopped
155 return chopped
156
156
157
157
158 def shorter(text, size=20, prefix=False):
158 def shorter(text, size=20, prefix=False):
159 postfix = '...'
159 postfix = '...'
160 if len(text) > size:
160 if len(text) > size:
161 if prefix:
161 if prefix:
162 # shorten in front
162 # shorten in front
163 return postfix + text[-(size - len(postfix)):]
163 return postfix + text[-(size - len(postfix)):]
164 else:
164 else:
165 return text[:size - len(postfix)] + postfix
165 return text[:size - len(postfix)] + postfix
166 return text
166 return text
167
167
168
168
169 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
169 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
170 """
170 """
171 Reset button
171 Reset button
172 """
172 """
173 return _input(type, name, value, id, attrs)
173 return _input(type, name, value, id, attrs)
174
174
175
175
176 def select(name, selected_values, options, id=NotGiven, **attrs):
176 def select(name, selected_values, options, id=NotGiven, **attrs):
177
177
178 if isinstance(options, (list, tuple)):
178 if isinstance(options, (list, tuple)):
179 options_iter = options
179 options_iter = options
180 # Handle old value,label lists ... where value also can be value,label lists
180 # Handle old value,label lists ... where value also can be value,label lists
181 options = Options()
181 options = Options()
182 for opt in options_iter:
182 for opt in options_iter:
183 if isinstance(opt, tuple) and len(opt) == 2:
183 if isinstance(opt, tuple) and len(opt) == 2:
184 value, label = opt
184 value, label = opt
185 elif isinstance(opt, basestring):
185 elif isinstance(opt, basestring):
186 value = label = opt
186 value = label = opt
187 else:
187 else:
188 raise ValueError('invalid select option type %r' % type(opt))
188 raise ValueError('invalid select option type %r' % type(opt))
189
189
190 if isinstance(value, (list, tuple)):
190 if isinstance(value, (list, tuple)):
191 option_group = options.add_optgroup(label)
191 option_group = options.add_optgroup(label)
192 for opt2 in value:
192 for opt2 in value:
193 if isinstance(opt2, tuple) and len(opt2) == 2:
193 if isinstance(opt2, tuple) and len(opt2) == 2:
194 group_value, group_label = opt
194 group_value, group_label = opt
195 elif isinstance(opt2, basestring):
195 elif isinstance(opt2, basestring):
196 group_value = group_label = opt2
196 group_value = group_label = opt2
197 else:
197 else:
198 raise ValueError('invalid select option type %r' % type(opt2))
198 raise ValueError('invalid select option type %r' % type(opt2))
199
199
200 option_group.add_option(group_label, group_value)
200 option_group.add_option(group_label, group_value)
201 else:
201 else:
202 options.add_option(label, value)
202 options.add_option(label, value)
203
203
204 return raw_select(name, selected_values, options, id=id, **attrs)
204 return raw_select(name, selected_values, options, id=id, **attrs)
205
205
206
206
207 def branding(name, length=40):
207 def branding(name, length=40):
208 return truncate(name, length, indicator="")
208 return truncate(name, length, indicator="")
209
209
210
210
211 def FID(raw_id, path):
211 def FID(raw_id, path):
212 """
212 """
213 Creates a unique ID for filenode based on it's hash of path and commit
213 Creates a unique ID for filenode based on it's hash of path and commit
214 it's safe to use in urls
214 it's safe to use in urls
215
215
216 :param raw_id:
216 :param raw_id:
217 :param path:
217 :param path:
218 """
218 """
219
219
220 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
220 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
221
221
222
222
223 class _GetError(object):
223 class _GetError(object):
224 """Get error from form_errors, and represent it as span wrapped error
224 """Get error from form_errors, and represent it as span wrapped error
225 message
225 message
226
226
227 :param field_name: field to fetch errors for
227 :param field_name: field to fetch errors for
228 :param form_errors: form errors dict
228 :param form_errors: form errors dict
229 """
229 """
230
230
231 def __call__(self, field_name, form_errors):
231 def __call__(self, field_name, form_errors):
232 tmpl = """<span class="error_msg">%s</span>"""
232 tmpl = """<span class="error_msg">%s</span>"""
233 if form_errors and field_name in form_errors:
233 if form_errors and field_name in form_errors:
234 return literal(tmpl % form_errors.get(field_name))
234 return literal(tmpl % form_errors.get(field_name))
235
235
236
236
237 get_error = _GetError()
237 get_error = _GetError()
238
238
239
239
240 class _ToolTip(object):
240 class _ToolTip(object):
241
241
242 def __call__(self, tooltip_title, trim_at=50):
242 def __call__(self, tooltip_title, trim_at=50):
243 """
243 """
244 Special function just to wrap our text into nice formatted
244 Special function just to wrap our text into nice formatted
245 autowrapped text
245 autowrapped text
246
246
247 :param tooltip_title:
247 :param tooltip_title:
248 """
248 """
249 tooltip_title = escape(tooltip_title)
249 tooltip_title = escape(tooltip_title)
250 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
250 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
251 return tooltip_title
251 return tooltip_title
252
252
253
253
254 tooltip = _ToolTip()
254 tooltip = _ToolTip()
255
255
256 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
256 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy the full path"></i>'
257
257
258
258
259 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
259 def files_breadcrumbs(repo_name, commit_id, file_path, at_ref=None, limit_items=False, linkify_last_item=False):
260 if isinstance(file_path, str):
260 if isinstance(file_path, str):
261 file_path = safe_unicode(file_path)
261 file_path = safe_unicode(file_path)
262
262
263 route_qry = {'at': at_ref} if at_ref else None
263 route_qry = {'at': at_ref} if at_ref else None
264
264
265 # first segment is a `..` link to repo files
265 # first segment is a `..` link to repo files
266 root_name = literal(u'<i class="icon-home"></i>')
266 root_name = literal(u'<i class="icon-home"></i>')
267 url_segments = [
267 url_segments = [
268 link_to(
268 link_to(
269 root_name,
269 root_name,
270 route_path(
270 route_path(
271 'repo_files',
271 'repo_files',
272 repo_name=repo_name,
272 repo_name=repo_name,
273 commit_id=commit_id,
273 commit_id=commit_id,
274 f_path='',
274 f_path='',
275 _query=route_qry),
275 _query=route_qry),
276 )]
276 )]
277
277
278 path_segments = file_path.split('/')
278 path_segments = file_path.split('/')
279 last_cnt = len(path_segments) - 1
279 last_cnt = len(path_segments) - 1
280 for cnt, segment in enumerate(path_segments):
280 for cnt, segment in enumerate(path_segments):
281 if not segment:
281 if not segment:
282 continue
282 continue
283 segment_html = escape(segment)
283 segment_html = escape(segment)
284
284
285 last_item = cnt == last_cnt
285 last_item = cnt == last_cnt
286
286
287 if last_item and linkify_last_item is False:
287 if last_item and linkify_last_item is False:
288 # plain version
288 # plain version
289 url_segments.append(segment_html)
289 url_segments.append(segment_html)
290 else:
290 else:
291 url_segments.append(
291 url_segments.append(
292 link_to(
292 link_to(
293 segment_html,
293 segment_html,
294 route_path(
294 route_path(
295 'repo_files',
295 'repo_files',
296 repo_name=repo_name,
296 repo_name=repo_name,
297 commit_id=commit_id,
297 commit_id=commit_id,
298 f_path='/'.join(path_segments[:cnt + 1]),
298 f_path='/'.join(path_segments[:cnt + 1]),
299 _query=route_qry),
299 _query=route_qry),
300 ))
300 ))
301
301
302 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
302 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
303 if limit_items and len(limited_url_segments) < len(url_segments):
303 if limit_items and len(limited_url_segments) < len(url_segments):
304 url_segments = limited_url_segments
304 url_segments = limited_url_segments
305
305
306 full_path = file_path
306 full_path = file_path
307 icon = files_icon.format(escape(full_path))
307 icon = files_icon.format(escape(full_path))
308 if file_path == '':
308 if file_path == '':
309 return root_name
309 return root_name
310 else:
310 else:
311 return literal(' / '.join(url_segments) + icon)
311 return literal(' / '.join(url_segments) + icon)
312
312
313
313
314 def files_url_data(request):
314 def files_url_data(request):
315 matchdict = request.matchdict
315 matchdict = request.matchdict
316
316
317 if 'f_path' not in matchdict:
317 if 'f_path' not in matchdict:
318 matchdict['f_path'] = ''
318 matchdict['f_path'] = ''
319
319
320 if 'commit_id' not in matchdict:
320 if 'commit_id' not in matchdict:
321 matchdict['commit_id'] = 'tip'
321 matchdict['commit_id'] = 'tip'
322
322
323 return json.dumps(matchdict)
323 return json.dumps(matchdict)
324
324
325
325
326 def code_highlight(code, lexer, formatter, use_hl_filter=False):
326 def code_highlight(code, lexer, formatter, use_hl_filter=False):
327 """
327 """
328 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
328 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
329
329
330 If ``outfile`` is given and a valid file object (an object
330 If ``outfile`` is given and a valid file object (an object
331 with a ``write`` method), the result will be written to it, otherwise
331 with a ``write`` method), the result will be written to it, otherwise
332 it is returned as a string.
332 it is returned as a string.
333 """
333 """
334 if use_hl_filter:
334 if use_hl_filter:
335 # add HL filter
335 # add HL filter
336 from rhodecode.lib.index import search_utils
336 from rhodecode.lib.index import search_utils
337 lexer.add_filter(search_utils.ElasticSearchHLFilter())
337 lexer.add_filter(search_utils.ElasticSearchHLFilter())
338 return pygments.format(pygments.lex(code, lexer), formatter)
338 return pygments.format(pygments.lex(code, lexer), formatter)
339
339
340
340
341 class CodeHtmlFormatter(HtmlFormatter):
341 class CodeHtmlFormatter(HtmlFormatter):
342 """
342 """
343 My code Html Formatter for source codes
343 My code Html Formatter for source codes
344 """
344 """
345
345
346 def wrap(self, source, outfile):
346 def wrap(self, source, outfile):
347 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
347 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
348
348
349 def _wrap_code(self, source):
349 def _wrap_code(self, source):
350 for cnt, it in enumerate(source):
350 for cnt, it in enumerate(source):
351 i, t = it
351 i, t = it
352 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
352 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
353 yield i, t
353 yield i, t
354
354
355 def _wrap_tablelinenos(self, inner):
355 def _wrap_tablelinenos(self, inner):
356 dummyoutfile = StringIO.StringIO()
356 dummyoutfile = StringIO.StringIO()
357 lncount = 0
357 lncount = 0
358 for t, line in inner:
358 for t, line in inner:
359 if t:
359 if t:
360 lncount += 1
360 lncount += 1
361 dummyoutfile.write(line)
361 dummyoutfile.write(line)
362
362
363 fl = self.linenostart
363 fl = self.linenostart
364 mw = len(str(lncount + fl - 1))
364 mw = len(str(lncount + fl - 1))
365 sp = self.linenospecial
365 sp = self.linenospecial
366 st = self.linenostep
366 st = self.linenostep
367 la = self.lineanchors
367 la = self.lineanchors
368 aln = self.anchorlinenos
368 aln = self.anchorlinenos
369 nocls = self.noclasses
369 nocls = self.noclasses
370 if sp:
370 if sp:
371 lines = []
371 lines = []
372
372
373 for i in range(fl, fl + lncount):
373 for i in range(fl, fl + lncount):
374 if i % st == 0:
374 if i % st == 0:
375 if i % sp == 0:
375 if i % sp == 0:
376 if aln:
376 if aln:
377 lines.append('<a href="#%s%d" class="special">%*d</a>' %
377 lines.append('<a href="#%s%d" class="special">%*d</a>' %
378 (la, i, mw, i))
378 (la, i, mw, i))
379 else:
379 else:
380 lines.append('<span class="special">%*d</span>' % (mw, i))
380 lines.append('<span class="special">%*d</span>' % (mw, i))
381 else:
381 else:
382 if aln:
382 if aln:
383 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
383 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
384 else:
384 else:
385 lines.append('%*d' % (mw, i))
385 lines.append('%*d' % (mw, i))
386 else:
386 else:
387 lines.append('')
387 lines.append('')
388 ls = '\n'.join(lines)
388 ls = '\n'.join(lines)
389 else:
389 else:
390 lines = []
390 lines = []
391 for i in range(fl, fl + lncount):
391 for i in range(fl, fl + lncount):
392 if i % st == 0:
392 if i % st == 0:
393 if aln:
393 if aln:
394 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
394 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
395 else:
395 else:
396 lines.append('%*d' % (mw, i))
396 lines.append('%*d' % (mw, i))
397 else:
397 else:
398 lines.append('')
398 lines.append('')
399 ls = '\n'.join(lines)
399 ls = '\n'.join(lines)
400
400
401 # in case you wonder about the seemingly redundant <div> here: since the
401 # in case you wonder about the seemingly redundant <div> here: since the
402 # content in the other cell also is wrapped in a div, some browsers in
402 # content in the other cell also is wrapped in a div, some browsers in
403 # some configurations seem to mess up the formatting...
403 # some configurations seem to mess up the formatting...
404 if nocls:
404 if nocls:
405 yield 0, ('<table class="%stable">' % self.cssclass +
405 yield 0, ('<table class="%stable">' % self.cssclass +
406 '<tr><td><div class="linenodiv" '
406 '<tr><td><div class="linenodiv" '
407 'style="background-color: #f0f0f0; padding-right: 10px">'
407 'style="background-color: #f0f0f0; padding-right: 10px">'
408 '<pre style="line-height: 125%">' +
408 '<pre style="line-height: 125%">' +
409 ls + '</pre></div></td><td id="hlcode" class="code">')
409 ls + '</pre></div></td><td id="hlcode" class="code">')
410 else:
410 else:
411 yield 0, ('<table class="%stable">' % self.cssclass +
411 yield 0, ('<table class="%stable">' % self.cssclass +
412 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
412 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
413 ls + '</pre></div></td><td id="hlcode" class="code">')
413 ls + '</pre></div></td><td id="hlcode" class="code">')
414 yield 0, dummyoutfile.getvalue()
414 yield 0, dummyoutfile.getvalue()
415 yield 0, '</td></tr></table>'
415 yield 0, '</td></tr></table>'
416
416
417
417
418 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
418 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
419 def __init__(self, **kw):
419 def __init__(self, **kw):
420 # only show these line numbers if set
420 # only show these line numbers if set
421 self.only_lines = kw.pop('only_line_numbers', [])
421 self.only_lines = kw.pop('only_line_numbers', [])
422 self.query_terms = kw.pop('query_terms', [])
422 self.query_terms = kw.pop('query_terms', [])
423 self.max_lines = kw.pop('max_lines', 5)
423 self.max_lines = kw.pop('max_lines', 5)
424 self.line_context = kw.pop('line_context', 3)
424 self.line_context = kw.pop('line_context', 3)
425 self.url = kw.pop('url', None)
425 self.url = kw.pop('url', None)
426
426
427 super(CodeHtmlFormatter, self).__init__(**kw)
427 super(CodeHtmlFormatter, self).__init__(**kw)
428
428
429 def _wrap_code(self, source):
429 def _wrap_code(self, source):
430 for cnt, it in enumerate(source):
430 for cnt, it in enumerate(source):
431 i, t = it
431 i, t = it
432 t = '<pre>%s</pre>' % t
432 t = '<pre>%s</pre>' % t
433 yield i, t
433 yield i, t
434
434
435 def _wrap_tablelinenos(self, inner):
435 def _wrap_tablelinenos(self, inner):
436 yield 0, '<table class="code-highlight %stable">' % self.cssclass
436 yield 0, '<table class="code-highlight %stable">' % self.cssclass
437
437
438 last_shown_line_number = 0
438 last_shown_line_number = 0
439 current_line_number = 1
439 current_line_number = 1
440
440
441 for t, line in inner:
441 for t, line in inner:
442 if not t:
442 if not t:
443 yield t, line
443 yield t, line
444 continue
444 continue
445
445
446 if current_line_number in self.only_lines:
446 if current_line_number in self.only_lines:
447 if last_shown_line_number + 1 != current_line_number:
447 if last_shown_line_number + 1 != current_line_number:
448 yield 0, '<tr>'
448 yield 0, '<tr>'
449 yield 0, '<td class="line">...</td>'
449 yield 0, '<td class="line">...</td>'
450 yield 0, '<td id="hlcode" class="code"></td>'
450 yield 0, '<td id="hlcode" class="code"></td>'
451 yield 0, '</tr>'
451 yield 0, '</tr>'
452
452
453 yield 0, '<tr>'
453 yield 0, '<tr>'
454 if self.url:
454 if self.url:
455 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
455 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
456 self.url, current_line_number, current_line_number)
456 self.url, current_line_number, current_line_number)
457 else:
457 else:
458 yield 0, '<td class="line"><a href="">%i</a></td>' % (
458 yield 0, '<td class="line"><a href="">%i</a></td>' % (
459 current_line_number)
459 current_line_number)
460 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
460 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
461 yield 0, '</tr>'
461 yield 0, '</tr>'
462
462
463 last_shown_line_number = current_line_number
463 last_shown_line_number = current_line_number
464
464
465 current_line_number += 1
465 current_line_number += 1
466
466
467 yield 0, '</table>'
467 yield 0, '</table>'
468
468
469
469
470 def hsv_to_rgb(h, s, v):
470 def hsv_to_rgb(h, s, v):
471 """ Convert hsv color values to rgb """
471 """ Convert hsv color values to rgb """
472
472
473 if s == 0.0:
473 if s == 0.0:
474 return v, v, v
474 return v, v, v
475 i = int(h * 6.0) # XXX assume int() truncates!
475 i = int(h * 6.0) # XXX assume int() truncates!
476 f = (h * 6.0) - i
476 f = (h * 6.0) - i
477 p = v * (1.0 - s)
477 p = v * (1.0 - s)
478 q = v * (1.0 - s * f)
478 q = v * (1.0 - s * f)
479 t = v * (1.0 - s * (1.0 - f))
479 t = v * (1.0 - s * (1.0 - f))
480 i = i % 6
480 i = i % 6
481 if i == 0:
481 if i == 0:
482 return v, t, p
482 return v, t, p
483 if i == 1:
483 if i == 1:
484 return q, v, p
484 return q, v, p
485 if i == 2:
485 if i == 2:
486 return p, v, t
486 return p, v, t
487 if i == 3:
487 if i == 3:
488 return p, q, v
488 return p, q, v
489 if i == 4:
489 if i == 4:
490 return t, p, v
490 return t, p, v
491 if i == 5:
491 if i == 5:
492 return v, p, q
492 return v, p, q
493
493
494
494
495 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
495 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
496 """
496 """
497 Generator for getting n of evenly distributed colors using
497 Generator for getting n of evenly distributed colors using
498 hsv color and golden ratio. It always return same order of colors
498 hsv color and golden ratio. It always return same order of colors
499
499
500 :param n: number of colors to generate
500 :param n: number of colors to generate
501 :param saturation: saturation of returned colors
501 :param saturation: saturation of returned colors
502 :param lightness: lightness of returned colors
502 :param lightness: lightness of returned colors
503 :returns: RGB tuple
503 :returns: RGB tuple
504 """
504 """
505
505
506 golden_ratio = 0.618033988749895
506 golden_ratio = 0.618033988749895
507 h = 0.22717784590367374
507 h = 0.22717784590367374
508
508
509 for _ in xrange(n):
509 for _ in xrange(n):
510 h += golden_ratio
510 h += golden_ratio
511 h %= 1
511 h %= 1
512 HSV_tuple = [h, saturation, lightness]
512 HSV_tuple = [h, saturation, lightness]
513 RGB_tuple = hsv_to_rgb(*HSV_tuple)
513 RGB_tuple = hsv_to_rgb(*HSV_tuple)
514 yield map(lambda x: str(int(x * 256)), RGB_tuple)
514 yield map(lambda x: str(int(x * 256)), RGB_tuple)
515
515
516
516
517 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
517 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
518 """
518 """
519 Returns a function which when called with an argument returns a unique
519 Returns a function which when called with an argument returns a unique
520 color for that argument, eg.
520 color for that argument, eg.
521
521
522 :param n: number of colors to generate
522 :param n: number of colors to generate
523 :param saturation: saturation of returned colors
523 :param saturation: saturation of returned colors
524 :param lightness: lightness of returned colors
524 :param lightness: lightness of returned colors
525 :returns: css RGB string
525 :returns: css RGB string
526
526
527 >>> color_hash = color_hasher()
527 >>> color_hash = color_hasher()
528 >>> color_hash('hello')
528 >>> color_hash('hello')
529 'rgb(34, 12, 59)'
529 'rgb(34, 12, 59)'
530 >>> color_hash('hello')
530 >>> color_hash('hello')
531 'rgb(34, 12, 59)'
531 'rgb(34, 12, 59)'
532 >>> color_hash('other')
532 >>> color_hash('other')
533 'rgb(90, 224, 159)'
533 'rgb(90, 224, 159)'
534 """
534 """
535
535
536 color_dict = {}
536 color_dict = {}
537 cgenerator = unique_color_generator(
537 cgenerator = unique_color_generator(
538 saturation=saturation, lightness=lightness)
538 saturation=saturation, lightness=lightness)
539
539
540 def get_color_string(thing):
540 def get_color_string(thing):
541 if thing in color_dict:
541 if thing in color_dict:
542 col = color_dict[thing]
542 col = color_dict[thing]
543 else:
543 else:
544 col = color_dict[thing] = cgenerator.next()
544 col = color_dict[thing] = cgenerator.next()
545 return "rgb(%s)" % (', '.join(col))
545 return "rgb(%s)" % (', '.join(col))
546
546
547 return get_color_string
547 return get_color_string
548
548
549
549
550 def get_lexer_safe(mimetype=None, filepath=None):
550 def get_lexer_safe(mimetype=None, filepath=None):
551 """
551 """
552 Tries to return a relevant pygments lexer using mimetype/filepath name,
552 Tries to return a relevant pygments lexer using mimetype/filepath name,
553 defaulting to plain text if none could be found
553 defaulting to plain text if none could be found
554 """
554 """
555 lexer = None
555 lexer = None
556 try:
556 try:
557 if mimetype:
557 if mimetype:
558 lexer = get_lexer_for_mimetype(mimetype)
558 lexer = get_lexer_for_mimetype(mimetype)
559 if not lexer:
559 if not lexer:
560 lexer = get_lexer_for_filename(filepath)
560 lexer = get_lexer_for_filename(filepath)
561 except pygments.util.ClassNotFound:
561 except pygments.util.ClassNotFound:
562 pass
562 pass
563
563
564 if not lexer:
564 if not lexer:
565 lexer = get_lexer_by_name('text')
565 lexer = get_lexer_by_name('text')
566
566
567 return lexer
567 return lexer
568
568
569
569
570 def get_lexer_for_filenode(filenode):
570 def get_lexer_for_filenode(filenode):
571 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
571 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
572 return lexer
572 return lexer
573
573
574
574
575 def pygmentize(filenode, **kwargs):
575 def pygmentize(filenode, **kwargs):
576 """
576 """
577 pygmentize function using pygments
577 pygmentize function using pygments
578
578
579 :param filenode:
579 :param filenode:
580 """
580 """
581 lexer = get_lexer_for_filenode(filenode)
581 lexer = get_lexer_for_filenode(filenode)
582 return literal(code_highlight(filenode.content, lexer,
582 return literal(code_highlight(filenode.content, lexer,
583 CodeHtmlFormatter(**kwargs)))
583 CodeHtmlFormatter(**kwargs)))
584
584
585
585
586 def is_following_repo(repo_name, user_id):
586 def is_following_repo(repo_name, user_id):
587 from rhodecode.model.scm import ScmModel
587 from rhodecode.model.scm import ScmModel
588 return ScmModel().is_following_repo(repo_name, user_id)
588 return ScmModel().is_following_repo(repo_name, user_id)
589
589
590
590
591 class _Message(object):
591 class _Message(object):
592 """A message returned by ``Flash.pop_messages()``.
592 """A message returned by ``Flash.pop_messages()``.
593
593
594 Converting the message to a string returns the message text. Instances
594 Converting the message to a string returns the message text. Instances
595 also have the following attributes:
595 also have the following attributes:
596
596
597 * ``message``: the message text.
597 * ``message``: the message text.
598 * ``category``: the category specified when the message was created.
598 * ``category``: the category specified when the message was created.
599 """
599 """
600
600
601 def __init__(self, category, message):
601 def __init__(self, category, message):
602 self.category = category
602 self.category = category
603 self.message = message
603 self.message = message
604
604
605 def __str__(self):
605 def __str__(self):
606 return self.message
606 return self.message
607
607
608 __unicode__ = __str__
608 __unicode__ = __str__
609
609
610 def __html__(self):
610 def __html__(self):
611 return escape(safe_unicode(self.message))
611 return escape(safe_unicode(self.message))
612
612
613
613
614 class Flash(object):
614 class Flash(object):
615 # List of allowed categories. If None, allow any category.
615 # List of allowed categories. If None, allow any category.
616 categories = ["warning", "notice", "error", "success"]
616 categories = ["warning", "notice", "error", "success"]
617
617
618 # Default category if none is specified.
618 # Default category if none is specified.
619 default_category = "notice"
619 default_category = "notice"
620
620
621 def __init__(self, session_key="flash", categories=None,
621 def __init__(self, session_key="flash", categories=None,
622 default_category=None):
622 default_category=None):
623 """
623 """
624 Instantiate a ``Flash`` object.
624 Instantiate a ``Flash`` object.
625
625
626 ``session_key`` is the key to save the messages under in the user's
626 ``session_key`` is the key to save the messages under in the user's
627 session.
627 session.
628
628
629 ``categories`` is an optional list which overrides the default list
629 ``categories`` is an optional list which overrides the default list
630 of categories.
630 of categories.
631
631
632 ``default_category`` overrides the default category used for messages
632 ``default_category`` overrides the default category used for messages
633 when none is specified.
633 when none is specified.
634 """
634 """
635 self.session_key = session_key
635 self.session_key = session_key
636 if categories is not None:
636 if categories is not None:
637 self.categories = categories
637 self.categories = categories
638 if default_category is not None:
638 if default_category is not None:
639 self.default_category = default_category
639 self.default_category = default_category
640 if self.categories and self.default_category not in self.categories:
640 if self.categories and self.default_category not in self.categories:
641 raise ValueError(
641 raise ValueError(
642 "unrecognized default category %r" % (self.default_category,))
642 "unrecognized default category %r" % (self.default_category,))
643
643
644 def pop_messages(self, session=None, request=None):
644 def pop_messages(self, session=None, request=None):
645 """
645 """
646 Return all accumulated messages and delete them from the session.
646 Return all accumulated messages and delete them from the session.
647
647
648 The return value is a list of ``Message`` objects.
648 The return value is a list of ``Message`` objects.
649 """
649 """
650 messages = []
650 messages = []
651
651
652 if not session:
652 if not session:
653 if not request:
653 if not request:
654 request = get_current_request()
654 request = get_current_request()
655 session = request.session
655 session = request.session
656
656
657 # Pop the 'old' pylons flash messages. They are tuples of the form
657 # Pop the 'old' pylons flash messages. They are tuples of the form
658 # (category, message)
658 # (category, message)
659 for cat, msg in session.pop(self.session_key, []):
659 for cat, msg in session.pop(self.session_key, []):
660 messages.append(_Message(cat, msg))
660 messages.append(_Message(cat, msg))
661
661
662 # Pop the 'new' pyramid flash messages for each category as list
662 # Pop the 'new' pyramid flash messages for each category as list
663 # of strings.
663 # of strings.
664 for cat in self.categories:
664 for cat in self.categories:
665 for msg in session.pop_flash(queue=cat):
665 for msg in session.pop_flash(queue=cat):
666 messages.append(_Message(cat, msg))
666 messages.append(_Message(cat, msg))
667 # Map messages from the default queue to the 'notice' category.
667 # Map messages from the default queue to the 'notice' category.
668 for msg in session.pop_flash():
668 for msg in session.pop_flash():
669 messages.append(_Message('notice', msg))
669 messages.append(_Message('notice', msg))
670
670
671 session.save()
671 session.save()
672 return messages
672 return messages
673
673
674 def json_alerts(self, session=None, request=None):
674 def json_alerts(self, session=None, request=None):
675 payloads = []
675 payloads = []
676 messages = flash.pop_messages(session=session, request=request)
676 messages = flash.pop_messages(session=session, request=request)
677 if messages:
677 if messages:
678 for message in messages:
678 for message in messages:
679 subdata = {}
679 subdata = {}
680 if hasattr(message.message, 'rsplit'):
680 if hasattr(message.message, 'rsplit'):
681 flash_data = message.message.rsplit('|DELIM|', 1)
681 flash_data = message.message.rsplit('|DELIM|', 1)
682 org_message = flash_data[0]
682 org_message = flash_data[0]
683 if len(flash_data) > 1:
683 if len(flash_data) > 1:
684 subdata = json.loads(flash_data[1])
684 subdata = json.loads(flash_data[1])
685 else:
685 else:
686 org_message = message.message
686 org_message = message.message
687 payloads.append({
687 payloads.append({
688 'message': {
688 'message': {
689 'message': u'{}'.format(org_message),
689 'message': u'{}'.format(org_message),
690 'level': message.category,
690 'level': message.category,
691 'force': True,
691 'force': True,
692 'subdata': subdata
692 'subdata': subdata
693 }
693 }
694 })
694 })
695 return json.dumps(payloads)
695 return json.dumps(payloads)
696
696
697 def __call__(self, message, category=None, ignore_duplicate=True,
697 def __call__(self, message, category=None, ignore_duplicate=True,
698 session=None, request=None):
698 session=None, request=None):
699
699
700 if not session:
700 if not session:
701 if not request:
701 if not request:
702 request = get_current_request()
702 request = get_current_request()
703 session = request.session
703 session = request.session
704
704
705 session.flash(
705 session.flash(
706 message, queue=category, allow_duplicate=not ignore_duplicate)
706 message, queue=category, allow_duplicate=not ignore_duplicate)
707
707
708
708
709 flash = Flash()
709 flash = Flash()
710
710
711 #==============================================================================
711 #==============================================================================
712 # SCM FILTERS available via h.
712 # SCM FILTERS available via h.
713 #==============================================================================
713 #==============================================================================
714 from rhodecode.lib.vcs.utils import author_name, author_email
714 from rhodecode.lib.vcs.utils import author_name, author_email
715 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
715 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
716 from rhodecode.model.db import User, ChangesetStatus
716 from rhodecode.model.db import User, ChangesetStatus
717
717
718 capitalize = lambda x: x.capitalize()
718 capitalize = lambda x: x.capitalize()
719 email = author_email
719 email = author_email
720 short_id = lambda x: x[:12]
720 short_id = lambda x: x[:12]
721 hide_credentials = lambda x: ''.join(credentials_filter(x))
721 hide_credentials = lambda x: ''.join(credentials_filter(x))
722
722
723
723
724 import pytz
724 import pytz
725 import tzlocal
725 import tzlocal
726 local_timezone = tzlocal.get_localzone()
726 local_timezone = tzlocal.get_localzone()
727
727
728
728
729 def age_component(datetime_iso, value=None, time_is_local=False):
729 def age_component(datetime_iso, value=None, time_is_local=False):
730 title = value or format_date(datetime_iso)
730 title = value or format_date(datetime_iso)
731 tzinfo = '+00:00'
731 tzinfo = '+00:00'
732
732
733 # detect if we have a timezone info, otherwise, add it
733 # detect if we have a timezone info, otherwise, add it
734 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
734 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
735 force_timezone = os.environ.get('RC_TIMEZONE', '')
735 force_timezone = os.environ.get('RC_TIMEZONE', '')
736 if force_timezone:
736 if force_timezone:
737 force_timezone = pytz.timezone(force_timezone)
737 force_timezone = pytz.timezone(force_timezone)
738 timezone = force_timezone or local_timezone
738 timezone = force_timezone or local_timezone
739 offset = timezone.localize(datetime_iso).strftime('%z')
739 offset = timezone.localize(datetime_iso).strftime('%z')
740 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
740 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
741
741
742 return literal(
742 return literal(
743 '<time class="timeago tooltip" '
743 '<time class="timeago tooltip" '
744 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
744 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
745 datetime_iso, title, tzinfo))
745 datetime_iso, title, tzinfo))
746
746
747
747
748 def _shorten_commit_id(commit_id, commit_len=None):
748 def _shorten_commit_id(commit_id, commit_len=None):
749 if commit_len is None:
749 if commit_len is None:
750 request = get_current_request()
750 request = get_current_request()
751 commit_len = request.call_context.visual.show_sha_length
751 commit_len = request.call_context.visual.show_sha_length
752 return commit_id[:commit_len]
752 return commit_id[:commit_len]
753
753
754
754
755 def show_id(commit, show_idx=None, commit_len=None):
755 def show_id(commit, show_idx=None, commit_len=None):
756 """
756 """
757 Configurable function that shows ID
757 Configurable function that shows ID
758 by default it's r123:fffeeefffeee
758 by default it's r123:fffeeefffeee
759
759
760 :param commit: commit instance
760 :param commit: commit instance
761 """
761 """
762 if show_idx is None:
762 if show_idx is None:
763 request = get_current_request()
763 request = get_current_request()
764 show_idx = request.call_context.visual.show_revision_number
764 show_idx = request.call_context.visual.show_revision_number
765
765
766 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
766 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
767 if show_idx:
767 if show_idx:
768 return 'r%s:%s' % (commit.idx, raw_id)
768 return 'r%s:%s' % (commit.idx, raw_id)
769 else:
769 else:
770 return '%s' % (raw_id, )
770 return '%s' % (raw_id, )
771
771
772
772
773 def format_date(date):
773 def format_date(date):
774 """
774 """
775 use a standardized formatting for dates used in RhodeCode
775 use a standardized formatting for dates used in RhodeCode
776
776
777 :param date: date/datetime object
777 :param date: date/datetime object
778 :return: formatted date
778 :return: formatted date
779 """
779 """
780
780
781 if date:
781 if date:
782 _fmt = "%a, %d %b %Y %H:%M:%S"
782 _fmt = "%a, %d %b %Y %H:%M:%S"
783 return safe_unicode(date.strftime(_fmt))
783 return safe_unicode(date.strftime(_fmt))
784
784
785 return u""
785 return u""
786
786
787
787
788 class _RepoChecker(object):
788 class _RepoChecker(object):
789
789
790 def __init__(self, backend_alias):
790 def __init__(self, backend_alias):
791 self._backend_alias = backend_alias
791 self._backend_alias = backend_alias
792
792
793 def __call__(self, repository):
793 def __call__(self, repository):
794 if hasattr(repository, 'alias'):
794 if hasattr(repository, 'alias'):
795 _type = repository.alias
795 _type = repository.alias
796 elif hasattr(repository, 'repo_type'):
796 elif hasattr(repository, 'repo_type'):
797 _type = repository.repo_type
797 _type = repository.repo_type
798 else:
798 else:
799 _type = repository
799 _type = repository
800 return _type == self._backend_alias
800 return _type == self._backend_alias
801
801
802
802
803 is_git = _RepoChecker('git')
803 is_git = _RepoChecker('git')
804 is_hg = _RepoChecker('hg')
804 is_hg = _RepoChecker('hg')
805 is_svn = _RepoChecker('svn')
805 is_svn = _RepoChecker('svn')
806
806
807
807
808 def get_repo_type_by_name(repo_name):
808 def get_repo_type_by_name(repo_name):
809 repo = Repository.get_by_repo_name(repo_name)
809 repo = Repository.get_by_repo_name(repo_name)
810 if repo:
810 if repo:
811 return repo.repo_type
811 return repo.repo_type
812
812
813
813
814 def is_svn_without_proxy(repository):
814 def is_svn_without_proxy(repository):
815 if is_svn(repository):
815 if is_svn(repository):
816 from rhodecode.model.settings import VcsSettingsModel
816 from rhodecode.model.settings import VcsSettingsModel
817 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
817 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
818 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
818 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
819 return False
819 return False
820
820
821
821
822 def discover_user(author):
822 def discover_user(author):
823 """
823 """
824 Tries to discover RhodeCode User based on the autho string. Author string
824 Tries to discover RhodeCode User based on the author string. Author string
825 is typically `FirstName LastName <email@address.com>`
825 is typically `FirstName LastName <email@address.com>`
826 """
826 """
827
827
828 # if author is already an instance use it for extraction
828 # if author is already an instance use it for extraction
829 if isinstance(author, User):
829 if isinstance(author, User):
830 return author
830 return author
831
831
832 # Valid email in the attribute passed, see if they're in the system
832 # Valid email in the attribute passed, see if they're in the system
833 _email = author_email(author)
833 _email = author_email(author)
834 if _email != '':
834 if _email != '':
835 user = User.get_by_email(_email, case_insensitive=True, cache=True)
835 user = User.get_by_email(_email, case_insensitive=True, cache=True)
836 if user is not None:
836 if user is not None:
837 return user
837 return user
838
838
839 # Maybe it's a username, we try to extract it and fetch by username ?
839 # Maybe it's a username, we try to extract it and fetch by username ?
840 _author = author_name(author)
840 _author = author_name(author)
841 user = User.get_by_username(_author, case_insensitive=True, cache=True)
841 user = User.get_by_username(_author, case_insensitive=True, cache=True)
842 if user is not None:
842 if user is not None:
843 return user
843 return user
844
844
845 return None
845 return None
846
846
847
847
848 def email_or_none(author):
848 def email_or_none(author):
849 # extract email from the commit string
849 # extract email from the commit string
850 _email = author_email(author)
850 _email = author_email(author)
851
851
852 # If we have an email, use it, otherwise
852 # If we have an email, use it, otherwise
853 # see if it contains a username we can get an email from
853 # see if it contains a username we can get an email from
854 if _email != '':
854 if _email != '':
855 return _email
855 return _email
856 else:
856 else:
857 user = User.get_by_username(
857 user = User.get_by_username(
858 author_name(author), case_insensitive=True, cache=True)
858 author_name(author), case_insensitive=True, cache=True)
859
859
860 if user is not None:
860 if user is not None:
861 return user.email
861 return user.email
862
862
863 # No valid email, not a valid user in the system, none!
863 # No valid email, not a valid user in the system, none!
864 return None
864 return None
865
865
866
866
867 def link_to_user(author, length=0, **kwargs):
867 def link_to_user(author, length=0, **kwargs):
868 user = discover_user(author)
868 user = discover_user(author)
869 # user can be None, but if we have it already it means we can re-use it
869 # user can be None, but if we have it already it means we can re-use it
870 # in the person() function, so we save 1 intensive-query
870 # in the person() function, so we save 1 intensive-query
871 if user:
871 if user:
872 author = user
872 author = user
873
873
874 display_person = person(author, 'username_or_name_or_email')
874 display_person = person(author, 'username_or_name_or_email')
875 if length:
875 if length:
876 display_person = shorter(display_person, length)
876 display_person = shorter(display_person, length)
877
877
878 if user:
878 if user:
879 return link_to(
879 return link_to(
880 escape(display_person),
880 escape(display_person),
881 route_path('user_profile', username=user.username),
881 route_path('user_profile', username=user.username),
882 **kwargs)
882 **kwargs)
883 else:
883 else:
884 return escape(display_person)
884 return escape(display_person)
885
885
886
886
887 def link_to_group(users_group_name, **kwargs):
887 def link_to_group(users_group_name, **kwargs):
888 return link_to(
888 return link_to(
889 escape(users_group_name),
889 escape(users_group_name),
890 route_path('user_group_profile', user_group_name=users_group_name),
890 route_path('user_group_profile', user_group_name=users_group_name),
891 **kwargs)
891 **kwargs)
892
892
893
893
894 def person(author, show_attr="username_and_name"):
894 def person(author, show_attr="username_and_name"):
895 user = discover_user(author)
895 user = discover_user(author)
896 if user:
896 if user:
897 return getattr(user, show_attr)
897 return getattr(user, show_attr)
898 else:
898 else:
899 _author = author_name(author)
899 _author = author_name(author)
900 _email = email(author)
900 _email = email(author)
901 return _author or _email
901 return _author or _email
902
902
903
903
904 def author_string(email):
904 def author_string(email):
905 if email:
905 if email:
906 user = User.get_by_email(email, case_insensitive=True, cache=True)
906 user = User.get_by_email(email, case_insensitive=True, cache=True)
907 if user:
907 if user:
908 if user.first_name or user.last_name:
908 if user.first_name or user.last_name:
909 return '%s %s &lt;%s&gt;' % (
909 return '%s %s &lt;%s&gt;' % (
910 user.first_name, user.last_name, email)
910 user.first_name, user.last_name, email)
911 else:
911 else:
912 return email
912 return email
913 else:
913 else:
914 return email
914 return email
915 else:
915 else:
916 return None
916 return None
917
917
918
918
919 def person_by_id(id_, show_attr="username_and_name"):
919 def person_by_id(id_, show_attr="username_and_name"):
920 # attr to return from fetched user
920 # attr to return from fetched user
921 person_getter = lambda usr: getattr(usr, show_attr)
921 person_getter = lambda usr: getattr(usr, show_attr)
922
922
923 #maybe it's an ID ?
923 #maybe it's an ID ?
924 if str(id_).isdigit() or isinstance(id_, int):
924 if str(id_).isdigit() or isinstance(id_, int):
925 id_ = int(id_)
925 id_ = int(id_)
926 user = User.get(id_)
926 user = User.get(id_)
927 if user is not None:
927 if user is not None:
928 return person_getter(user)
928 return person_getter(user)
929 return id_
929 return id_
930
930
931
931
932 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
932 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
933 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
933 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
934 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
934 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
935
935
936
936
937 tags_paterns = OrderedDict((
937 tags_paterns = OrderedDict((
938 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
938 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
939 '<div class="metatag" tag="lang">\\2</div>')),
939 '<div class="metatag" tag="lang">\\2</div>')),
940
940
941 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
941 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
942 '<div class="metatag" tag="see">see: \\1 </div>')),
942 '<div class="metatag" tag="see">see: \\1 </div>')),
943
943
944 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
944 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
945 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
945 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
946
946
947 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
947 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
948 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
948 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
949
949
950 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
950 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
951 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
951 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
952
952
953 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
953 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
954 '<div class="metatag" tag="state \\1">\\1</div>')),
954 '<div class="metatag" tag="state \\1">\\1</div>')),
955
955
956 # label in grey
956 # label in grey
957 ('label', (re.compile(r'\[([a-z]+)\]'),
957 ('label', (re.compile(r'\[([a-z]+)\]'),
958 '<div class="metatag" tag="label">\\1</div>')),
958 '<div class="metatag" tag="label">\\1</div>')),
959
959
960 # generic catch all in grey
960 # generic catch all in grey
961 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
961 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
962 '<div class="metatag" tag="generic">\\1</div>')),
962 '<div class="metatag" tag="generic">\\1</div>')),
963 ))
963 ))
964
964
965
965
966 def extract_metatags(value):
966 def extract_metatags(value):
967 """
967 """
968 Extract supported meta-tags from given text value
968 Extract supported meta-tags from given text value
969 """
969 """
970 tags = []
970 tags = []
971 if not value:
971 if not value:
972 return tags, ''
972 return tags, ''
973
973
974 for key, val in tags_paterns.items():
974 for key, val in tags_paterns.items():
975 pat, replace_html = val
975 pat, replace_html = val
976 tags.extend([(key, x.group()) for x in pat.finditer(value)])
976 tags.extend([(key, x.group()) for x in pat.finditer(value)])
977 value = pat.sub('', value)
977 value = pat.sub('', value)
978
978
979 return tags, value
979 return tags, value
980
980
981
981
982 def style_metatag(tag_type, value):
982 def style_metatag(tag_type, value):
983 """
983 """
984 converts tags from value into html equivalent
984 converts tags from value into html equivalent
985 """
985 """
986 if not value:
986 if not value:
987 return ''
987 return ''
988
988
989 html_value = value
989 html_value = value
990 tag_data = tags_paterns.get(tag_type)
990 tag_data = tags_paterns.get(tag_type)
991 if tag_data:
991 if tag_data:
992 pat, replace_html = tag_data
992 pat, replace_html = tag_data
993 # convert to plain `unicode` instead of a markup tag to be used in
993 # convert to plain `unicode` instead of a markup tag to be used in
994 # regex expressions. safe_unicode doesn't work here
994 # regex expressions. safe_unicode doesn't work here
995 html_value = pat.sub(replace_html, unicode(value))
995 html_value = pat.sub(replace_html, unicode(value))
996
996
997 return html_value
997 return html_value
998
998
999
999
1000 def bool2icon(value, show_at_false=True):
1000 def bool2icon(value, show_at_false=True):
1001 """
1001 """
1002 Returns boolean value of a given value, represented as html element with
1002 Returns boolean value of a given value, represented as html element with
1003 classes that will represent icons
1003 classes that will represent icons
1004
1004
1005 :param value: given value to convert to html node
1005 :param value: given value to convert to html node
1006 """
1006 """
1007
1007
1008 if value: # does bool conversion
1008 if value: # does bool conversion
1009 return HTML.tag('i', class_="icon-true", title='True')
1009 return HTML.tag('i', class_="icon-true", title='True')
1010 else: # not true as bool
1010 else: # not true as bool
1011 if show_at_false:
1011 if show_at_false:
1012 return HTML.tag('i', class_="icon-false", title='False')
1012 return HTML.tag('i', class_="icon-false", title='False')
1013 return HTML.tag('i')
1013 return HTML.tag('i')
1014
1014
1015 #==============================================================================
1015 #==============================================================================
1016 # PERMS
1016 # PERMS
1017 #==============================================================================
1017 #==============================================================================
1018 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1018 from rhodecode.lib.auth import (
1019 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1019 HasPermissionAny, HasPermissionAll,
1020 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1020 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1021 csrf_token_key
1021 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1022 csrf_token_key, AuthUser)
1022
1023
1023
1024
1024 #==============================================================================
1025 #==============================================================================
1025 # GRAVATAR URL
1026 # GRAVATAR URL
1026 #==============================================================================
1027 #==============================================================================
1027 class InitialsGravatar(object):
1028 class InitialsGravatar(object):
1028 def __init__(self, email_address, first_name, last_name, size=30,
1029 def __init__(self, email_address, first_name, last_name, size=30,
1029 background=None, text_color='#fff'):
1030 background=None, text_color='#fff'):
1030 self.size = size
1031 self.size = size
1031 self.first_name = first_name
1032 self.first_name = first_name
1032 self.last_name = last_name
1033 self.last_name = last_name
1033 self.email_address = email_address
1034 self.email_address = email_address
1034 self.background = background or self.str2color(email_address)
1035 self.background = background or self.str2color(email_address)
1035 self.text_color = text_color
1036 self.text_color = text_color
1036
1037
1037 def get_color_bank(self):
1038 def get_color_bank(self):
1038 """
1039 """
1039 returns a predefined list of colors that gravatars can use.
1040 returns a predefined list of colors that gravatars can use.
1040 Those are randomized distinct colors that guarantee readability and
1041 Those are randomized distinct colors that guarantee readability and
1041 uniqueness.
1042 uniqueness.
1042
1043
1043 generated with: http://phrogz.net/css/distinct-colors.html
1044 generated with: http://phrogz.net/css/distinct-colors.html
1044 """
1045 """
1045 return [
1046 return [
1046 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1047 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1047 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1048 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1048 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1049 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1049 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1050 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1050 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1051 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1051 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1052 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1052 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1053 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1053 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1054 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1054 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1055 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1055 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1056 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1056 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1057 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1057 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1058 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1058 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1059 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1059 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1060 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1060 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1061 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1061 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1062 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1062 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1063 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1063 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1064 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1064 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1065 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1065 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1066 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1066 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1067 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1067 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1068 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1068 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1069 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1069 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1070 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1070 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1071 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1071 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1072 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1072 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1073 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1073 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1074 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1074 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1075 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1075 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1076 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1076 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1077 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1077 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1078 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1078 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1079 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1079 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1080 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1080 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1081 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1081 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1082 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1082 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1083 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1083 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1084 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1084 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1085 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1085 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1086 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1086 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1087 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1087 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1088 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1088 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1089 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1089 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1090 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1090 '#4f8c46', '#368dd9', '#5c0073'
1091 '#4f8c46', '#368dd9', '#5c0073'
1091 ]
1092 ]
1092
1093
1093 def rgb_to_hex_color(self, rgb_tuple):
1094 def rgb_to_hex_color(self, rgb_tuple):
1094 """
1095 """
1095 Converts an rgb_tuple passed to an hex color.
1096 Converts an rgb_tuple passed to an hex color.
1096
1097
1097 :param rgb_tuple: tuple with 3 ints represents rgb color space
1098 :param rgb_tuple: tuple with 3 ints represents rgb color space
1098 """
1099 """
1099 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1100 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1100
1101
1101 def email_to_int_list(self, email_str):
1102 def email_to_int_list(self, email_str):
1102 """
1103 """
1103 Get every byte of the hex digest value of email and turn it to integer.
1104 Get every byte of the hex digest value of email and turn it to integer.
1104 It's going to be always between 0-255
1105 It's going to be always between 0-255
1105 """
1106 """
1106 digest = md5_safe(email_str.lower())
1107 digest = md5_safe(email_str.lower())
1107 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1108 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1108
1109
1109 def pick_color_bank_index(self, email_str, color_bank):
1110 def pick_color_bank_index(self, email_str, color_bank):
1110 return self.email_to_int_list(email_str)[0] % len(color_bank)
1111 return self.email_to_int_list(email_str)[0] % len(color_bank)
1111
1112
1112 def str2color(self, email_str):
1113 def str2color(self, email_str):
1113 """
1114 """
1114 Tries to map in a stable algorithm an email to color
1115 Tries to map in a stable algorithm an email to color
1115
1116
1116 :param email_str:
1117 :param email_str:
1117 """
1118 """
1118 color_bank = self.get_color_bank()
1119 color_bank = self.get_color_bank()
1119 # pick position (module it's length so we always find it in the
1120 # pick position (module it's length so we always find it in the
1120 # bank even if it's smaller than 256 values
1121 # bank even if it's smaller than 256 values
1121 pos = self.pick_color_bank_index(email_str, color_bank)
1122 pos = self.pick_color_bank_index(email_str, color_bank)
1122 return color_bank[pos]
1123 return color_bank[pos]
1123
1124
1124 def normalize_email(self, email_address):
1125 def normalize_email(self, email_address):
1125 import unicodedata
1126 import unicodedata
1126 # default host used to fill in the fake/missing email
1127 # default host used to fill in the fake/missing email
1127 default_host = u'localhost'
1128 default_host = u'localhost'
1128
1129
1129 if not email_address:
1130 if not email_address:
1130 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1131 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1131
1132
1132 email_address = safe_unicode(email_address)
1133 email_address = safe_unicode(email_address)
1133
1134
1134 if u'@' not in email_address:
1135 if u'@' not in email_address:
1135 email_address = u'%s@%s' % (email_address, default_host)
1136 email_address = u'%s@%s' % (email_address, default_host)
1136
1137
1137 if email_address.endswith(u'@'):
1138 if email_address.endswith(u'@'):
1138 email_address = u'%s%s' % (email_address, default_host)
1139 email_address = u'%s%s' % (email_address, default_host)
1139
1140
1140 email_address = unicodedata.normalize('NFKD', email_address)\
1141 email_address = unicodedata.normalize('NFKD', email_address)\
1141 .encode('ascii', 'ignore')
1142 .encode('ascii', 'ignore')
1142 return email_address
1143 return email_address
1143
1144
1144 def get_initials(self):
1145 def get_initials(self):
1145 """
1146 """
1146 Returns 2 letter initials calculated based on the input.
1147 Returns 2 letter initials calculated based on the input.
1147 The algorithm picks first given email address, and takes first letter
1148 The algorithm picks first given email address, and takes first letter
1148 of part before @, and then the first letter of server name. In case
1149 of part before @, and then the first letter of server name. In case
1149 the part before @ is in a format of `somestring.somestring2` it replaces
1150 the part before @ is in a format of `somestring.somestring2` it replaces
1150 the server letter with first letter of somestring2
1151 the server letter with first letter of somestring2
1151
1152
1152 In case function was initialized with both first and lastname, this
1153 In case function was initialized with both first and lastname, this
1153 overrides the extraction from email by first letter of the first and
1154 overrides the extraction from email by first letter of the first and
1154 last name. We add special logic to that functionality, In case Full name
1155 last name. We add special logic to that functionality, In case Full name
1155 is compound, like Guido Von Rossum, we use last part of the last name
1156 is compound, like Guido Von Rossum, we use last part of the last name
1156 (Von Rossum) picking `R`.
1157 (Von Rossum) picking `R`.
1157
1158
1158 Function also normalizes the non-ascii characters to they ascii
1159 Function also normalizes the non-ascii characters to they ascii
1159 representation, eg Ą => A
1160 representation, eg Ą => A
1160 """
1161 """
1161 import unicodedata
1162 import unicodedata
1162 # replace non-ascii to ascii
1163 # replace non-ascii to ascii
1163 first_name = unicodedata.normalize(
1164 first_name = unicodedata.normalize(
1164 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1165 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1165 last_name = unicodedata.normalize(
1166 last_name = unicodedata.normalize(
1166 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1167 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1167
1168
1168 # do NFKD encoding, and also make sure email has proper format
1169 # do NFKD encoding, and also make sure email has proper format
1169 email_address = self.normalize_email(self.email_address)
1170 email_address = self.normalize_email(self.email_address)
1170
1171
1171 # first push the email initials
1172 # first push the email initials
1172 prefix, server = email_address.split('@', 1)
1173 prefix, server = email_address.split('@', 1)
1173
1174
1174 # check if prefix is maybe a 'first_name.last_name' syntax
1175 # check if prefix is maybe a 'first_name.last_name' syntax
1175 _dot_split = prefix.rsplit('.', 1)
1176 _dot_split = prefix.rsplit('.', 1)
1176 if len(_dot_split) == 2 and _dot_split[1]:
1177 if len(_dot_split) == 2 and _dot_split[1]:
1177 initials = [_dot_split[0][0], _dot_split[1][0]]
1178 initials = [_dot_split[0][0], _dot_split[1][0]]
1178 else:
1179 else:
1179 initials = [prefix[0], server[0]]
1180 initials = [prefix[0], server[0]]
1180
1181
1181 # then try to replace either first_name or last_name
1182 # then try to replace either first_name or last_name
1182 fn_letter = (first_name or " ")[0].strip()
1183 fn_letter = (first_name or " ")[0].strip()
1183 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1184 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1184
1185
1185 if fn_letter:
1186 if fn_letter:
1186 initials[0] = fn_letter
1187 initials[0] = fn_letter
1187
1188
1188 if ln_letter:
1189 if ln_letter:
1189 initials[1] = ln_letter
1190 initials[1] = ln_letter
1190
1191
1191 return ''.join(initials).upper()
1192 return ''.join(initials).upper()
1192
1193
1193 def get_img_data_by_type(self, font_family, img_type):
1194 def get_img_data_by_type(self, font_family, img_type):
1194 default_user = """
1195 default_user = """
1195 <svg xmlns="http://www.w3.org/2000/svg"
1196 <svg xmlns="http://www.w3.org/2000/svg"
1196 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1197 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1197 viewBox="-15 -10 439.165 429.164"
1198 viewBox="-15 -10 439.165 429.164"
1198
1199
1199 xml:space="preserve"
1200 xml:space="preserve"
1200 style="background:{background};" >
1201 style="background:{background};" >
1201
1202
1202 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1203 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1203 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1204 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1204 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1205 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1205 168.596,153.916,216.671,
1206 168.596,153.916,216.671,
1206 204.583,216.671z" fill="{text_color}"/>
1207 204.583,216.671z" fill="{text_color}"/>
1207 <path d="M407.164,374.717L360.88,
1208 <path d="M407.164,374.717L360.88,
1208 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1209 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1209 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1210 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1210 15.366-44.203,23.488-69.076,23.488c-24.877,
1211 15.366-44.203,23.488-69.076,23.488c-24.877,
1211 0-48.762-8.122-69.078-23.488
1212 0-48.762-8.122-69.078-23.488
1212 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1213 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1213 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1214 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1214 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1215 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1215 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1216 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1216 19.402-10.527 C409.699,390.129,
1217 19.402-10.527 C409.699,390.129,
1217 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1218 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1218 </svg>""".format(
1219 </svg>""".format(
1219 size=self.size,
1220 size=self.size,
1220 background='#979797', # @grey4
1221 background='#979797', # @grey4
1221 text_color=self.text_color,
1222 text_color=self.text_color,
1222 font_family=font_family)
1223 font_family=font_family)
1223
1224
1224 return {
1225 return {
1225 "default_user": default_user
1226 "default_user": default_user
1226 }[img_type]
1227 }[img_type]
1227
1228
1228 def get_img_data(self, svg_type=None):
1229 def get_img_data(self, svg_type=None):
1229 """
1230 """
1230 generates the svg metadata for image
1231 generates the svg metadata for image
1231 """
1232 """
1232 fonts = [
1233 fonts = [
1233 '-apple-system',
1234 '-apple-system',
1234 'BlinkMacSystemFont',
1235 'BlinkMacSystemFont',
1235 'Segoe UI',
1236 'Segoe UI',
1236 'Roboto',
1237 'Roboto',
1237 'Oxygen-Sans',
1238 'Oxygen-Sans',
1238 'Ubuntu',
1239 'Ubuntu',
1239 'Cantarell',
1240 'Cantarell',
1240 'Helvetica Neue',
1241 'Helvetica Neue',
1241 'sans-serif'
1242 'sans-serif'
1242 ]
1243 ]
1243 font_family = ','.join(fonts)
1244 font_family = ','.join(fonts)
1244 if svg_type:
1245 if svg_type:
1245 return self.get_img_data_by_type(font_family, svg_type)
1246 return self.get_img_data_by_type(font_family, svg_type)
1246
1247
1247 initials = self.get_initials()
1248 initials = self.get_initials()
1248 img_data = """
1249 img_data = """
1249 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1250 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1250 width="{size}" height="{size}"
1251 width="{size}" height="{size}"
1251 style="width: 100%; height: 100%; background-color: {background}"
1252 style="width: 100%; height: 100%; background-color: {background}"
1252 viewBox="0 0 {size} {size}">
1253 viewBox="0 0 {size} {size}">
1253 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1254 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1254 pointer-events="auto" fill="{text_color}"
1255 pointer-events="auto" fill="{text_color}"
1255 font-family="{font_family}"
1256 font-family="{font_family}"
1256 style="font-weight: 400; font-size: {f_size}px;">{text}
1257 style="font-weight: 400; font-size: {f_size}px;">{text}
1257 </text>
1258 </text>
1258 </svg>""".format(
1259 </svg>""".format(
1259 size=self.size,
1260 size=self.size,
1260 f_size=self.size/2.05, # scale the text inside the box nicely
1261 f_size=self.size/2.05, # scale the text inside the box nicely
1261 background=self.background,
1262 background=self.background,
1262 text_color=self.text_color,
1263 text_color=self.text_color,
1263 text=initials.upper(),
1264 text=initials.upper(),
1264 font_family=font_family)
1265 font_family=font_family)
1265
1266
1266 return img_data
1267 return img_data
1267
1268
1268 def generate_svg(self, svg_type=None):
1269 def generate_svg(self, svg_type=None):
1269 img_data = self.get_img_data(svg_type)
1270 img_data = self.get_img_data(svg_type)
1270 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1271 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1271
1272
1272
1273
1273 def initials_gravatar(email_address, first_name, last_name, size=30):
1274 def initials_gravatar(email_address, first_name, last_name, size=30):
1274 svg_type = None
1275 svg_type = None
1275 if email_address == User.DEFAULT_USER_EMAIL:
1276 if email_address == User.DEFAULT_USER_EMAIL:
1276 svg_type = 'default_user'
1277 svg_type = 'default_user'
1277 klass = InitialsGravatar(email_address, first_name, last_name, size)
1278 klass = InitialsGravatar(email_address, first_name, last_name, size)
1278 return klass.generate_svg(svg_type=svg_type)
1279 return klass.generate_svg(svg_type=svg_type)
1279
1280
1280
1281
1281 def gravatar_url(email_address, size=30, request=None):
1282 def gravatar_url(email_address, size=30, request=None):
1282 request = get_current_request()
1283 request = get_current_request()
1283 _use_gravatar = request.call_context.visual.use_gravatar
1284 _use_gravatar = request.call_context.visual.use_gravatar
1284 _gravatar_url = request.call_context.visual.gravatar_url
1285 _gravatar_url = request.call_context.visual.gravatar_url
1285
1286
1286 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1287 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1287
1288
1288 email_address = email_address or User.DEFAULT_USER_EMAIL
1289 email_address = email_address or User.DEFAULT_USER_EMAIL
1289 if isinstance(email_address, unicode):
1290 if isinstance(email_address, unicode):
1290 # hashlib crashes on unicode items
1291 # hashlib crashes on unicode items
1291 email_address = safe_str(email_address)
1292 email_address = safe_str(email_address)
1292
1293
1293 # empty email or default user
1294 # empty email or default user
1294 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1295 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1295 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1296 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1296
1297
1297 if _use_gravatar:
1298 if _use_gravatar:
1298 # TODO: Disuse pyramid thread locals. Think about another solution to
1299 # TODO: Disuse pyramid thread locals. Think about another solution to
1299 # get the host and schema here.
1300 # get the host and schema here.
1300 request = get_current_request()
1301 request = get_current_request()
1301 tmpl = safe_str(_gravatar_url)
1302 tmpl = safe_str(_gravatar_url)
1302 tmpl = tmpl.replace('{email}', email_address)\
1303 tmpl = tmpl.replace('{email}', email_address)\
1303 .replace('{md5email}', md5_safe(email_address.lower())) \
1304 .replace('{md5email}', md5_safe(email_address.lower())) \
1304 .replace('{netloc}', request.host)\
1305 .replace('{netloc}', request.host)\
1305 .replace('{scheme}', request.scheme)\
1306 .replace('{scheme}', request.scheme)\
1306 .replace('{size}', safe_str(size))
1307 .replace('{size}', safe_str(size))
1307 return tmpl
1308 return tmpl
1308 else:
1309 else:
1309 return initials_gravatar(email_address, '', '', size=size)
1310 return initials_gravatar(email_address, '', '', size=size)
1310
1311
1311
1312
1312 def breadcrumb_repo_link(repo):
1313 def breadcrumb_repo_link(repo):
1313 """
1314 """
1314 Makes a breadcrumbs path link to repo
1315 Makes a breadcrumbs path link to repo
1315
1316
1316 ex::
1317 ex::
1317 group >> subgroup >> repo
1318 group >> subgroup >> repo
1318
1319
1319 :param repo: a Repository instance
1320 :param repo: a Repository instance
1320 """
1321 """
1321
1322
1322 path = [
1323 path = [
1323 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1324 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1324 title='last change:{}'.format(format_date(group.last_commit_change)))
1325 title='last change:{}'.format(format_date(group.last_commit_change)))
1325 for group in repo.groups_with_parents
1326 for group in repo.groups_with_parents
1326 ] + [
1327 ] + [
1327 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1328 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1328 title='last change:{}'.format(format_date(repo.last_commit_change)))
1329 title='last change:{}'.format(format_date(repo.last_commit_change)))
1329 ]
1330 ]
1330
1331
1331 return literal(' &raquo; '.join(path))
1332 return literal(' &raquo; '.join(path))
1332
1333
1333
1334
1334 def breadcrumb_repo_group_link(repo_group):
1335 def breadcrumb_repo_group_link(repo_group):
1335 """
1336 """
1336 Makes a breadcrumbs path link to repo
1337 Makes a breadcrumbs path link to repo
1337
1338
1338 ex::
1339 ex::
1339 group >> subgroup
1340 group >> subgroup
1340
1341
1341 :param repo_group: a Repository Group instance
1342 :param repo_group: a Repository Group instance
1342 """
1343 """
1343
1344
1344 path = [
1345 path = [
1345 link_to(group.name,
1346 link_to(group.name,
1346 route_path('repo_group_home', repo_group_name=group.group_name),
1347 route_path('repo_group_home', repo_group_name=group.group_name),
1347 title='last change:{}'.format(format_date(group.last_commit_change)))
1348 title='last change:{}'.format(format_date(group.last_commit_change)))
1348 for group in repo_group.parents
1349 for group in repo_group.parents
1349 ] + [
1350 ] + [
1350 link_to(repo_group.name,
1351 link_to(repo_group.name,
1351 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1352 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1352 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1353 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1353 ]
1354 ]
1354
1355
1355 return literal(' &raquo; '.join(path))
1356 return literal(' &raquo; '.join(path))
1356
1357
1357
1358
1358 def format_byte_size_binary(file_size):
1359 def format_byte_size_binary(file_size):
1359 """
1360 """
1360 Formats file/folder sizes to standard.
1361 Formats file/folder sizes to standard.
1361 """
1362 """
1362 if file_size is None:
1363 if file_size is None:
1363 file_size = 0
1364 file_size = 0
1364
1365
1365 formatted_size = format_byte_size(file_size, binary=True)
1366 formatted_size = format_byte_size(file_size, binary=True)
1366 return formatted_size
1367 return formatted_size
1367
1368
1368
1369
1369 def urlify_text(text_, safe=True, **href_attrs):
1370 def urlify_text(text_, safe=True, **href_attrs):
1370 """
1371 """
1371 Extract urls from text and make html links out of them
1372 Extract urls from text and make html links out of them
1372 """
1373 """
1373
1374
1374 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1375 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1375 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1376 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1376
1377
1377 def url_func(match_obj):
1378 def url_func(match_obj):
1378 url_full = match_obj.groups()[0]
1379 url_full = match_obj.groups()[0]
1379 a_options = dict(href_attrs)
1380 a_options = dict(href_attrs)
1380 a_options['href'] = url_full
1381 a_options['href'] = url_full
1381 a_text = url_full
1382 a_text = url_full
1382 return HTML.tag("a", a_text, **a_options)
1383 return HTML.tag("a", a_text, **a_options)
1383
1384
1384 _new_text = url_pat.sub(url_func, text_)
1385 _new_text = url_pat.sub(url_func, text_)
1385
1386
1386 if safe:
1387 if safe:
1387 return literal(_new_text)
1388 return literal(_new_text)
1388 return _new_text
1389 return _new_text
1389
1390
1390
1391
1391 def urlify_commits(text_, repo_name):
1392 def urlify_commits(text_, repo_name):
1392 """
1393 """
1393 Extract commit ids from text and make link from them
1394 Extract commit ids from text and make link from them
1394
1395
1395 :param text_:
1396 :param text_:
1396 :param repo_name: repo name to build the URL with
1397 :param repo_name: repo name to build the URL with
1397 """
1398 """
1398
1399
1399 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1400 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1400
1401
1401 def url_func(match_obj):
1402 def url_func(match_obj):
1402 commit_id = match_obj.groups()[1]
1403 commit_id = match_obj.groups()[1]
1403 pref = match_obj.groups()[0]
1404 pref = match_obj.groups()[0]
1404 suf = match_obj.groups()[2]
1405 suf = match_obj.groups()[2]
1405
1406
1406 tmpl = (
1407 tmpl = (
1407 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1408 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1408 '%(commit_id)s</a>%(suf)s'
1409 '%(commit_id)s</a>%(suf)s'
1409 )
1410 )
1410 return tmpl % {
1411 return tmpl % {
1411 'pref': pref,
1412 'pref': pref,
1412 'cls': 'revision-link',
1413 'cls': 'revision-link',
1413 'url': route_url(
1414 'url': route_url(
1414 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1415 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1415 'commit_id': commit_id,
1416 'commit_id': commit_id,
1416 'suf': suf,
1417 'suf': suf,
1417 'hovercard_alt': 'Commit: {}'.format(commit_id),
1418 'hovercard_alt': 'Commit: {}'.format(commit_id),
1418 'hovercard_url': route_url(
1419 'hovercard_url': route_url(
1419 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1420 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1420 }
1421 }
1421
1422
1422 new_text = url_pat.sub(url_func, text_)
1423 new_text = url_pat.sub(url_func, text_)
1423
1424
1424 return new_text
1425 return new_text
1425
1426
1426
1427
1427 def _process_url_func(match_obj, repo_name, uid, entry,
1428 def _process_url_func(match_obj, repo_name, uid, entry,
1428 return_raw_data=False, link_format='html'):
1429 return_raw_data=False, link_format='html'):
1429 pref = ''
1430 pref = ''
1430 if match_obj.group().startswith(' '):
1431 if match_obj.group().startswith(' '):
1431 pref = ' '
1432 pref = ' '
1432
1433
1433 issue_id = ''.join(match_obj.groups())
1434 issue_id = ''.join(match_obj.groups())
1434
1435
1435 if link_format == 'html':
1436 if link_format == 'html':
1436 tmpl = (
1437 tmpl = (
1437 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1438 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1438 '%(issue-prefix)s%(id-repr)s'
1439 '%(issue-prefix)s%(id-repr)s'
1439 '</a>')
1440 '</a>')
1440 elif link_format == 'html+hovercard':
1441 elif link_format == 'html+hovercard':
1441 tmpl = (
1442 tmpl = (
1442 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1443 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1443 '%(issue-prefix)s%(id-repr)s'
1444 '%(issue-prefix)s%(id-repr)s'
1444 '</a>')
1445 '</a>')
1445 elif link_format in ['rst', 'rst+hovercard']:
1446 elif link_format in ['rst', 'rst+hovercard']:
1446 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1447 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1447 elif link_format in ['markdown', 'markdown+hovercard']:
1448 elif link_format in ['markdown', 'markdown+hovercard']:
1448 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1449 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1449 else:
1450 else:
1450 raise ValueError('Bad link_format:{}'.format(link_format))
1451 raise ValueError('Bad link_format:{}'.format(link_format))
1451
1452
1452 (repo_name_cleaned,
1453 (repo_name_cleaned,
1453 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1454 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1454
1455
1455 # variables replacement
1456 # variables replacement
1456 named_vars = {
1457 named_vars = {
1457 'id': issue_id,
1458 'id': issue_id,
1458 'repo': repo_name,
1459 'repo': repo_name,
1459 'repo_name': repo_name_cleaned,
1460 'repo_name': repo_name_cleaned,
1460 'group_name': parent_group_name,
1461 'group_name': parent_group_name,
1461 # set dummy keys so we always have them
1462 # set dummy keys so we always have them
1462 'hostname': '',
1463 'hostname': '',
1463 'netloc': '',
1464 'netloc': '',
1464 'scheme': ''
1465 'scheme': ''
1465 }
1466 }
1466
1467
1467 request = get_current_request()
1468 request = get_current_request()
1468 if request:
1469 if request:
1469 # exposes, hostname, netloc, scheme
1470 # exposes, hostname, netloc, scheme
1470 host_data = get_host_info(request)
1471 host_data = get_host_info(request)
1471 named_vars.update(host_data)
1472 named_vars.update(host_data)
1472
1473
1473 # named regex variables
1474 # named regex variables
1474 named_vars.update(match_obj.groupdict())
1475 named_vars.update(match_obj.groupdict())
1475 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1476 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1476 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1477 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1477 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1478 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1478
1479
1479 def quote_cleaner(input_str):
1480 def quote_cleaner(input_str):
1480 """Remove quotes as it's HTML"""
1481 """Remove quotes as it's HTML"""
1481 return input_str.replace('"', '')
1482 return input_str.replace('"', '')
1482
1483
1483 data = {
1484 data = {
1484 'pref': pref,
1485 'pref': pref,
1485 'cls': quote_cleaner('issue-tracker-link'),
1486 'cls': quote_cleaner('issue-tracker-link'),
1486 'url': quote_cleaner(_url),
1487 'url': quote_cleaner(_url),
1487 'id-repr': issue_id,
1488 'id-repr': issue_id,
1488 'issue-prefix': entry['pref'],
1489 'issue-prefix': entry['pref'],
1489 'serv': entry['url'],
1490 'serv': entry['url'],
1490 'title': desc,
1491 'title': desc,
1491 'hovercard_url': hovercard_url
1492 'hovercard_url': hovercard_url
1492 }
1493 }
1493
1494
1494 if return_raw_data:
1495 if return_raw_data:
1495 return {
1496 return {
1496 'id': issue_id,
1497 'id': issue_id,
1497 'url': _url
1498 'url': _url
1498 }
1499 }
1499 return tmpl % data
1500 return tmpl % data
1500
1501
1501
1502
1502 def get_active_pattern_entries(repo_name):
1503 def get_active_pattern_entries(repo_name):
1503 repo = None
1504 repo = None
1504 if repo_name:
1505 if repo_name:
1505 # Retrieving repo_name to avoid invalid repo_name to explode on
1506 # Retrieving repo_name to avoid invalid repo_name to explode on
1506 # IssueTrackerSettingsModel but still passing invalid name further down
1507 # IssueTrackerSettingsModel but still passing invalid name further down
1507 repo = Repository.get_by_repo_name(repo_name, cache=True)
1508 repo = Repository.get_by_repo_name(repo_name, cache=True)
1508
1509
1509 settings_model = IssueTrackerSettingsModel(repo=repo)
1510 settings_model = IssueTrackerSettingsModel(repo=repo)
1510 active_entries = settings_model.get_settings(cache=True)
1511 active_entries = settings_model.get_settings(cache=True)
1511 return active_entries
1512 return active_entries
1512
1513
1513
1514
1514 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1515 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1515
1516
1516 allowed_formats = ['html', 'rst', 'markdown',
1517 allowed_formats = ['html', 'rst', 'markdown',
1517 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1518 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1518 if link_format not in allowed_formats:
1519 if link_format not in allowed_formats:
1519 raise ValueError('Link format can be only one of:{} got {}'.format(
1520 raise ValueError('Link format can be only one of:{} got {}'.format(
1520 allowed_formats, link_format))
1521 allowed_formats, link_format))
1521
1522
1522 active_entries = active_entries or get_active_pattern_entries(repo_name)
1523 active_entries = active_entries or get_active_pattern_entries(repo_name)
1523 issues_data = []
1524 issues_data = []
1524 new_text = text_string
1525 new_text = text_string
1525
1526
1526 log.debug('Got %s entries to process', len(active_entries))
1527 log.debug('Got %s entries to process', len(active_entries))
1527 for uid, entry in active_entries.items():
1528 for uid, entry in active_entries.items():
1528 log.debug('found issue tracker entry with uid %s', uid)
1529 log.debug('found issue tracker entry with uid %s', uid)
1529
1530
1530 if not (entry['pat'] and entry['url']):
1531 if not (entry['pat'] and entry['url']):
1531 log.debug('skipping due to missing data')
1532 log.debug('skipping due to missing data')
1532 continue
1533 continue
1533
1534
1534 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1535 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1535 uid, entry['pat'], entry['url'], entry['pref'])
1536 uid, entry['pat'], entry['url'], entry['pref'])
1536
1537
1537 try:
1538 try:
1538 pattern = re.compile(r'%s' % entry['pat'])
1539 pattern = re.compile(r'%s' % entry['pat'])
1539 except re.error:
1540 except re.error:
1540 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1541 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1541 continue
1542 continue
1542
1543
1543 data_func = partial(
1544 data_func = partial(
1544 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1545 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1545 return_raw_data=True)
1546 return_raw_data=True)
1546
1547
1547 for match_obj in pattern.finditer(text_string):
1548 for match_obj in pattern.finditer(text_string):
1548 issues_data.append(data_func(match_obj))
1549 issues_data.append(data_func(match_obj))
1549
1550
1550 url_func = partial(
1551 url_func = partial(
1551 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1552 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1552 link_format=link_format)
1553 link_format=link_format)
1553
1554
1554 new_text = pattern.sub(url_func, new_text)
1555 new_text = pattern.sub(url_func, new_text)
1555 log.debug('processed prefix:uid `%s`', uid)
1556 log.debug('processed prefix:uid `%s`', uid)
1556
1557
1557 # finally use global replace, eg !123 -> pr-link, those will not catch
1558 # finally use global replace, eg !123 -> pr-link, those will not catch
1558 # if already similar pattern exists
1559 # if already similar pattern exists
1559 server_url = '${scheme}://${netloc}'
1560 server_url = '${scheme}://${netloc}'
1560 pr_entry = {
1561 pr_entry = {
1561 'pref': '!',
1562 'pref': '!',
1562 'url': server_url + '/_admin/pull-requests/${id}',
1563 'url': server_url + '/_admin/pull-requests/${id}',
1563 'desc': 'Pull Request !${id}',
1564 'desc': 'Pull Request !${id}',
1564 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1565 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1565 }
1566 }
1566 pr_url_func = partial(
1567 pr_url_func = partial(
1567 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1568 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1568 link_format=link_format+'+hovercard')
1569 link_format=link_format+'+hovercard')
1569 new_text = re.compile(r'(?:(?:^!)|(?: !))(\d+)').sub(pr_url_func, new_text)
1570 new_text = re.compile(r'(?:(?:^!)|(?: !))(\d+)').sub(pr_url_func, new_text)
1570 log.debug('processed !pr pattern')
1571 log.debug('processed !pr pattern')
1571
1572
1572 return new_text, issues_data
1573 return new_text, issues_data
1573
1574
1574
1575
1575 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1576 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1576 """
1577 """
1577 Parses given text message and makes proper links.
1578 Parses given text message and makes proper links.
1578 issues are linked to given issue-server, and rest is a commit link
1579 issues are linked to given issue-server, and rest is a commit link
1579 """
1580 """
1580 def escaper(_text):
1581 def escaper(_text):
1581 return _text.replace('<', '&lt;').replace('>', '&gt;')
1582 return _text.replace('<', '&lt;').replace('>', '&gt;')
1582
1583
1583 new_text = escaper(commit_text)
1584 new_text = escaper(commit_text)
1584
1585
1585 # extract http/https links and make them real urls
1586 # extract http/https links and make them real urls
1586 new_text = urlify_text(new_text, safe=False)
1587 new_text = urlify_text(new_text, safe=False)
1587
1588
1588 # urlify commits - extract commit ids and make link out of them, if we have
1589 # urlify commits - extract commit ids and make link out of them, if we have
1589 # the scope of repository present.
1590 # the scope of repository present.
1590 if repository:
1591 if repository:
1591 new_text = urlify_commits(new_text, repository)
1592 new_text = urlify_commits(new_text, repository)
1592
1593
1593 # process issue tracker patterns
1594 # process issue tracker patterns
1594 new_text, issues = process_patterns(new_text, repository or '',
1595 new_text, issues = process_patterns(new_text, repository or '',
1595 active_entries=active_pattern_entries)
1596 active_entries=active_pattern_entries)
1596
1597
1597 return literal(new_text)
1598 return literal(new_text)
1598
1599
1599
1600
1600 def render_binary(repo_name, file_obj):
1601 def render_binary(repo_name, file_obj):
1601 """
1602 """
1602 Choose how to render a binary file
1603 Choose how to render a binary file
1603 """
1604 """
1604
1605
1605 filename = file_obj.name
1606 filename = file_obj.name
1606
1607
1607 # images
1608 # images
1608 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1609 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1609 if fnmatch.fnmatch(filename, pat=ext):
1610 if fnmatch.fnmatch(filename, pat=ext):
1610 alt = escape(filename)
1611 alt = escape(filename)
1611 src = route_path(
1612 src = route_path(
1612 'repo_file_raw', repo_name=repo_name,
1613 'repo_file_raw', repo_name=repo_name,
1613 commit_id=file_obj.commit.raw_id,
1614 commit_id=file_obj.commit.raw_id,
1614 f_path=file_obj.path)
1615 f_path=file_obj.path)
1615 return literal(
1616 return literal(
1616 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1617 '<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1617
1618
1618
1619
1619 def renderer_from_filename(filename, exclude=None):
1620 def renderer_from_filename(filename, exclude=None):
1620 """
1621 """
1621 choose a renderer based on filename, this works only for text based files
1622 choose a renderer based on filename, this works only for text based files
1622 """
1623 """
1623
1624
1624 # ipython
1625 # ipython
1625 for ext in ['*.ipynb']:
1626 for ext in ['*.ipynb']:
1626 if fnmatch.fnmatch(filename, pat=ext):
1627 if fnmatch.fnmatch(filename, pat=ext):
1627 return 'jupyter'
1628 return 'jupyter'
1628
1629
1629 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1630 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1630 if is_markup:
1631 if is_markup:
1631 return is_markup
1632 return is_markup
1632 return None
1633 return None
1633
1634
1634
1635
1635 def render(source, renderer='rst', mentions=False, relative_urls=None,
1636 def render(source, renderer='rst', mentions=False, relative_urls=None,
1636 repo_name=None):
1637 repo_name=None):
1637
1638
1638 def maybe_convert_relative_links(html_source):
1639 def maybe_convert_relative_links(html_source):
1639 if relative_urls:
1640 if relative_urls:
1640 return relative_links(html_source, relative_urls)
1641 return relative_links(html_source, relative_urls)
1641 return html_source
1642 return html_source
1642
1643
1643 if renderer == 'plain':
1644 if renderer == 'plain':
1644 return literal(
1645 return literal(
1645 MarkupRenderer.plain(source, leading_newline=False))
1646 MarkupRenderer.plain(source, leading_newline=False))
1646
1647
1647 elif renderer == 'rst':
1648 elif renderer == 'rst':
1648 if repo_name:
1649 if repo_name:
1649 # process patterns on comments if we pass in repo name
1650 # process patterns on comments if we pass in repo name
1650 source, issues = process_patterns(
1651 source, issues = process_patterns(
1651 source, repo_name, link_format='rst')
1652 source, repo_name, link_format='rst')
1652
1653
1653 return literal(
1654 return literal(
1654 '<div class="rst-block">%s</div>' %
1655 '<div class="rst-block">%s</div>' %
1655 maybe_convert_relative_links(
1656 maybe_convert_relative_links(
1656 MarkupRenderer.rst(source, mentions=mentions)))
1657 MarkupRenderer.rst(source, mentions=mentions)))
1657
1658
1658 elif renderer == 'markdown':
1659 elif renderer == 'markdown':
1659 if repo_name:
1660 if repo_name:
1660 # process patterns on comments if we pass in repo name
1661 # process patterns on comments if we pass in repo name
1661 source, issues = process_patterns(
1662 source, issues = process_patterns(
1662 source, repo_name, link_format='markdown')
1663 source, repo_name, link_format='markdown')
1663
1664
1664 return literal(
1665 return literal(
1665 '<div class="markdown-block">%s</div>' %
1666 '<div class="markdown-block">%s</div>' %
1666 maybe_convert_relative_links(
1667 maybe_convert_relative_links(
1667 MarkupRenderer.markdown(source, flavored=True,
1668 MarkupRenderer.markdown(source, flavored=True,
1668 mentions=mentions)))
1669 mentions=mentions)))
1669
1670
1670 elif renderer == 'jupyter':
1671 elif renderer == 'jupyter':
1671 return literal(
1672 return literal(
1672 '<div class="ipynb">%s</div>' %
1673 '<div class="ipynb">%s</div>' %
1673 maybe_convert_relative_links(
1674 maybe_convert_relative_links(
1674 MarkupRenderer.jupyter(source)))
1675 MarkupRenderer.jupyter(source)))
1675
1676
1676 # None means just show the file-source
1677 # None means just show the file-source
1677 return None
1678 return None
1678
1679
1679
1680
1680 def commit_status(repo, commit_id):
1681 def commit_status(repo, commit_id):
1681 return ChangesetStatusModel().get_status(repo, commit_id)
1682 return ChangesetStatusModel().get_status(repo, commit_id)
1682
1683
1683
1684
1684 def commit_status_lbl(commit_status):
1685 def commit_status_lbl(commit_status):
1685 return dict(ChangesetStatus.STATUSES).get(commit_status)
1686 return dict(ChangesetStatus.STATUSES).get(commit_status)
1686
1687
1687
1688
1688 def commit_time(repo_name, commit_id):
1689 def commit_time(repo_name, commit_id):
1689 repo = Repository.get_by_repo_name(repo_name)
1690 repo = Repository.get_by_repo_name(repo_name)
1690 commit = repo.get_commit(commit_id=commit_id)
1691 commit = repo.get_commit(commit_id=commit_id)
1691 return commit.date
1692 return commit.date
1692
1693
1693
1694
1694 def get_permission_name(key):
1695 def get_permission_name(key):
1695 return dict(Permission.PERMS).get(key)
1696 return dict(Permission.PERMS).get(key)
1696
1697
1697
1698
1698 def journal_filter_help(request):
1699 def journal_filter_help(request):
1699 _ = request.translate
1700 _ = request.translate
1700 from rhodecode.lib.audit_logger import ACTIONS
1701 from rhodecode.lib.audit_logger import ACTIONS
1701 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1702 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1702
1703
1703 return _(
1704 return _(
1704 'Example filter terms:\n' +
1705 'Example filter terms:\n' +
1705 ' repository:vcs\n' +
1706 ' repository:vcs\n' +
1706 ' username:marcin\n' +
1707 ' username:marcin\n' +
1707 ' username:(NOT marcin)\n' +
1708 ' username:(NOT marcin)\n' +
1708 ' action:*push*\n' +
1709 ' action:*push*\n' +
1709 ' ip:127.0.0.1\n' +
1710 ' ip:127.0.0.1\n' +
1710 ' date:20120101\n' +
1711 ' date:20120101\n' +
1711 ' date:[20120101100000 TO 20120102]\n' +
1712 ' date:[20120101100000 TO 20120102]\n' +
1712 '\n' +
1713 '\n' +
1713 'Actions: {actions}\n' +
1714 'Actions: {actions}\n' +
1714 '\n' +
1715 '\n' +
1715 'Generate wildcards using \'*\' character:\n' +
1716 'Generate wildcards using \'*\' character:\n' +
1716 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1717 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1717 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1718 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1718 '\n' +
1719 '\n' +
1719 'Optional AND / OR operators in queries\n' +
1720 'Optional AND / OR operators in queries\n' +
1720 ' "repository:vcs OR repository:test"\n' +
1721 ' "repository:vcs OR repository:test"\n' +
1721 ' "username:test AND repository:test*"\n'
1722 ' "username:test AND repository:test*"\n'
1722 ).format(actions=actions)
1723 ).format(actions=actions)
1723
1724
1724
1725
1725 def not_mapped_error(repo_name):
1726 def not_mapped_error(repo_name):
1726 from rhodecode.translation import _
1727 from rhodecode.translation import _
1727 flash(_('%s repository is not mapped to db perhaps'
1728 flash(_('%s repository is not mapped to db perhaps'
1728 ' it was created or renamed from the filesystem'
1729 ' it was created or renamed from the filesystem'
1729 ' please run the application again'
1730 ' please run the application again'
1730 ' in order to rescan repositories') % repo_name, category='error')
1731 ' in order to rescan repositories') % repo_name, category='error')
1731
1732
1732
1733
1733 def ip_range(ip_addr):
1734 def ip_range(ip_addr):
1734 from rhodecode.model.db import UserIpMap
1735 from rhodecode.model.db import UserIpMap
1735 s, e = UserIpMap._get_ip_range(ip_addr)
1736 s, e = UserIpMap._get_ip_range(ip_addr)
1736 return '%s - %s' % (s, e)
1737 return '%s - %s' % (s, e)
1737
1738
1738
1739
1739 def form(url, method='post', needs_csrf_token=True, **attrs):
1740 def form(url, method='post', needs_csrf_token=True, **attrs):
1740 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1741 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1741 if method.lower() != 'get' and needs_csrf_token:
1742 if method.lower() != 'get' and needs_csrf_token:
1742 raise Exception(
1743 raise Exception(
1743 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1744 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1744 'CSRF token. If the endpoint does not require such token you can ' +
1745 'CSRF token. If the endpoint does not require such token you can ' +
1745 'explicitly set the parameter needs_csrf_token to false.')
1746 'explicitly set the parameter needs_csrf_token to false.')
1746
1747
1747 return insecure_form(url, method=method, **attrs)
1748 return insecure_form(url, method=method, **attrs)
1748
1749
1749
1750
1750 def secure_form(form_url, method="POST", multipart=False, **attrs):
1751 def secure_form(form_url, method="POST", multipart=False, **attrs):
1751 """Start a form tag that points the action to an url. This
1752 """Start a form tag that points the action to an url. This
1752 form tag will also include the hidden field containing
1753 form tag will also include the hidden field containing
1753 the auth token.
1754 the auth token.
1754
1755
1755 The url options should be given either as a string, or as a
1756 The url options should be given either as a string, or as a
1756 ``url()`` function. The method for the form defaults to POST.
1757 ``url()`` function. The method for the form defaults to POST.
1757
1758
1758 Options:
1759 Options:
1759
1760
1760 ``multipart``
1761 ``multipart``
1761 If set to True, the enctype is set to "multipart/form-data".
1762 If set to True, the enctype is set to "multipart/form-data".
1762 ``method``
1763 ``method``
1763 The method to use when submitting the form, usually either
1764 The method to use when submitting the form, usually either
1764 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1765 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1765 hidden input with name _method is added to simulate the verb
1766 hidden input with name _method is added to simulate the verb
1766 over POST.
1767 over POST.
1767
1768
1768 """
1769 """
1769
1770
1770 if 'request' in attrs:
1771 if 'request' in attrs:
1771 session = attrs['request'].session
1772 session = attrs['request'].session
1772 del attrs['request']
1773 del attrs['request']
1773 else:
1774 else:
1774 raise ValueError(
1775 raise ValueError(
1775 'Calling this form requires request= to be passed as argument')
1776 'Calling this form requires request= to be passed as argument')
1776
1777
1777 _form = insecure_form(form_url, method, multipart, **attrs)
1778 _form = insecure_form(form_url, method, multipart, **attrs)
1778 token = literal(
1779 token = literal(
1779 '<input type="hidden" name="{}" value="{}">'.format(
1780 '<input type="hidden" name="{}" value="{}">'.format(
1780 csrf_token_key, get_csrf_token(session)))
1781 csrf_token_key, get_csrf_token(session)))
1781
1782
1782 return literal("%s\n%s" % (_form, token))
1783 return literal("%s\n%s" % (_form, token))
1783
1784
1784
1785
1785 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1786 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1786 select_html = select(name, selected, options, **attrs)
1787 select_html = select(name, selected, options, **attrs)
1787
1788
1788 select2 = """
1789 select2 = """
1789 <script>
1790 <script>
1790 $(document).ready(function() {
1791 $(document).ready(function() {
1791 $('#%s').select2({
1792 $('#%s').select2({
1792 containerCssClass: 'drop-menu %s',
1793 containerCssClass: 'drop-menu %s',
1793 dropdownCssClass: 'drop-menu-dropdown',
1794 dropdownCssClass: 'drop-menu-dropdown',
1794 dropdownAutoWidth: true%s
1795 dropdownAutoWidth: true%s
1795 });
1796 });
1796 });
1797 });
1797 </script>
1798 </script>
1798 """
1799 """
1799
1800
1800 filter_option = """,
1801 filter_option = """,
1801 minimumResultsForSearch: -1
1802 minimumResultsForSearch: -1
1802 """
1803 """
1803 input_id = attrs.get('id') or name
1804 input_id = attrs.get('id') or name
1804 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1805 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1805 filter_enabled = "" if enable_filter else filter_option
1806 filter_enabled = "" if enable_filter else filter_option
1806 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1807 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1807
1808
1808 return literal(select_html+select_script)
1809 return literal(select_html+select_script)
1809
1810
1810
1811
1811 def get_visual_attr(tmpl_context_var, attr_name):
1812 def get_visual_attr(tmpl_context_var, attr_name):
1812 """
1813 """
1813 A safe way to get a variable from visual variable of template context
1814 A safe way to get a variable from visual variable of template context
1814
1815
1815 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1816 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1816 :param attr_name: name of the attribute we fetch from the c.visual
1817 :param attr_name: name of the attribute we fetch from the c.visual
1817 """
1818 """
1818 visual = getattr(tmpl_context_var, 'visual', None)
1819 visual = getattr(tmpl_context_var, 'visual', None)
1819 if not visual:
1820 if not visual:
1820 return
1821 return
1821 else:
1822 else:
1822 return getattr(visual, attr_name, None)
1823 return getattr(visual, attr_name, None)
1823
1824
1824
1825
1825 def get_last_path_part(file_node):
1826 def get_last_path_part(file_node):
1826 if not file_node.path:
1827 if not file_node.path:
1827 return u'/'
1828 return u'/'
1828
1829
1829 path = safe_unicode(file_node.path.split('/')[-1])
1830 path = safe_unicode(file_node.path.split('/')[-1])
1830 return u'../' + path
1831 return u'../' + path
1831
1832
1832
1833
1833 def route_url(*args, **kwargs):
1834 def route_url(*args, **kwargs):
1834 """
1835 """
1835 Wrapper around pyramids `route_url` (fully qualified url) function.
1836 Wrapper around pyramids `route_url` (fully qualified url) function.
1836 """
1837 """
1837 req = get_current_request()
1838 req = get_current_request()
1838 return req.route_url(*args, **kwargs)
1839 return req.route_url(*args, **kwargs)
1839
1840
1840
1841
1841 def route_path(*args, **kwargs):
1842 def route_path(*args, **kwargs):
1842 """
1843 """
1843 Wrapper around pyramids `route_path` function.
1844 Wrapper around pyramids `route_path` function.
1844 """
1845 """
1845 req = get_current_request()
1846 req = get_current_request()
1846 return req.route_path(*args, **kwargs)
1847 return req.route_path(*args, **kwargs)
1847
1848
1848
1849
1849 def route_path_or_none(*args, **kwargs):
1850 def route_path_or_none(*args, **kwargs):
1850 try:
1851 try:
1851 return route_path(*args, **kwargs)
1852 return route_path(*args, **kwargs)
1852 except KeyError:
1853 except KeyError:
1853 return None
1854 return None
1854
1855
1855
1856
1856 def current_route_path(request, **kw):
1857 def current_route_path(request, **kw):
1857 new_args = request.GET.mixed()
1858 new_args = request.GET.mixed()
1858 new_args.update(kw)
1859 new_args.update(kw)
1859 return request.current_route_path(_query=new_args)
1860 return request.current_route_path(_query=new_args)
1860
1861
1861
1862
1862 def curl_api_example(method, args):
1863 def curl_api_example(method, args):
1863 args_json = json.dumps(OrderedDict([
1864 args_json = json.dumps(OrderedDict([
1864 ('id', 1),
1865 ('id', 1),
1865 ('auth_token', 'SECRET'),
1866 ('auth_token', 'SECRET'),
1866 ('method', method),
1867 ('method', method),
1867 ('args', args)
1868 ('args', args)
1868 ]))
1869 ]))
1869
1870
1870 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1871 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
1871 api_url=route_url('apiv2'),
1872 api_url=route_url('apiv2'),
1872 args_json=args_json
1873 args_json=args_json
1873 )
1874 )
1874
1875
1875
1876
1876 def api_call_example(method, args):
1877 def api_call_example(method, args):
1877 """
1878 """
1878 Generates an API call example via CURL
1879 Generates an API call example via CURL
1879 """
1880 """
1880 curl_call = curl_api_example(method, args)
1881 curl_call = curl_api_example(method, args)
1881
1882
1882 return literal(
1883 return literal(
1883 curl_call +
1884 curl_call +
1884 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1885 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
1885 "and needs to be of `api calls` role."
1886 "and needs to be of `api calls` role."
1886 .format(token_url=route_url('my_account_auth_tokens')))
1887 .format(token_url=route_url('my_account_auth_tokens')))
1887
1888
1888
1889
1889 def notification_description(notification, request):
1890 def notification_description(notification, request):
1890 """
1891 """
1891 Generate notification human readable description based on notification type
1892 Generate notification human readable description based on notification type
1892 """
1893 """
1893 from rhodecode.model.notification import NotificationModel
1894 from rhodecode.model.notification import NotificationModel
1894 return NotificationModel().make_description(
1895 return NotificationModel().make_description(
1895 notification, translate=request.translate)
1896 notification, translate=request.translate)
1896
1897
1897
1898
1898 def go_import_header(request, db_repo=None):
1899 def go_import_header(request, db_repo=None):
1899 """
1900 """
1900 Creates a header for go-import functionality in Go Lang
1901 Creates a header for go-import functionality in Go Lang
1901 """
1902 """
1902
1903
1903 if not db_repo:
1904 if not db_repo:
1904 return
1905 return
1905 if 'go-get' not in request.GET:
1906 if 'go-get' not in request.GET:
1906 return
1907 return
1907
1908
1908 clone_url = db_repo.clone_url()
1909 clone_url = db_repo.clone_url()
1909 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
1910 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
1910 # we have a repo and go-get flag,
1911 # we have a repo and go-get flag,
1911 return literal('<meta name="go-import" content="{} {} {}">'.format(
1912 return literal('<meta name="go-import" content="{} {} {}">'.format(
1912 prefix, db_repo.repo_type, clone_url))
1913 prefix, db_repo.repo_type, clone_url))
1913
1914
1914
1915
1915 def reviewer_as_json(*args, **kwargs):
1916 def reviewer_as_json(*args, **kwargs):
1916 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
1917 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
1917 return _reviewer_as_json(*args, **kwargs)
1918 return _reviewer_as_json(*args, **kwargs)
1918
1919
1919
1920
1920 def get_repo_view_type(request):
1921 def get_repo_view_type(request):
1921 route_name = request.matched_route.name
1922 route_name = request.matched_route.name
1922 route_to_view_type = {
1923 route_to_view_type = {
1923 'repo_changelog': 'commits',
1924 'repo_changelog': 'commits',
1924 'repo_commits': 'commits',
1925 'repo_commits': 'commits',
1925 'repo_files': 'files',
1926 'repo_files': 'files',
1926 'repo_summary': 'summary',
1927 'repo_summary': 'summary',
1927 'repo_commit': 'commit'
1928 'repo_commit': 'commit'
1928 }
1929 }
1929
1930
1930 return route_to_view_type.get(route_name)
1931 return route_to_view_type.get(route_name)
1931
1932
1932
1933
1933 def is_active(menu_entry, selected):
1934 def is_active(menu_entry, selected):
1934 """
1935 """
1935 Returns active class for selecting menus in templates
1936 Returns active class for selecting menus in templates
1936 <li class=${h.is_active('settings', current_active)}></li>
1937 <li class=${h.is_active('settings', current_active)}></li>
1937 """
1938 """
1938 if not isinstance(menu_entry, list):
1939 if not isinstance(menu_entry, list):
1939 menu_entry = [menu_entry]
1940 menu_entry = [menu_entry]
1940
1941
1941 if selected in menu_entry:
1942 if selected in menu_entry:
1942 return "active"
1943 return "active"
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,386 +1,390 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Model for notifications
23 Model for notifications
24 """
24 """
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from pyramid.threadlocal import get_current_request
29 from pyramid.threadlocal import get_current_request
30 from sqlalchemy.sql.expression import false, true
30 from sqlalchemy.sql.expression import false, true
31
31
32 import rhodecode
32 import rhodecode
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.model import BaseModel
34 from rhodecode.model import BaseModel
35 from rhodecode.model.db import Notification, User, UserNotification
35 from rhodecode.model.db import Notification, User, UserNotification
36 from rhodecode.model.meta import Session
36 from rhodecode.model.meta import Session
37 from rhodecode.translation import TranslationString
37 from rhodecode.translation import TranslationString
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class NotificationModel(BaseModel):
42 class NotificationModel(BaseModel):
43
43
44 cls = Notification
44 cls = Notification
45
45
46 def __get_notification(self, notification):
46 def __get_notification(self, notification):
47 if isinstance(notification, Notification):
47 if isinstance(notification, Notification):
48 return notification
48 return notification
49 elif isinstance(notification, (int, long)):
49 elif isinstance(notification, (int, long)):
50 return Notification.get(notification)
50 return Notification.get(notification)
51 else:
51 else:
52 if notification:
52 if notification:
53 raise Exception('notification must be int, long or Instance'
53 raise Exception('notification must be int, long or Instance'
54 ' of Notification got %s' % type(notification))
54 ' of Notification got %s' % type(notification))
55
55
56 def create(
56 def create(
57 self, created_by, notification_subject, notification_body,
57 self, created_by, notification_subject, notification_body,
58 notification_type=Notification.TYPE_MESSAGE, recipients=None,
58 notification_type=Notification.TYPE_MESSAGE, recipients=None,
59 mention_recipients=None, with_email=True, email_kwargs=None):
59 mention_recipients=None, with_email=True, email_kwargs=None):
60 """
60 """
61
61
62 Creates notification of given type
62 Creates notification of given type
63
63
64 :param created_by: int, str or User instance. User who created this
64 :param created_by: int, str or User instance. User who created this
65 notification
65 notification
66 :param notification_subject: subject of notification itself
66 :param notification_subject: subject of notification itself
67 :param notification_body: body of notification text
67 :param notification_body: body of notification text
68 :param notification_type: type of notification, based on that we
68 :param notification_type: type of notification, based on that we
69 pick templates
69 pick templates
70
70
71 :param recipients: list of int, str or User objects, when None
71 :param recipients: list of int, str or User objects, when None
72 is given send to all admins
72 is given send to all admins
73 :param mention_recipients: list of int, str or User objects,
73 :param mention_recipients: list of int, str or User objects,
74 that were mentioned
74 that were mentioned
75 :param with_email: send email with this notification
75 :param with_email: send email with this notification
76 :param email_kwargs: dict with arguments to generate email
76 :param email_kwargs: dict with arguments to generate email
77 """
77 """
78
78
79 from rhodecode.lib.celerylib import tasks, run_task
79 from rhodecode.lib.celerylib import tasks, run_task
80
80
81 if recipients and not getattr(recipients, '__iter__', False):
81 if recipients and not getattr(recipients, '__iter__', False):
82 raise Exception('recipients must be an iterable object')
82 raise Exception('recipients must be an iterable object')
83
83
84 created_by_obj = self._get_user(created_by)
84 created_by_obj = self._get_user(created_by)
85 # default MAIN body if not given
85 # default MAIN body if not given
86 email_kwargs = email_kwargs or {'body': notification_body}
86 email_kwargs = email_kwargs or {'body': notification_body}
87 mention_recipients = mention_recipients or set()
87 mention_recipients = mention_recipients or set()
88
88
89 if not created_by_obj:
89 if not created_by_obj:
90 raise Exception('unknown user %s' % created_by)
90 raise Exception('unknown user %s' % created_by)
91
91
92 if recipients is None:
92 if recipients is None:
93 # recipients is None means to all admins
93 # recipients is None means to all admins
94 recipients_objs = User.query().filter(User.admin == true()).all()
94 recipients_objs = User.query().filter(User.admin == true()).all()
95 log.debug('sending notifications %s to admins: %s',
95 log.debug('sending notifications %s to admins: %s',
96 notification_type, recipients_objs)
96 notification_type, recipients_objs)
97 else:
97 else:
98 recipients_objs = set()
98 recipients_objs = set()
99 for u in recipients:
99 for u in recipients:
100 obj = self._get_user(u)
100 obj = self._get_user(u)
101 if obj:
101 if obj:
102 recipients_objs.add(obj)
102 recipients_objs.add(obj)
103 else: # we didn't find this user, log the error and carry on
103 else: # we didn't find this user, log the error and carry on
104 log.error('cannot notify unknown user %r', u)
104 log.error('cannot notify unknown user %r', u)
105
105
106 if not recipients_objs:
106 if not recipients_objs:
107 raise Exception('no valid recipients specified')
107 raise Exception('no valid recipients specified')
108
108
109 log.debug('sending notifications %s to %s',
109 log.debug('sending notifications %s to %s',
110 notification_type, recipients_objs)
110 notification_type, recipients_objs)
111
111
112 # add mentioned users into recipients
112 # add mentioned users into recipients
113 final_recipients = set(recipients_objs).union(mention_recipients)
113 final_recipients = set(recipients_objs).union(mention_recipients)
114
114
115 notification = Notification.create(
115 notification = Notification.create(
116 created_by=created_by_obj, subject=notification_subject,
116 created_by=created_by_obj, subject=notification_subject,
117 body=notification_body, recipients=final_recipients,
117 body=notification_body, recipients=final_recipients,
118 type_=notification_type
118 type_=notification_type
119 )
119 )
120
120
121 if not with_email: # skip sending email, and just create notification
121 if not with_email: # skip sending email, and just create notification
122 return notification
122 return notification
123
123
124 # don't send email to person who created this comment
124 # don't send email to person who created this comment
125 rec_objs = set(recipients_objs).difference({created_by_obj})
125 rec_objs = set(recipients_objs).difference({created_by_obj})
126
126
127 # now notify all recipients in question
127 # now notify all recipients in question
128
128
129 for recipient in rec_objs.union(mention_recipients):
129 for recipient in rec_objs.union(mention_recipients):
130 # inject current recipient
130 # inject current recipient
131 email_kwargs['recipient'] = recipient
131 email_kwargs['recipient'] = recipient
132 email_kwargs['mention'] = recipient in mention_recipients
132 email_kwargs['mention'] = recipient in mention_recipients
133 (subject, headers, email_body,
133 (subject, headers, email_body,
134 email_body_plaintext) = EmailNotificationModel().render_email(
134 email_body_plaintext) = EmailNotificationModel().render_email(
135 notification_type, **email_kwargs)
135 notification_type, **email_kwargs)
136
136
137 log.debug(
137 log.debug(
138 'Creating notification email task for user:`%s`', recipient)
138 'Creating notification email task for user:`%s`', recipient)
139 task = run_task(
139 task = run_task(
140 tasks.send_email, recipient.email, subject,
140 tasks.send_email, recipient.email, subject,
141 email_body_plaintext, email_body)
141 email_body_plaintext, email_body)
142 log.debug('Created email task: %s', task)
142 log.debug('Created email task: %s', task)
143
143
144 return notification
144 return notification
145
145
146 def delete(self, user, notification):
146 def delete(self, user, notification):
147 # we don't want to remove actual notification just the assignment
147 # we don't want to remove actual notification just the assignment
148 try:
148 try:
149 notification = self.__get_notification(notification)
149 notification = self.__get_notification(notification)
150 user = self._get_user(user)
150 user = self._get_user(user)
151 if notification and user:
151 if notification and user:
152 obj = UserNotification.query()\
152 obj = UserNotification.query()\
153 .filter(UserNotification.user == user)\
153 .filter(UserNotification.user == user)\
154 .filter(UserNotification.notification == notification)\
154 .filter(UserNotification.notification == notification)\
155 .one()
155 .one()
156 Session().delete(obj)
156 Session().delete(obj)
157 return True
157 return True
158 except Exception:
158 except Exception:
159 log.error(traceback.format_exc())
159 log.error(traceback.format_exc())
160 raise
160 raise
161
161
162 def get_for_user(self, user, filter_=None):
162 def get_for_user(self, user, filter_=None):
163 """
163 """
164 Get mentions for given user, filter them if filter dict is given
164 Get mentions for given user, filter them if filter dict is given
165 """
165 """
166 user = self._get_user(user)
166 user = self._get_user(user)
167
167
168 q = UserNotification.query()\
168 q = UserNotification.query()\
169 .filter(UserNotification.user == user)\
169 .filter(UserNotification.user == user)\
170 .join((
170 .join((
171 Notification, UserNotification.notification_id ==
171 Notification, UserNotification.notification_id ==
172 Notification.notification_id))
172 Notification.notification_id))
173 if filter_ == ['all']:
173 if filter_ == ['all']:
174 q = q # no filter
174 q = q # no filter
175 elif filter_ == ['unread']:
175 elif filter_ == ['unread']:
176 q = q.filter(UserNotification.read == false())
176 q = q.filter(UserNotification.read == false())
177 elif filter_:
177 elif filter_:
178 q = q.filter(Notification.type_.in_(filter_))
178 q = q.filter(Notification.type_.in_(filter_))
179
179
180 return q
180 return q
181
181
182 def mark_read(self, user, notification):
182 def mark_read(self, user, notification):
183 try:
183 try:
184 notification = self.__get_notification(notification)
184 notification = self.__get_notification(notification)
185 user = self._get_user(user)
185 user = self._get_user(user)
186 if notification and user:
186 if notification and user:
187 obj = UserNotification.query()\
187 obj = UserNotification.query()\
188 .filter(UserNotification.user == user)\
188 .filter(UserNotification.user == user)\
189 .filter(UserNotification.notification == notification)\
189 .filter(UserNotification.notification == notification)\
190 .one()
190 .one()
191 obj.read = True
191 obj.read = True
192 Session().add(obj)
192 Session().add(obj)
193 return True
193 return True
194 except Exception:
194 except Exception:
195 log.error(traceback.format_exc())
195 log.error(traceback.format_exc())
196 raise
196 raise
197
197
198 def mark_all_read_for_user(self, user, filter_=None):
198 def mark_all_read_for_user(self, user, filter_=None):
199 user = self._get_user(user)
199 user = self._get_user(user)
200 q = UserNotification.query()\
200 q = UserNotification.query()\
201 .filter(UserNotification.user == user)\
201 .filter(UserNotification.user == user)\
202 .filter(UserNotification.read == false())\
202 .filter(UserNotification.read == false())\
203 .join((
203 .join((
204 Notification, UserNotification.notification_id ==
204 Notification, UserNotification.notification_id ==
205 Notification.notification_id))
205 Notification.notification_id))
206 if filter_ == ['unread']:
206 if filter_ == ['unread']:
207 q = q.filter(UserNotification.read == false())
207 q = q.filter(UserNotification.read == false())
208 elif filter_:
208 elif filter_:
209 q = q.filter(Notification.type_.in_(filter_))
209 q = q.filter(Notification.type_.in_(filter_))
210
210
211 # this is a little inefficient but sqlalchemy doesn't support
211 # this is a little inefficient but sqlalchemy doesn't support
212 # update on joined tables :(
212 # update on joined tables :(
213 for obj in q.all():
213 for obj in q.all():
214 obj.read = True
214 obj.read = True
215 Session().add(obj)
215 Session().add(obj)
216
216
217 def get_unread_cnt_for_user(self, user):
217 def get_unread_cnt_for_user(self, user):
218 user = self._get_user(user)
218 user = self._get_user(user)
219 return UserNotification.query()\
219 return UserNotification.query()\
220 .filter(UserNotification.read == false())\
220 .filter(UserNotification.read == false())\
221 .filter(UserNotification.user == user).count()
221 .filter(UserNotification.user == user).count()
222
222
223 def get_unread_for_user(self, user):
223 def get_unread_for_user(self, user):
224 user = self._get_user(user)
224 user = self._get_user(user)
225 return [x.notification for x in UserNotification.query()
225 return [x.notification for x in UserNotification.query()
226 .filter(UserNotification.read == false())
226 .filter(UserNotification.read == false())
227 .filter(UserNotification.user == user).all()]
227 .filter(UserNotification.user == user).all()]
228
228
229 def get_user_notification(self, user, notification):
229 def get_user_notification(self, user, notification):
230 user = self._get_user(user)
230 user = self._get_user(user)
231 notification = self.__get_notification(notification)
231 notification = self.__get_notification(notification)
232
232
233 return UserNotification.query()\
233 return UserNotification.query()\
234 .filter(UserNotification.notification == notification)\
234 .filter(UserNotification.notification == notification)\
235 .filter(UserNotification.user == user).scalar()
235 .filter(UserNotification.user == user).scalar()
236
236
237 def make_description(self, notification, translate, show_age=True):
237 def make_description(self, notification, translate, show_age=True):
238 """
238 """
239 Creates a human readable description based on properties
239 Creates a human readable description based on properties
240 of notification object
240 of notification object
241 """
241 """
242 _ = translate
242 _ = translate
243 _map = {
243 _map = {
244 notification.TYPE_CHANGESET_COMMENT: [
244 notification.TYPE_CHANGESET_COMMENT: [
245 _('%(user)s commented on commit %(date_or_age)s'),
245 _('%(user)s commented on commit %(date_or_age)s'),
246 _('%(user)s commented on commit at %(date_or_age)s'),
246 _('%(user)s commented on commit at %(date_or_age)s'),
247 ],
247 ],
248 notification.TYPE_MESSAGE: [
248 notification.TYPE_MESSAGE: [
249 _('%(user)s sent message %(date_or_age)s'),
249 _('%(user)s sent message %(date_or_age)s'),
250 _('%(user)s sent message at %(date_or_age)s'),
250 _('%(user)s sent message at %(date_or_age)s'),
251 ],
251 ],
252 notification.TYPE_MENTION: [
252 notification.TYPE_MENTION: [
253 _('%(user)s mentioned you %(date_or_age)s'),
253 _('%(user)s mentioned you %(date_or_age)s'),
254 _('%(user)s mentioned you at %(date_or_age)s'),
254 _('%(user)s mentioned you at %(date_or_age)s'),
255 ],
255 ],
256 notification.TYPE_REGISTRATION: [
256 notification.TYPE_REGISTRATION: [
257 _('%(user)s registered in RhodeCode %(date_or_age)s'),
257 _('%(user)s registered in RhodeCode %(date_or_age)s'),
258 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
258 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
259 ],
259 ],
260 notification.TYPE_PULL_REQUEST: [
260 notification.TYPE_PULL_REQUEST: [
261 _('%(user)s opened new pull request %(date_or_age)s'),
261 _('%(user)s opened new pull request %(date_or_age)s'),
262 _('%(user)s opened new pull request at %(date_or_age)s'),
262 _('%(user)s opened new pull request at %(date_or_age)s'),
263 ],
263 ],
264 notification.TYPE_PULL_REQUEST_COMMENT: [
264 notification.TYPE_PULL_REQUEST_COMMENT: [
265 _('%(user)s commented on pull request %(date_or_age)s'),
265 _('%(user)s commented on pull request %(date_or_age)s'),
266 _('%(user)s commented on pull request at %(date_or_age)s'),
266 _('%(user)s commented on pull request at %(date_or_age)s'),
267 ],
267 ],
268 }
268 }
269
269
270 templates = _map[notification.type_]
270 templates = _map[notification.type_]
271
271
272 if show_age:
272 if show_age:
273 template = templates[0]
273 template = templates[0]
274 date_or_age = h.age(notification.created_on)
274 date_or_age = h.age(notification.created_on)
275 if translate:
275 if translate:
276 date_or_age = translate(date_or_age)
276 date_or_age = translate(date_or_age)
277
277
278 if isinstance(date_or_age, TranslationString):
278 if isinstance(date_or_age, TranslationString):
279 date_or_age = date_or_age.interpolate()
279 date_or_age = date_or_age.interpolate()
280
280
281 else:
281 else:
282 template = templates[1]
282 template = templates[1]
283 date_or_age = h.format_date(notification.created_on)
283 date_or_age = h.format_date(notification.created_on)
284
284
285 return template % {
285 return template % {
286 'user': notification.created_by_user.username,
286 'user': notification.created_by_user.username,
287 'date_or_age': date_or_age,
287 'date_or_age': date_or_age,
288 }
288 }
289
289
290
290
291 class EmailNotificationModel(BaseModel):
291 class EmailNotificationModel(BaseModel):
292 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
292 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
293 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
293 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
294 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
294 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
295 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
295 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
296 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
296 TYPE_MAIN = Notification.TYPE_MESSAGE
297 TYPE_MAIN = Notification.TYPE_MESSAGE
297
298
298 TYPE_PASSWORD_RESET = 'password_reset'
299 TYPE_PASSWORD_RESET = 'password_reset'
299 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
300 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
300 TYPE_EMAIL_TEST = 'email_test'
301 TYPE_EMAIL_TEST = 'email_test'
301 TYPE_TEST = 'test'
302 TYPE_TEST = 'test'
302
303
303 email_types = {
304 email_types = {
304 TYPE_MAIN:
305 TYPE_MAIN:
305 'rhodecode:templates/email_templates/main.mako',
306 'rhodecode:templates/email_templates/main.mako',
306 TYPE_TEST:
307 TYPE_TEST:
307 'rhodecode:templates/email_templates/test.mako',
308 'rhodecode:templates/email_templates/test.mako',
308 TYPE_EMAIL_TEST:
309 TYPE_EMAIL_TEST:
309 'rhodecode:templates/email_templates/email_test.mako',
310 'rhodecode:templates/email_templates/email_test.mako',
310 TYPE_REGISTRATION:
311 TYPE_REGISTRATION:
311 'rhodecode:templates/email_templates/user_registration.mako',
312 'rhodecode:templates/email_templates/user_registration.mako',
312 TYPE_PASSWORD_RESET:
313 TYPE_PASSWORD_RESET:
313 'rhodecode:templates/email_templates/password_reset.mako',
314 'rhodecode:templates/email_templates/password_reset.mako',
314 TYPE_PASSWORD_RESET_CONFIRMATION:
315 TYPE_PASSWORD_RESET_CONFIRMATION:
315 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
316 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
316 TYPE_COMMIT_COMMENT:
317 TYPE_COMMIT_COMMENT:
317 'rhodecode:templates/email_templates/commit_comment.mako',
318 'rhodecode:templates/email_templates/commit_comment.mako',
318 TYPE_PULL_REQUEST:
319 TYPE_PULL_REQUEST:
319 'rhodecode:templates/email_templates/pull_request_review.mako',
320 'rhodecode:templates/email_templates/pull_request_review.mako',
320 TYPE_PULL_REQUEST_COMMENT:
321 TYPE_PULL_REQUEST_COMMENT:
321 'rhodecode:templates/email_templates/pull_request_comment.mako',
322 'rhodecode:templates/email_templates/pull_request_comment.mako',
323 TYPE_PULL_REQUEST_UPDATE:
324 'rhodecode:templates/email_templates/pull_request_update.mako',
322 }
325 }
323
326
324 def __init__(self):
327 def __init__(self):
325 """
328 """
326 Example usage::
329 Example usage::
327
330
328 (subject, headers, email_body,
331 (subject, headers, email_body,
329 email_body_plaintext) = EmailNotificationModel().render_email(
332 email_body_plaintext) = EmailNotificationModel().render_email(
330 EmailNotificationModel.TYPE_TEST, **email_kwargs)
333 EmailNotificationModel.TYPE_TEST, **email_kwargs)
331
334
332 """
335 """
333 super(EmailNotificationModel, self).__init__()
336 super(EmailNotificationModel, self).__init__()
334 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
337 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
335
338
336 def _update_kwargs_for_render(self, kwargs):
339 def _update_kwargs_for_render(self, kwargs):
337 """
340 """
338 Inject params required for Mako rendering
341 Inject params required for Mako rendering
339
342
340 :param kwargs:
343 :param kwargs:
341 """
344 """
342
345
343 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
346 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
347 kwargs['rhodecode_version'] = rhodecode.__version__
344 instance_url = h.route_url('home')
348 instance_url = h.route_url('home')
345 _kwargs = {
349 _kwargs = {
346 'instance_url': instance_url,
350 'instance_url': instance_url,
347 'whitespace_filter': self.whitespace_filter
351 'whitespace_filter': self.whitespace_filter
348 }
352 }
349 _kwargs.update(kwargs)
353 _kwargs.update(kwargs)
350 return _kwargs
354 return _kwargs
351
355
352 def whitespace_filter(self, text):
356 def whitespace_filter(self, text):
353 return text.replace('\n', '').replace('\t', '')
357 return text.replace('\n', '').replace('\t', '')
354
358
355 def get_renderer(self, type_, request):
359 def get_renderer(self, type_, request):
356 template_name = self.email_types[type_]
360 template_name = self.email_types[type_]
357 return request.get_partial_renderer(template_name)
361 return request.get_partial_renderer(template_name)
358
362
359 def render_email(self, type_, **kwargs):
363 def render_email(self, type_, **kwargs):
360 """
364 """
361 renders template for email, and returns a tuple of
365 renders template for email, and returns a tuple of
362 (subject, email_headers, email_html_body, email_plaintext_body)
366 (subject, email_headers, email_html_body, email_plaintext_body)
363 """
367 """
364 # translator and helpers inject
368 # translator and helpers inject
365 _kwargs = self._update_kwargs_for_render(kwargs)
369 _kwargs = self._update_kwargs_for_render(kwargs)
366 request = get_current_request()
370 request = get_current_request()
367 email_template = self.get_renderer(type_, request=request)
371 email_template = self.get_renderer(type_, request=request)
368
372
369 subject = email_template.render('subject', **_kwargs)
373 subject = email_template.render('subject', **_kwargs)
370
374
371 try:
375 try:
372 headers = email_template.render('headers', **_kwargs)
376 headers = email_template.render('headers', **_kwargs)
373 except AttributeError:
377 except AttributeError:
374 # it's not defined in template, ok we can skip it
378 # it's not defined in template, ok we can skip it
375 headers = ''
379 headers = ''
376
380
377 try:
381 try:
378 body_plaintext = email_template.render('body_plaintext', **_kwargs)
382 body_plaintext = email_template.render('body_plaintext', **_kwargs)
379 except AttributeError:
383 except AttributeError:
380 # it's not defined in template, ok we can skip it
384 # it's not defined in template, ok we can skip it
381 body_plaintext = ''
385 body_plaintext = ''
382
386
383 # render WHOLE template
387 # render WHOLE template
384 body = email_template.render(None, **_kwargs)
388 body = email_template.render(None, **_kwargs)
385
389
386 return subject, headers, body, body_plaintext
390 return subject, headers, body, body_plaintext
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 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
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 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
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 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
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 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now